1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
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, IndentGuideSettings, LanguageConfig,
26 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
27 language_settings::{
28 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
29 SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
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,
42};
43use serde_json::{self, json};
44use settings::{
45 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
46 ProjectSettingsContent,
47};
48use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
49use std::{
50 iter,
51 sync::atomic::{self, AtomicUsize},
52};
53use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
54use text::ToPoint as _;
55use unindent::Unindent;
56use util::{
57 assert_set_eq, path,
58 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
59 uri,
60};
61use workspace::{
62 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
63 OpenOptions, ViewId,
64 invalid_buffer_view::InvalidBufferView,
65 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
66 register_project_item,
67};
68
69#[gpui::test]
70fn test_edit_events(cx: &mut TestAppContext) {
71 init_test(cx, |_| {});
72
73 let buffer = cx.new(|cx| {
74 let mut buffer = language::Buffer::local("123456", cx);
75 buffer.set_group_interval(Duration::from_secs(1));
76 buffer
77 });
78
79 let events = Rc::new(RefCell::new(Vec::new()));
80 let editor1 = cx.add_window({
81 let events = events.clone();
82 |window, cx| {
83 let entity = cx.entity();
84 cx.subscribe_in(
85 &entity,
86 window,
87 move |_, _, event: &EditorEvent, _, _| match event {
88 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
89 EditorEvent::BufferEdited => {
90 events.borrow_mut().push(("editor1", "buffer edited"))
91 }
92 _ => {}
93 },
94 )
95 .detach();
96 Editor::for_buffer(buffer.clone(), None, window, cx)
97 }
98 });
99
100 let editor2 = cx.add_window({
101 let events = events.clone();
102 |window, cx| {
103 cx.subscribe_in(
104 &cx.entity(),
105 window,
106 move |_, _, event: &EditorEvent, _, _| match event {
107 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
108 EditorEvent::BufferEdited => {
109 events.borrow_mut().push(("editor2", "buffer edited"))
110 }
111 _ => {}
112 },
113 )
114 .detach();
115 Editor::for_buffer(buffer.clone(), None, window, cx)
116 }
117 });
118
119 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
120
121 // Mutating editor 1 will emit an `Edited` event only for that editor.
122 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor1", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Mutating editor 2 will emit an `Edited` event only for that editor.
133 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor2", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Undoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Redoing on editor 1 will emit an `Edited` event only for that editor.
155 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor1", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Undoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // Redoing on editor 2 will emit an `Edited` event only for that editor.
177 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
178 assert_eq!(
179 mem::take(&mut *events.borrow_mut()),
180 [
181 ("editor2", "edited"),
182 ("editor1", "buffer edited"),
183 ("editor2", "buffer edited"),
184 ]
185 );
186
187 // No event is emitted when the mutation is a no-op.
188 _ = editor2.update(cx, |editor, window, cx| {
189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
190 s.select_ranges([0..0])
191 });
192
193 editor.backspace(&Backspace, window, cx);
194 });
195 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
196}
197
198#[gpui::test]
199fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
200 init_test(cx, |_| {});
201
202 let mut now = Instant::now();
203 let group_interval = Duration::from_millis(1);
204 let buffer = cx.new(|cx| {
205 let mut buf = language::Buffer::local("123456", cx);
206 buf.set_group_interval(group_interval);
207 buf
208 });
209 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
210 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
211
212 _ = editor.update(cx, |editor, window, cx| {
213 editor.start_transaction_at(now, window, cx);
214 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
215 s.select_ranges([2..4])
216 });
217
218 editor.insert("cd", window, cx);
219 editor.end_transaction_at(now, cx);
220 assert_eq!(editor.text(cx), "12cd56");
221 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
222
223 editor.start_transaction_at(now, window, cx);
224 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
225 s.select_ranges([4..5])
226 });
227 editor.insert("e", window, cx);
228 editor.end_transaction_at(now, cx);
229 assert_eq!(editor.text(cx), "12cde6");
230 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
231
232 now += group_interval + Duration::from_millis(1);
233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
234 s.select_ranges([2..2])
235 });
236
237 // Simulate an edit in another editor
238 buffer.update(cx, |buffer, cx| {
239 buffer.start_transaction_at(now, cx);
240 buffer.edit([(0..1, "a")], None, cx);
241 buffer.edit([(1..1, "b")], None, cx);
242 buffer.end_transaction_at(now, cx);
243 });
244
245 assert_eq!(editor.text(cx), "ab2cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
247
248 // Last transaction happened past the group interval in a different editor.
249 // Undo it individually and don't restore selections.
250 editor.undo(&Undo, window, cx);
251 assert_eq!(editor.text(cx), "12cde6");
252 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
253
254 // First two transactions happened within the group interval in this editor.
255 // Undo them together and restore selections.
256 editor.undo(&Undo, window, cx);
257 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
258 assert_eq!(editor.text(cx), "123456");
259 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
260
261 // Redo the first two transactions together.
262 editor.redo(&Redo, window, cx);
263 assert_eq!(editor.text(cx), "12cde6");
264 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
265
266 // Redo the last transaction on its own.
267 editor.redo(&Redo, window, cx);
268 assert_eq!(editor.text(cx), "ab2cde6");
269 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
270
271 // Test empty transactions.
272 editor.start_transaction_at(now, window, cx);
273 editor.end_transaction_at(now, cx);
274 editor.undo(&Undo, window, cx);
275 assert_eq!(editor.text(cx), "12cde6");
276 });
277}
278
279#[gpui::test]
280fn test_ime_composition(cx: &mut TestAppContext) {
281 init_test(cx, |_| {});
282
283 let buffer = cx.new(|cx| {
284 let mut buffer = language::Buffer::local("abcde", cx);
285 // Ensure automatic grouping doesn't occur.
286 buffer.set_group_interval(Duration::ZERO);
287 buffer
288 });
289
290 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
291 cx.add_window(|window, cx| {
292 let mut editor = build_editor(buffer.clone(), window, cx);
293
294 // Start a new IME composition.
295 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
296 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
297 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
298 assert_eq!(editor.text(cx), "äbcde");
299 assert_eq!(
300 editor.marked_text_ranges(cx),
301 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
302 );
303
304 // Finalize IME composition.
305 editor.replace_text_in_range(None, "ā", window, cx);
306 assert_eq!(editor.text(cx), "ābcde");
307 assert_eq!(editor.marked_text_ranges(cx), None);
308
309 // IME composition edits are grouped and are undone/redone at once.
310 editor.undo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "abcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313 editor.redo(&Default::default(), window, cx);
314 assert_eq!(editor.text(cx), "ābcde");
315 assert_eq!(editor.marked_text_ranges(cx), None);
316
317 // Start a new IME composition.
318 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
319 assert_eq!(
320 editor.marked_text_ranges(cx),
321 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
322 );
323
324 // Undoing during an IME composition cancels it.
325 editor.undo(&Default::default(), window, cx);
326 assert_eq!(editor.text(cx), "ābcde");
327 assert_eq!(editor.marked_text_ranges(cx), None);
328
329 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
330 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
331 assert_eq!(editor.text(cx), "ābcdè");
332 assert_eq!(
333 editor.marked_text_ranges(cx),
334 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
335 );
336
337 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
338 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
339 assert_eq!(editor.text(cx), "ābcdę");
340 assert_eq!(editor.marked_text_ranges(cx), None);
341
342 // Start a new IME composition with multiple cursors.
343 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
344 s.select_ranges([
345 OffsetUtf16(1)..OffsetUtf16(1),
346 OffsetUtf16(3)..OffsetUtf16(3),
347 OffsetUtf16(5)..OffsetUtf16(5),
348 ])
349 });
350 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
351 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
352 assert_eq!(
353 editor.marked_text_ranges(cx),
354 Some(vec![
355 OffsetUtf16(0)..OffsetUtf16(3),
356 OffsetUtf16(4)..OffsetUtf16(7),
357 OffsetUtf16(8)..OffsetUtf16(11)
358 ])
359 );
360
361 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
362 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
363 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
364 assert_eq!(
365 editor.marked_text_ranges(cx),
366 Some(vec![
367 OffsetUtf16(1)..OffsetUtf16(2),
368 OffsetUtf16(5)..OffsetUtf16(6),
369 OffsetUtf16(9)..OffsetUtf16(10)
370 ])
371 );
372
373 // Finalize IME composition with multiple cursors.
374 editor.replace_text_in_range(Some(9..10), "2", window, cx);
375 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
376 assert_eq!(editor.marked_text_ranges(cx), None);
377
378 editor
379 });
380}
381
382#[gpui::test]
383fn test_selection_with_mouse(cx: &mut TestAppContext) {
384 init_test(cx, |_| {});
385
386 let editor = cx.add_window(|window, cx| {
387 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
388 build_editor(buffer, window, cx)
389 });
390
391 _ = editor.update(cx, |editor, window, cx| {
392 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
393 });
394 assert_eq!(
395 editor
396 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
397 .unwrap(),
398 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
399 );
400
401 _ = editor.update(cx, |editor, window, cx| {
402 editor.update_selection(
403 DisplayPoint::new(DisplayRow(3), 3),
404 0,
405 gpui::Point::<f32>::default(),
406 window,
407 cx,
408 );
409 });
410
411 assert_eq!(
412 editor
413 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
414 .unwrap(),
415 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
416 );
417
418 _ = editor.update(cx, |editor, window, cx| {
419 editor.update_selection(
420 DisplayPoint::new(DisplayRow(1), 1),
421 0,
422 gpui::Point::<f32>::default(),
423 window,
424 cx,
425 );
426 });
427
428 assert_eq!(
429 editor
430 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
431 .unwrap(),
432 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
433 );
434
435 _ = editor.update(cx, |editor, window, cx| {
436 editor.end_selection(window, cx);
437 editor.update_selection(
438 DisplayPoint::new(DisplayRow(3), 3),
439 0,
440 gpui::Point::<f32>::default(),
441 window,
442 cx,
443 );
444 });
445
446 assert_eq!(
447 editor
448 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
449 .unwrap(),
450 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
451 );
452
453 _ = editor.update(cx, |editor, window, cx| {
454 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
455 editor.update_selection(
456 DisplayPoint::new(DisplayRow(0), 0),
457 0,
458 gpui::Point::<f32>::default(),
459 window,
460 cx,
461 );
462 });
463
464 assert_eq!(
465 editor
466 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
467 .unwrap(),
468 [
469 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
470 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
471 ]
472 );
473
474 _ = editor.update(cx, |editor, window, cx| {
475 editor.end_selection(window, cx);
476 });
477
478 assert_eq!(
479 editor
480 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
481 .unwrap(),
482 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
483 );
484}
485
486#[gpui::test]
487fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
488 init_test(cx, |_| {});
489
490 let editor = cx.add_window(|window, cx| {
491 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
492 build_editor(buffer, window, cx)
493 });
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.end_selection(window, cx);
501 });
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
505 });
506
507 _ = editor.update(cx, |editor, window, cx| {
508 editor.end_selection(window, cx);
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
514 .unwrap(),
515 [
516 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
517 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
518 ]
519 );
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
523 });
524
525 _ = editor.update(cx, |editor, window, cx| {
526 editor.end_selection(window, cx);
527 });
528
529 assert_eq!(
530 editor
531 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
532 .unwrap(),
533 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
534 );
535}
536
537#[gpui::test]
538fn test_canceling_pending_selection(cx: &mut TestAppContext) {
539 init_test(cx, |_| {});
540
541 let editor = cx.add_window(|window, cx| {
542 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
543 build_editor(buffer, window, cx)
544 });
545
546 _ = editor.update(cx, |editor, window, cx| {
547 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
548 assert_eq!(
549 editor.selections.display_ranges(cx),
550 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
551 );
552 });
553
554 _ = editor.update(cx, |editor, window, cx| {
555 editor.update_selection(
556 DisplayPoint::new(DisplayRow(3), 3),
557 0,
558 gpui::Point::<f32>::default(),
559 window,
560 cx,
561 );
562 assert_eq!(
563 editor.selections.display_ranges(cx),
564 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
565 );
566 });
567
568 _ = editor.update(cx, |editor, window, cx| {
569 editor.cancel(&Cancel, window, cx);
570 editor.update_selection(
571 DisplayPoint::new(DisplayRow(1), 1),
572 0,
573 gpui::Point::<f32>::default(),
574 window,
575 cx,
576 );
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
580 );
581 });
582}
583
584#[gpui::test]
585fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
586 init_test(cx, |_| {});
587
588 let editor = cx.add_window(|window, cx| {
589 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
590 build_editor(buffer, window, cx)
591 });
592
593 _ = editor.update(cx, |editor, window, cx| {
594 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
598 );
599
600 editor.move_down(&Default::default(), window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
604 );
605
606 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
610 );
611
612 editor.move_up(&Default::default(), window, cx);
613 assert_eq!(
614 editor.selections.display_ranges(cx),
615 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
616 );
617 });
618}
619
620#[gpui::test]
621fn test_clone(cx: &mut TestAppContext) {
622 init_test(cx, |_| {});
623
624 let (text, selection_ranges) = marked_text_ranges(
625 indoc! {"
626 one
627 two
628 threeˇ
629 four
630 fiveˇ
631 "},
632 true,
633 );
634
635 let editor = cx.add_window(|window, cx| {
636 let buffer = MultiBuffer::build_simple(&text, cx);
637 build_editor(buffer, window, cx)
638 });
639
640 _ = editor.update(cx, |editor, window, cx| {
641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
642 s.select_ranges(selection_ranges.clone())
643 });
644 editor.fold_creases(
645 vec![
646 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
647 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
648 ],
649 true,
650 window,
651 cx,
652 );
653 });
654
655 let cloned_editor = editor
656 .update(cx, |editor, _, cx| {
657 cx.open_window(Default::default(), |window, cx| {
658 cx.new(|cx| editor.clone(window, cx))
659 })
660 })
661 .unwrap()
662 .unwrap();
663
664 let snapshot = editor
665 .update(cx, |e, window, cx| e.snapshot(window, cx))
666 .unwrap();
667 let cloned_snapshot = cloned_editor
668 .update(cx, |e, window, cx| e.snapshot(window, cx))
669 .unwrap();
670
671 assert_eq!(
672 cloned_editor
673 .update(cx, |e, _, cx| e.display_text(cx))
674 .unwrap(),
675 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
676 );
677 assert_eq!(
678 cloned_snapshot
679 .folds_in_range(0..text.len())
680 .collect::<Vec<_>>(),
681 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
682 );
683 assert_set_eq!(
684 cloned_editor
685 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
686 .unwrap(),
687 editor
688 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
689 .unwrap()
690 );
691 assert_set_eq!(
692 cloned_editor
693 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
694 .unwrap(),
695 editor
696 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
697 .unwrap()
698 );
699}
700
701#[gpui::test]
702async fn test_navigation_history(cx: &mut TestAppContext) {
703 init_test(cx, |_| {});
704
705 use workspace::item::Item;
706
707 let fs = FakeFs::new(cx.executor());
708 let project = Project::test(fs, [], cx).await;
709 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
710 let pane = workspace
711 .update(cx, |workspace, _, _| workspace.active_pane().clone())
712 .unwrap();
713
714 _ = workspace.update(cx, |_v, window, cx| {
715 cx.new(|cx| {
716 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
717 let mut editor = build_editor(buffer, window, cx);
718 let handle = cx.entity();
719 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
720
721 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
722 editor.nav_history.as_mut().unwrap().pop_backward(cx)
723 }
724
725 // Move the cursor a small distance.
726 // Nothing is added to the navigation history.
727 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
728 s.select_display_ranges([
729 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
730 ])
731 });
732 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
733 s.select_display_ranges([
734 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
735 ])
736 });
737 assert!(pop_history(&mut editor, cx).is_none());
738
739 // Move the cursor a large distance.
740 // The history can jump back to the previous position.
741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
742 s.select_display_ranges([
743 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
744 ])
745 });
746 let nav_entry = pop_history(&mut editor, cx).unwrap();
747 editor.navigate(nav_entry.data.unwrap(), window, cx);
748 assert_eq!(nav_entry.item.id(), cx.entity_id());
749 assert_eq!(
750 editor.selections.display_ranges(cx),
751 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
752 );
753 assert!(pop_history(&mut editor, cx).is_none());
754
755 // Move the cursor a small distance via the mouse.
756 // Nothing is added to the navigation history.
757 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
758 editor.end_selection(window, cx);
759 assert_eq!(
760 editor.selections.display_ranges(cx),
761 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
762 );
763 assert!(pop_history(&mut editor, cx).is_none());
764
765 // Move the cursor a large distance via the mouse.
766 // The history can jump back to the previous position.
767 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
768 editor.end_selection(window, cx);
769 assert_eq!(
770 editor.selections.display_ranges(cx),
771 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
772 );
773 let nav_entry = pop_history(&mut editor, cx).unwrap();
774 editor.navigate(nav_entry.data.unwrap(), window, cx);
775 assert_eq!(nav_entry.item.id(), cx.entity_id());
776 assert_eq!(
777 editor.selections.display_ranges(cx),
778 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
779 );
780 assert!(pop_history(&mut editor, cx).is_none());
781
782 // Set scroll position to check later
783 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
784 let original_scroll_position = editor.scroll_manager.anchor();
785
786 // Jump to the end of the document and adjust scroll
787 editor.move_to_end(&MoveToEnd, window, cx);
788 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
789 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
790
791 let nav_entry = pop_history(&mut editor, cx).unwrap();
792 editor.navigate(nav_entry.data.unwrap(), window, cx);
793 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
794
795 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
796 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
797 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
798 let invalid_point = Point::new(9999, 0);
799 editor.navigate(
800 Box::new(NavigationData {
801 cursor_anchor: invalid_anchor,
802 cursor_position: invalid_point,
803 scroll_anchor: ScrollAnchor {
804 anchor: invalid_anchor,
805 offset: Default::default(),
806 },
807 scroll_top_row: invalid_point.row,
808 }),
809 window,
810 cx,
811 );
812 assert_eq!(
813 editor.selections.display_ranges(cx),
814 &[editor.max_point(cx)..editor.max_point(cx)]
815 );
816 assert_eq!(
817 editor.scroll_position(cx),
818 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
819 );
820
821 editor
822 })
823 });
824}
825
826#[gpui::test]
827fn test_cancel(cx: &mut TestAppContext) {
828 init_test(cx, |_| {});
829
830 let editor = cx.add_window(|window, cx| {
831 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
832 build_editor(buffer, window, cx)
833 });
834
835 _ = editor.update(cx, |editor, window, cx| {
836 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
837 editor.update_selection(
838 DisplayPoint::new(DisplayRow(1), 1),
839 0,
840 gpui::Point::<f32>::default(),
841 window,
842 cx,
843 );
844 editor.end_selection(window, cx);
845
846 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
847 editor.update_selection(
848 DisplayPoint::new(DisplayRow(0), 3),
849 0,
850 gpui::Point::<f32>::default(),
851 window,
852 cx,
853 );
854 editor.end_selection(window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [
858 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
859 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
860 ]
861 );
862 });
863
864 _ = editor.update(cx, |editor, window, cx| {
865 editor.cancel(&Cancel, window, cx);
866 assert_eq!(
867 editor.selections.display_ranges(cx),
868 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
869 );
870 });
871
872 _ = editor.update(cx, |editor, window, cx| {
873 editor.cancel(&Cancel, window, cx);
874 assert_eq!(
875 editor.selections.display_ranges(cx),
876 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
877 );
878 });
879}
880
881#[gpui::test]
882fn test_fold_action(cx: &mut TestAppContext) {
883 init_test(cx, |_| {});
884
885 let editor = cx.add_window(|window, cx| {
886 let buffer = MultiBuffer::build_simple(
887 &"
888 impl Foo {
889 // Hello!
890
891 fn a() {
892 1
893 }
894
895 fn b() {
896 2
897 }
898
899 fn c() {
900 3
901 }
902 }
903 "
904 .unindent(),
905 cx,
906 );
907 build_editor(buffer, window, cx)
908 });
909
910 _ = editor.update(cx, |editor, window, cx| {
911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
912 s.select_display_ranges([
913 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
914 ]);
915 });
916 editor.fold(&Fold, window, cx);
917 assert_eq!(
918 editor.display_text(cx),
919 "
920 impl Foo {
921 // Hello!
922
923 fn a() {
924 1
925 }
926
927 fn b() {⋯
928 }
929
930 fn c() {⋯
931 }
932 }
933 "
934 .unindent(),
935 );
936
937 editor.fold(&Fold, window, cx);
938 assert_eq!(
939 editor.display_text(cx),
940 "
941 impl Foo {⋯
942 }
943 "
944 .unindent(),
945 );
946
947 editor.unfold_lines(&UnfoldLines, window, cx);
948 assert_eq!(
949 editor.display_text(cx),
950 "
951 impl Foo {
952 // Hello!
953
954 fn a() {
955 1
956 }
957
958 fn b() {⋯
959 }
960
961 fn c() {⋯
962 }
963 }
964 "
965 .unindent(),
966 );
967
968 editor.unfold_lines(&UnfoldLines, window, cx);
969 assert_eq!(
970 editor.display_text(cx),
971 editor.buffer.read(cx).read(cx).text()
972 );
973 });
974}
975
976#[gpui::test]
977fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
978 init_test(cx, |_| {});
979
980 let editor = cx.add_window(|window, cx| {
981 let buffer = MultiBuffer::build_simple(
982 &"
983 class Foo:
984 # Hello!
985
986 def a():
987 print(1)
988
989 def b():
990 print(2)
991
992 def c():
993 print(3)
994 "
995 .unindent(),
996 cx,
997 );
998 build_editor(buffer, window, cx)
999 });
1000
1001 _ = editor.update(cx, |editor, window, cx| {
1002 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1003 s.select_display_ranges([
1004 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1005 ]);
1006 });
1007 editor.fold(&Fold, window, cx);
1008 assert_eq!(
1009 editor.display_text(cx),
1010 "
1011 class Foo:
1012 # Hello!
1013
1014 def a():
1015 print(1)
1016
1017 def b():⋯
1018
1019 def c():⋯
1020 "
1021 .unindent(),
1022 );
1023
1024 editor.fold(&Fold, window, cx);
1025 assert_eq!(
1026 editor.display_text(cx),
1027 "
1028 class Foo:⋯
1029 "
1030 .unindent(),
1031 );
1032
1033 editor.unfold_lines(&UnfoldLines, window, cx);
1034 assert_eq!(
1035 editor.display_text(cx),
1036 "
1037 class Foo:
1038 # Hello!
1039
1040 def a():
1041 print(1)
1042
1043 def b():⋯
1044
1045 def c():⋯
1046 "
1047 .unindent(),
1048 );
1049
1050 editor.unfold_lines(&UnfoldLines, window, cx);
1051 assert_eq!(
1052 editor.display_text(cx),
1053 editor.buffer.read(cx).read(cx).text()
1054 );
1055 });
1056}
1057
1058#[gpui::test]
1059fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1060 init_test(cx, |_| {});
1061
1062 let editor = cx.add_window(|window, cx| {
1063 let buffer = MultiBuffer::build_simple(
1064 &"
1065 class Foo:
1066 # Hello!
1067
1068 def a():
1069 print(1)
1070
1071 def b():
1072 print(2)
1073
1074
1075 def c():
1076 print(3)
1077
1078
1079 "
1080 .unindent(),
1081 cx,
1082 );
1083 build_editor(buffer, window, cx)
1084 });
1085
1086 _ = editor.update(cx, |editor, window, cx| {
1087 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1088 s.select_display_ranges([
1089 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1090 ]);
1091 });
1092 editor.fold(&Fold, window, cx);
1093 assert_eq!(
1094 editor.display_text(cx),
1095 "
1096 class Foo:
1097 # Hello!
1098
1099 def a():
1100 print(1)
1101
1102 def b():⋯
1103
1104
1105 def c():⋯
1106
1107
1108 "
1109 .unindent(),
1110 );
1111
1112 editor.fold(&Fold, window, cx);
1113 assert_eq!(
1114 editor.display_text(cx),
1115 "
1116 class Foo:⋯
1117
1118
1119 "
1120 .unindent(),
1121 );
1122
1123 editor.unfold_lines(&UnfoldLines, window, cx);
1124 assert_eq!(
1125 editor.display_text(cx),
1126 "
1127 class Foo:
1128 # Hello!
1129
1130 def a():
1131 print(1)
1132
1133 def b():⋯
1134
1135
1136 def c():⋯
1137
1138
1139 "
1140 .unindent(),
1141 );
1142
1143 editor.unfold_lines(&UnfoldLines, window, cx);
1144 assert_eq!(
1145 editor.display_text(cx),
1146 editor.buffer.read(cx).read(cx).text()
1147 );
1148 });
1149}
1150
1151#[gpui::test]
1152fn test_fold_at_level(cx: &mut TestAppContext) {
1153 init_test(cx, |_| {});
1154
1155 let editor = cx.add_window(|window, cx| {
1156 let buffer = MultiBuffer::build_simple(
1157 &"
1158 class Foo:
1159 # Hello!
1160
1161 def a():
1162 print(1)
1163
1164 def b():
1165 print(2)
1166
1167
1168 class Bar:
1169 # World!
1170
1171 def a():
1172 print(1)
1173
1174 def b():
1175 print(2)
1176
1177
1178 "
1179 .unindent(),
1180 cx,
1181 );
1182 build_editor(buffer, window, cx)
1183 });
1184
1185 _ = editor.update(cx, |editor, window, cx| {
1186 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1187 assert_eq!(
1188 editor.display_text(cx),
1189 "
1190 class Foo:
1191 # Hello!
1192
1193 def a():⋯
1194
1195 def b():⋯
1196
1197
1198 class Bar:
1199 # World!
1200
1201 def a():⋯
1202
1203 def b():⋯
1204
1205
1206 "
1207 .unindent(),
1208 );
1209
1210 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 "
1214 class Foo:⋯
1215
1216
1217 class Bar:⋯
1218
1219
1220 "
1221 .unindent(),
1222 );
1223
1224 editor.unfold_all(&UnfoldAll, window, cx);
1225 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1226 assert_eq!(
1227 editor.display_text(cx),
1228 "
1229 class Foo:
1230 # Hello!
1231
1232 def a():
1233 print(1)
1234
1235 def b():
1236 print(2)
1237
1238
1239 class Bar:
1240 # World!
1241
1242 def a():
1243 print(1)
1244
1245 def b():
1246 print(2)
1247
1248
1249 "
1250 .unindent(),
1251 );
1252
1253 assert_eq!(
1254 editor.display_text(cx),
1255 editor.buffer.read(cx).read(cx).text()
1256 );
1257 });
1258}
1259
1260#[gpui::test]
1261fn test_move_cursor(cx: &mut TestAppContext) {
1262 init_test(cx, |_| {});
1263
1264 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1265 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1266
1267 buffer.update(cx, |buffer, cx| {
1268 buffer.edit(
1269 vec![
1270 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1271 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1272 ],
1273 None,
1274 cx,
1275 );
1276 });
1277 _ = editor.update(cx, |editor, window, cx| {
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1281 );
1282
1283 editor.move_down(&MoveDown, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1287 );
1288
1289 editor.move_right(&MoveRight, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1293 );
1294
1295 editor.move_left(&MoveLeft, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1299 );
1300
1301 editor.move_up(&MoveUp, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1305 );
1306
1307 editor.move_to_end(&MoveToEnd, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1311 );
1312
1313 editor.move_to_beginning(&MoveToBeginning, window, cx);
1314 assert_eq!(
1315 editor.selections.display_ranges(cx),
1316 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1317 );
1318
1319 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1320 s.select_display_ranges([
1321 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1322 ]);
1323 });
1324 editor.select_to_beginning(&SelectToBeginning, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1328 );
1329
1330 editor.select_to_end(&SelectToEnd, window, cx);
1331 assert_eq!(
1332 editor.selections.display_ranges(cx),
1333 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1334 );
1335 });
1336}
1337
1338#[gpui::test]
1339fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1340 init_test(cx, |_| {});
1341
1342 let editor = cx.add_window(|window, cx| {
1343 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1344 build_editor(buffer, window, cx)
1345 });
1346
1347 assert_eq!('🟥'.len_utf8(), 4);
1348 assert_eq!('α'.len_utf8(), 2);
1349
1350 _ = editor.update(cx, |editor, window, cx| {
1351 editor.fold_creases(
1352 vec![
1353 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1354 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1355 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1356 ],
1357 true,
1358 window,
1359 cx,
1360 );
1361 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1362
1363 editor.move_right(&MoveRight, window, cx);
1364 assert_eq!(
1365 editor.selections.display_ranges(cx),
1366 &[empty_range(0, "🟥".len())]
1367 );
1368 editor.move_right(&MoveRight, window, cx);
1369 assert_eq!(
1370 editor.selections.display_ranges(cx),
1371 &[empty_range(0, "🟥🟧".len())]
1372 );
1373 editor.move_right(&MoveRight, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(0, "🟥🟧⋯".len())]
1377 );
1378
1379 editor.move_down(&MoveDown, window, cx);
1380 assert_eq!(
1381 editor.selections.display_ranges(cx),
1382 &[empty_range(1, "ab⋯e".len())]
1383 );
1384 editor.move_left(&MoveLeft, window, cx);
1385 assert_eq!(
1386 editor.selections.display_ranges(cx),
1387 &[empty_range(1, "ab⋯".len())]
1388 );
1389 editor.move_left(&MoveLeft, window, cx);
1390 assert_eq!(
1391 editor.selections.display_ranges(cx),
1392 &[empty_range(1, "ab".len())]
1393 );
1394 editor.move_left(&MoveLeft, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(1, "a".len())]
1398 );
1399
1400 editor.move_down(&MoveDown, window, cx);
1401 assert_eq!(
1402 editor.selections.display_ranges(cx),
1403 &[empty_range(2, "α".len())]
1404 );
1405 editor.move_right(&MoveRight, window, cx);
1406 assert_eq!(
1407 editor.selections.display_ranges(cx),
1408 &[empty_range(2, "αβ".len())]
1409 );
1410 editor.move_right(&MoveRight, window, cx);
1411 assert_eq!(
1412 editor.selections.display_ranges(cx),
1413 &[empty_range(2, "αβ⋯".len())]
1414 );
1415 editor.move_right(&MoveRight, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(2, "αβ⋯ε".len())]
1419 );
1420
1421 editor.move_up(&MoveUp, window, cx);
1422 assert_eq!(
1423 editor.selections.display_ranges(cx),
1424 &[empty_range(1, "ab⋯e".len())]
1425 );
1426 editor.move_down(&MoveDown, window, cx);
1427 assert_eq!(
1428 editor.selections.display_ranges(cx),
1429 &[empty_range(2, "αβ⋯ε".len())]
1430 );
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(1, "ab⋯e".len())]
1435 );
1436
1437 editor.move_up(&MoveUp, window, cx);
1438 assert_eq!(
1439 editor.selections.display_ranges(cx),
1440 &[empty_range(0, "🟥🟧".len())]
1441 );
1442 editor.move_left(&MoveLeft, window, cx);
1443 assert_eq!(
1444 editor.selections.display_ranges(cx),
1445 &[empty_range(0, "🟥".len())]
1446 );
1447 editor.move_left(&MoveLeft, window, cx);
1448 assert_eq!(
1449 editor.selections.display_ranges(cx),
1450 &[empty_range(0, "".len())]
1451 );
1452 });
1453}
1454
1455#[gpui::test]
1456fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1457 init_test(cx, |_| {});
1458
1459 let editor = cx.add_window(|window, cx| {
1460 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1461 build_editor(buffer, window, cx)
1462 });
1463 _ = editor.update(cx, |editor, window, cx| {
1464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1465 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1466 });
1467
1468 // moving above start of document should move selection to start of document,
1469 // but the next move down should still be at the original goal_x
1470 editor.move_up(&MoveUp, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(0, "".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(1, "abcd".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(2, "αβγ".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(3, "abcd".len())]
1492 );
1493
1494 editor.move_down(&MoveDown, window, cx);
1495 assert_eq!(
1496 editor.selections.display_ranges(cx),
1497 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1498 );
1499
1500 // moving past end of document should not change goal_x
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_down(&MoveDown, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(5, "".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(3, "abcd".len())]
1523 );
1524
1525 editor.move_up(&MoveUp, window, cx);
1526 assert_eq!(
1527 editor.selections.display_ranges(cx),
1528 &[empty_range(2, "αβγ".len())]
1529 );
1530 });
1531}
1532
1533#[gpui::test]
1534fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1535 init_test(cx, |_| {});
1536 let move_to_beg = MoveToBeginningOfLine {
1537 stop_at_soft_wraps: true,
1538 stop_at_indent: true,
1539 };
1540
1541 let delete_to_beg = DeleteToBeginningOfLine {
1542 stop_at_indent: false,
1543 };
1544
1545 let move_to_end = MoveToEndOfLine {
1546 stop_at_soft_wraps: true,
1547 };
1548
1549 let editor = cx.add_window(|window, cx| {
1550 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1551 build_editor(buffer, window, cx)
1552 });
1553 _ = editor.update(cx, |editor, window, cx| {
1554 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1555 s.select_display_ranges([
1556 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1557 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1558 ]);
1559 });
1560 });
1561
1562 _ = editor.update(cx, |editor, window, cx| {
1563 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1564 assert_eq!(
1565 editor.selections.display_ranges(cx),
1566 &[
1567 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1568 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1569 ]
1570 );
1571 });
1572
1573 _ = editor.update(cx, |editor, window, cx| {
1574 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1575 assert_eq!(
1576 editor.selections.display_ranges(cx),
1577 &[
1578 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1579 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1580 ]
1581 );
1582 });
1583
1584 _ = editor.update(cx, |editor, window, cx| {
1585 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1586 assert_eq!(
1587 editor.selections.display_ranges(cx),
1588 &[
1589 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1590 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1591 ]
1592 );
1593 });
1594
1595 _ = editor.update(cx, |editor, window, cx| {
1596 editor.move_to_end_of_line(&move_to_end, window, cx);
1597 assert_eq!(
1598 editor.selections.display_ranges(cx),
1599 &[
1600 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1601 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1602 ]
1603 );
1604 });
1605
1606 // Moving to the end of line again is a no-op.
1607 _ = editor.update(cx, |editor, window, cx| {
1608 editor.move_to_end_of_line(&move_to_end, window, cx);
1609 assert_eq!(
1610 editor.selections.display_ranges(cx),
1611 &[
1612 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1613 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1614 ]
1615 );
1616 });
1617
1618 _ = editor.update(cx, |editor, window, cx| {
1619 editor.move_left(&MoveLeft, window, cx);
1620 editor.select_to_beginning_of_line(
1621 &SelectToBeginningOfLine {
1622 stop_at_soft_wraps: true,
1623 stop_at_indent: true,
1624 },
1625 window,
1626 cx,
1627 );
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[
1631 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1632 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1633 ]
1634 );
1635 });
1636
1637 _ = editor.update(cx, |editor, window, cx| {
1638 editor.select_to_beginning_of_line(
1639 &SelectToBeginningOfLine {
1640 stop_at_soft_wraps: true,
1641 stop_at_indent: true,
1642 },
1643 window,
1644 cx,
1645 );
1646 assert_eq!(
1647 editor.selections.display_ranges(cx),
1648 &[
1649 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1650 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1651 ]
1652 );
1653 });
1654
1655 _ = editor.update(cx, |editor, window, cx| {
1656 editor.select_to_beginning_of_line(
1657 &SelectToBeginningOfLine {
1658 stop_at_soft_wraps: true,
1659 stop_at_indent: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.select_to_end_of_line(
1675 &SelectToEndOfLine {
1676 stop_at_soft_wraps: true,
1677 },
1678 window,
1679 cx,
1680 );
1681 assert_eq!(
1682 editor.selections.display_ranges(cx),
1683 &[
1684 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1685 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1686 ]
1687 );
1688 });
1689
1690 _ = editor.update(cx, |editor, window, cx| {
1691 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1692 assert_eq!(editor.display_text(cx), "ab\n de");
1693 assert_eq!(
1694 editor.selections.display_ranges(cx),
1695 &[
1696 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1697 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1698 ]
1699 );
1700 });
1701
1702 _ = editor.update(cx, |editor, window, cx| {
1703 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1704 assert_eq!(editor.display_text(cx), "\n");
1705 assert_eq!(
1706 editor.selections.display_ranges(cx),
1707 &[
1708 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1709 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1710 ]
1711 );
1712 });
1713}
1714
1715#[gpui::test]
1716fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1717 init_test(cx, |_| {});
1718 let move_to_beg = MoveToBeginningOfLine {
1719 stop_at_soft_wraps: false,
1720 stop_at_indent: false,
1721 };
1722
1723 let move_to_end = MoveToEndOfLine {
1724 stop_at_soft_wraps: false,
1725 };
1726
1727 let editor = cx.add_window(|window, cx| {
1728 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1729 build_editor(buffer, window, cx)
1730 });
1731
1732 _ = editor.update(cx, |editor, window, cx| {
1733 editor.set_wrap_width(Some(140.0.into()), cx);
1734
1735 // We expect the following lines after wrapping
1736 // ```
1737 // thequickbrownfox
1738 // jumpedoverthelazydo
1739 // gs
1740 // ```
1741 // The final `gs` was soft-wrapped onto a new line.
1742 assert_eq!(
1743 "thequickbrownfox\njumpedoverthelaz\nydogs",
1744 editor.display_text(cx),
1745 );
1746
1747 // First, let's assert behavior on the first line, that was not soft-wrapped.
1748 // Start the cursor at the `k` on the first line
1749 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1750 s.select_display_ranges([
1751 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1752 ]);
1753 });
1754
1755 // Moving to the beginning of the line should put us at the beginning of the line.
1756 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1757 assert_eq!(
1758 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1759 editor.selections.display_ranges(cx)
1760 );
1761
1762 // Moving to the end of the line should put us at the end of the line.
1763 editor.move_to_end_of_line(&move_to_end, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1770 // Start the cursor at the last line (`y` that was wrapped to a new line)
1771 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1772 s.select_display_ranges([
1773 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1774 ]);
1775 });
1776
1777 // Moving to the beginning of the line should put us at the start of the second line of
1778 // display text, i.e., the `j`.
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 beginning of the line again should be a no-op.
1786 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1787 assert_eq!(
1788 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1789 editor.selections.display_ranges(cx)
1790 );
1791
1792 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1793 // next display line.
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 // Moving to the end of the line again should be a no-op.
1801 editor.move_to_end_of_line(&move_to_end, window, cx);
1802 assert_eq!(
1803 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1804 editor.selections.display_ranges(cx)
1805 );
1806 });
1807}
1808
1809#[gpui::test]
1810fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1811 init_test(cx, |_| {});
1812
1813 let move_to_beg = MoveToBeginningOfLine {
1814 stop_at_soft_wraps: true,
1815 stop_at_indent: true,
1816 };
1817
1818 let select_to_beg = SelectToBeginningOfLine {
1819 stop_at_soft_wraps: true,
1820 stop_at_indent: true,
1821 };
1822
1823 let delete_to_beg = DeleteToBeginningOfLine {
1824 stop_at_indent: true,
1825 };
1826
1827 let move_to_end = MoveToEndOfLine {
1828 stop_at_soft_wraps: false,
1829 };
1830
1831 let editor = cx.add_window(|window, cx| {
1832 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1833 build_editor(buffer, window, cx)
1834 });
1835
1836 _ = editor.update(cx, |editor, window, cx| {
1837 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1838 s.select_display_ranges([
1839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1840 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1841 ]);
1842 });
1843
1844 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1845 // and the second cursor at the first non-whitespace character in the line.
1846 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1847 assert_eq!(
1848 editor.selections.display_ranges(cx),
1849 &[
1850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1851 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1852 ]
1853 );
1854
1855 // Moving to the beginning of the line again should be a no-op for the first cursor,
1856 // and should move the second cursor to the beginning of the line.
1857 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1858 assert_eq!(
1859 editor.selections.display_ranges(cx),
1860 &[
1861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1862 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1863 ]
1864 );
1865
1866 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1867 // and should move the second cursor back to the first non-whitespace character in the line.
1868 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1869 assert_eq!(
1870 editor.selections.display_ranges(cx),
1871 &[
1872 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1873 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1874 ]
1875 );
1876
1877 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1878 // and to the first non-whitespace character in the line for the second cursor.
1879 editor.move_to_end_of_line(&move_to_end, window, cx);
1880 editor.move_left(&MoveLeft, window, cx);
1881 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1882 assert_eq!(
1883 editor.selections.display_ranges(cx),
1884 &[
1885 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1886 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1887 ]
1888 );
1889
1890 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1891 // and should select to the beginning of the line for the second cursor.
1892 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1893 assert_eq!(
1894 editor.selections.display_ranges(cx),
1895 &[
1896 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1897 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1898 ]
1899 );
1900
1901 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1902 // and should delete to the first non-whitespace character in the line for the second cursor.
1903 editor.move_to_end_of_line(&move_to_end, window, cx);
1904 editor.move_left(&MoveLeft, window, cx);
1905 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1906 assert_eq!(editor.text(cx), "c\n f");
1907 });
1908}
1909
1910#[gpui::test]
1911fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1912 init_test(cx, |_| {});
1913
1914 let move_to_beg = MoveToBeginningOfLine {
1915 stop_at_soft_wraps: true,
1916 stop_at_indent: true,
1917 };
1918
1919 let editor = cx.add_window(|window, cx| {
1920 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1921 build_editor(buffer, window, cx)
1922 });
1923
1924 _ = editor.update(cx, |editor, window, cx| {
1925 // test cursor between line_start and indent_start
1926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1927 s.select_display_ranges([
1928 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1929 ]);
1930 });
1931
1932 // cursor should move to line_start
1933 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1934 assert_eq!(
1935 editor.selections.display_ranges(cx),
1936 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1937 );
1938
1939 // cursor should move to indent_start
1940 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1941 assert_eq!(
1942 editor.selections.display_ranges(cx),
1943 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1944 );
1945
1946 // cursor should move to back to line_start
1947 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1948 assert_eq!(
1949 editor.selections.display_ranges(cx),
1950 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1951 );
1952 });
1953}
1954
1955#[gpui::test]
1956fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1957 init_test(cx, |_| {});
1958
1959 let editor = cx.add_window(|window, cx| {
1960 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1961 build_editor(buffer, window, cx)
1962 });
1963 _ = editor.update(cx, |editor, window, cx| {
1964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1965 s.select_display_ranges([
1966 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1967 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1968 ])
1969 });
1970 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1971 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1972
1973 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1974 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1975
1976 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1977 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1978
1979 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1980 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1981
1982 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1983 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1987
1988 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1989 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1993
1994 editor.move_right(&MoveRight, window, cx);
1995 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1996 assert_selection_ranges(
1997 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1998 editor,
1999 cx,
2000 );
2001
2002 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2003 assert_selection_ranges(
2004 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2005 editor,
2006 cx,
2007 );
2008
2009 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2010 assert_selection_ranges(
2011 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2012 editor,
2013 cx,
2014 );
2015 });
2016}
2017
2018#[gpui::test]
2019fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2020 init_test(cx, |_| {});
2021
2022 let editor = cx.add_window(|window, cx| {
2023 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2024 build_editor(buffer, window, cx)
2025 });
2026
2027 _ = editor.update(cx, |editor, window, cx| {
2028 editor.set_wrap_width(Some(140.0.into()), cx);
2029 assert_eq!(
2030 editor.display_text(cx),
2031 "use one::{\n two::three::\n four::five\n};"
2032 );
2033
2034 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2035 s.select_display_ranges([
2036 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2037 ]);
2038 });
2039
2040 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2041 assert_eq!(
2042 editor.selections.display_ranges(cx),
2043 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2044 );
2045
2046 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2047 assert_eq!(
2048 editor.selections.display_ranges(cx),
2049 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2050 );
2051
2052 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2053 assert_eq!(
2054 editor.selections.display_ranges(cx),
2055 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2056 );
2057
2058 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2059 assert_eq!(
2060 editor.selections.display_ranges(cx),
2061 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2062 );
2063
2064 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2065 assert_eq!(
2066 editor.selections.display_ranges(cx),
2067 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2068 );
2069
2070 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2071 assert_eq!(
2072 editor.selections.display_ranges(cx),
2073 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2074 );
2075 });
2076}
2077
2078#[gpui::test]
2079async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2080 init_test(cx, |_| {});
2081 let mut cx = EditorTestContext::new(cx).await;
2082
2083 let line_height = cx.editor(|editor, window, _| {
2084 editor
2085 .style()
2086 .unwrap()
2087 .text
2088 .line_height_in_pixels(window.rem_size())
2089 });
2090 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2091
2092 cx.set_state(
2093 &r#"ˇone
2094 two
2095
2096 three
2097 fourˇ
2098 five
2099
2100 six"#
2101 .unindent(),
2102 );
2103
2104 cx.update_editor(|editor, window, cx| {
2105 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2106 });
2107 cx.assert_editor_state(
2108 &r#"one
2109 two
2110 ˇ
2111 three
2112 four
2113 five
2114 ˇ
2115 six"#
2116 .unindent(),
2117 );
2118
2119 cx.update_editor(|editor, window, cx| {
2120 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2121 });
2122 cx.assert_editor_state(
2123 &r#"one
2124 two
2125
2126 three
2127 four
2128 five
2129 ˇ
2130 sixˇ"#
2131 .unindent(),
2132 );
2133
2134 cx.update_editor(|editor, window, cx| {
2135 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2136 });
2137 cx.assert_editor_state(
2138 &r#"one
2139 two
2140
2141 three
2142 four
2143 five
2144
2145 sixˇ"#
2146 .unindent(),
2147 );
2148
2149 cx.update_editor(|editor, window, cx| {
2150 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2151 });
2152 cx.assert_editor_state(
2153 &r#"one
2154 two
2155
2156 three
2157 four
2158 five
2159 ˇ
2160 six"#
2161 .unindent(),
2162 );
2163
2164 cx.update_editor(|editor, window, cx| {
2165 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2166 });
2167 cx.assert_editor_state(
2168 &r#"one
2169 two
2170 ˇ
2171 three
2172 four
2173 five
2174
2175 six"#
2176 .unindent(),
2177 );
2178
2179 cx.update_editor(|editor, window, cx| {
2180 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2181 });
2182 cx.assert_editor_state(
2183 &r#"ˇone
2184 two
2185
2186 three
2187 four
2188 five
2189
2190 six"#
2191 .unindent(),
2192 );
2193}
2194
2195#[gpui::test]
2196async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2197 init_test(cx, |_| {});
2198 let mut cx = EditorTestContext::new(cx).await;
2199 let line_height = cx.editor(|editor, window, _| {
2200 editor
2201 .style()
2202 .unwrap()
2203 .text
2204 .line_height_in_pixels(window.rem_size())
2205 });
2206 let window = cx.window;
2207 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2208
2209 cx.set_state(
2210 r#"ˇone
2211 two
2212 three
2213 four
2214 five
2215 six
2216 seven
2217 eight
2218 nine
2219 ten
2220 "#,
2221 );
2222
2223 cx.update_editor(|editor, window, cx| {
2224 assert_eq!(
2225 editor.snapshot(window, cx).scroll_position(),
2226 gpui::Point::new(0., 0.)
2227 );
2228 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2229 assert_eq!(
2230 editor.snapshot(window, cx).scroll_position(),
2231 gpui::Point::new(0., 3.)
2232 );
2233 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2234 assert_eq!(
2235 editor.snapshot(window, cx).scroll_position(),
2236 gpui::Point::new(0., 6.)
2237 );
2238 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2239 assert_eq!(
2240 editor.snapshot(window, cx).scroll_position(),
2241 gpui::Point::new(0., 3.)
2242 );
2243
2244 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2245 assert_eq!(
2246 editor.snapshot(window, cx).scroll_position(),
2247 gpui::Point::new(0., 1.)
2248 );
2249 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2250 assert_eq!(
2251 editor.snapshot(window, cx).scroll_position(),
2252 gpui::Point::new(0., 3.)
2253 );
2254 });
2255}
2256
2257#[gpui::test]
2258async fn test_autoscroll(cx: &mut TestAppContext) {
2259 init_test(cx, |_| {});
2260 let mut cx = EditorTestContext::new(cx).await;
2261
2262 let line_height = cx.update_editor(|editor, window, cx| {
2263 editor.set_vertical_scroll_margin(2, cx);
2264 editor
2265 .style()
2266 .unwrap()
2267 .text
2268 .line_height_in_pixels(window.rem_size())
2269 });
2270 let window = cx.window;
2271 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2272
2273 cx.set_state(
2274 r#"ˇone
2275 two
2276 three
2277 four
2278 five
2279 six
2280 seven
2281 eight
2282 nine
2283 ten
2284 "#,
2285 );
2286 cx.update_editor(|editor, window, cx| {
2287 assert_eq!(
2288 editor.snapshot(window, cx).scroll_position(),
2289 gpui::Point::new(0., 0.0)
2290 );
2291 });
2292
2293 // Add a cursor below the visible area. Since both cursors cannot fit
2294 // on screen, the editor autoscrolls to reveal the newest cursor, and
2295 // allows the vertical scroll margin below that cursor.
2296 cx.update_editor(|editor, window, cx| {
2297 editor.change_selections(Default::default(), window, cx, |selections| {
2298 selections.select_ranges([
2299 Point::new(0, 0)..Point::new(0, 0),
2300 Point::new(6, 0)..Point::new(6, 0),
2301 ]);
2302 })
2303 });
2304 cx.update_editor(|editor, window, cx| {
2305 assert_eq!(
2306 editor.snapshot(window, cx).scroll_position(),
2307 gpui::Point::new(0., 3.0)
2308 );
2309 });
2310
2311 // Move down. The editor cursor scrolls down to track the newest cursor.
2312 cx.update_editor(|editor, window, cx| {
2313 editor.move_down(&Default::default(), window, cx);
2314 });
2315 cx.update_editor(|editor, window, cx| {
2316 assert_eq!(
2317 editor.snapshot(window, cx).scroll_position(),
2318 gpui::Point::new(0., 4.0)
2319 );
2320 });
2321
2322 // Add a cursor above the visible area. Since both cursors fit on screen,
2323 // the editor scrolls to show both.
2324 cx.update_editor(|editor, window, cx| {
2325 editor.change_selections(Default::default(), window, cx, |selections| {
2326 selections.select_ranges([
2327 Point::new(1, 0)..Point::new(1, 0),
2328 Point::new(6, 0)..Point::new(6, 0),
2329 ]);
2330 })
2331 });
2332 cx.update_editor(|editor, window, cx| {
2333 assert_eq!(
2334 editor.snapshot(window, cx).scroll_position(),
2335 gpui::Point::new(0., 1.0)
2336 );
2337 });
2338}
2339
2340#[gpui::test]
2341async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2342 init_test(cx, |_| {});
2343 let mut cx = EditorTestContext::new(cx).await;
2344
2345 let line_height = cx.editor(|editor, window, _cx| {
2346 editor
2347 .style()
2348 .unwrap()
2349 .text
2350 .line_height_in_pixels(window.rem_size())
2351 });
2352 let window = cx.window;
2353 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2354 cx.set_state(
2355 &r#"
2356 ˇone
2357 two
2358 threeˇ
2359 four
2360 five
2361 six
2362 seven
2363 eight
2364 nine
2365 ten
2366 "#
2367 .unindent(),
2368 );
2369
2370 cx.update_editor(|editor, window, cx| {
2371 editor.move_page_down(&MovePageDown::default(), window, cx)
2372 });
2373 cx.assert_editor_state(
2374 &r#"
2375 one
2376 two
2377 three
2378 ˇfour
2379 five
2380 sixˇ
2381 seven
2382 eight
2383 nine
2384 ten
2385 "#
2386 .unindent(),
2387 );
2388
2389 cx.update_editor(|editor, window, cx| {
2390 editor.move_page_down(&MovePageDown::default(), window, cx)
2391 });
2392 cx.assert_editor_state(
2393 &r#"
2394 one
2395 two
2396 three
2397 four
2398 five
2399 six
2400 ˇseven
2401 eight
2402 nineˇ
2403 ten
2404 "#
2405 .unindent(),
2406 );
2407
2408 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2409 cx.assert_editor_state(
2410 &r#"
2411 one
2412 two
2413 three
2414 ˇfour
2415 five
2416 sixˇ
2417 seven
2418 eight
2419 nine
2420 ten
2421 "#
2422 .unindent(),
2423 );
2424
2425 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2426 cx.assert_editor_state(
2427 &r#"
2428 ˇone
2429 two
2430 threeˇ
2431 four
2432 five
2433 six
2434 seven
2435 eight
2436 nine
2437 ten
2438 "#
2439 .unindent(),
2440 );
2441
2442 // Test select collapsing
2443 cx.update_editor(|editor, window, cx| {
2444 editor.move_page_down(&MovePageDown::default(), window, cx);
2445 editor.move_page_down(&MovePageDown::default(), window, cx);
2446 editor.move_page_down(&MovePageDown::default(), window, cx);
2447 });
2448 cx.assert_editor_state(
2449 &r#"
2450 one
2451 two
2452 three
2453 four
2454 five
2455 six
2456 seven
2457 eight
2458 nine
2459 ˇten
2460 ˇ"#
2461 .unindent(),
2462 );
2463}
2464
2465#[gpui::test]
2466async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2467 init_test(cx, |_| {});
2468 let mut cx = EditorTestContext::new(cx).await;
2469 cx.set_state("one «two threeˇ» four");
2470 cx.update_editor(|editor, window, cx| {
2471 editor.delete_to_beginning_of_line(
2472 &DeleteToBeginningOfLine {
2473 stop_at_indent: false,
2474 },
2475 window,
2476 cx,
2477 );
2478 assert_eq!(editor.text(cx), " four");
2479 });
2480}
2481
2482#[gpui::test]
2483async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2484 init_test(cx, |_| {});
2485
2486 let mut cx = EditorTestContext::new(cx).await;
2487
2488 // For an empty selection, the preceding word fragment is deleted.
2489 // For non-empty selections, only selected characters are deleted.
2490 cx.set_state("onˇe two t«hreˇ»e four");
2491 cx.update_editor(|editor, window, cx| {
2492 editor.delete_to_previous_word_start(
2493 &DeleteToPreviousWordStart {
2494 ignore_newlines: false,
2495 ignore_brackets: false,
2496 },
2497 window,
2498 cx,
2499 );
2500 });
2501 cx.assert_editor_state("ˇe two tˇe four");
2502
2503 cx.set_state("e tˇwo te «fˇ»our");
2504 cx.update_editor(|editor, window, cx| {
2505 editor.delete_to_next_word_end(
2506 &DeleteToNextWordEnd {
2507 ignore_newlines: false,
2508 ignore_brackets: false,
2509 },
2510 window,
2511 cx,
2512 );
2513 });
2514 cx.assert_editor_state("e tˇ te ˇour");
2515}
2516
2517#[gpui::test]
2518async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2519 init_test(cx, |_| {});
2520
2521 let mut cx = EditorTestContext::new(cx).await;
2522
2523 cx.set_state("here is some text ˇwith a space");
2524 cx.update_editor(|editor, window, cx| {
2525 editor.delete_to_previous_word_start(
2526 &DeleteToPreviousWordStart {
2527 ignore_newlines: false,
2528 ignore_brackets: true,
2529 },
2530 window,
2531 cx,
2532 );
2533 });
2534 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2535 cx.assert_editor_state("here is some textˇwith a space");
2536
2537 cx.set_state("here is some text ˇwith a space");
2538 cx.update_editor(|editor, window, cx| {
2539 editor.delete_to_previous_word_start(
2540 &DeleteToPreviousWordStart {
2541 ignore_newlines: false,
2542 ignore_brackets: false,
2543 },
2544 window,
2545 cx,
2546 );
2547 });
2548 cx.assert_editor_state("here is some textˇwith a space");
2549
2550 cx.set_state("here is some textˇ with a space");
2551 cx.update_editor(|editor, window, cx| {
2552 editor.delete_to_next_word_end(
2553 &DeleteToNextWordEnd {
2554 ignore_newlines: false,
2555 ignore_brackets: true,
2556 },
2557 window,
2558 cx,
2559 );
2560 });
2561 // Same happens in the other direction.
2562 cx.assert_editor_state("here is some textˇwith a space");
2563
2564 cx.set_state("here is some textˇ with a space");
2565 cx.update_editor(|editor, window, cx| {
2566 editor.delete_to_next_word_end(
2567 &DeleteToNextWordEnd {
2568 ignore_newlines: false,
2569 ignore_brackets: false,
2570 },
2571 window,
2572 cx,
2573 );
2574 });
2575 cx.assert_editor_state("here is some textˇwith a space");
2576
2577 cx.set_state("here is some textˇ with a space");
2578 cx.update_editor(|editor, window, cx| {
2579 editor.delete_to_next_word_end(
2580 &DeleteToNextWordEnd {
2581 ignore_newlines: true,
2582 ignore_brackets: false,
2583 },
2584 window,
2585 cx,
2586 );
2587 });
2588 cx.assert_editor_state("here is some textˇwith a space");
2589 cx.update_editor(|editor, window, cx| {
2590 editor.delete_to_previous_word_start(
2591 &DeleteToPreviousWordStart {
2592 ignore_newlines: true,
2593 ignore_brackets: false,
2594 },
2595 window,
2596 cx,
2597 );
2598 });
2599 cx.assert_editor_state("here is some ˇwith a space");
2600 cx.update_editor(|editor, window, cx| {
2601 editor.delete_to_previous_word_start(
2602 &DeleteToPreviousWordStart {
2603 ignore_newlines: true,
2604 ignore_brackets: false,
2605 },
2606 window,
2607 cx,
2608 );
2609 });
2610 // Single whitespaces are removed with the word behind them.
2611 cx.assert_editor_state("here is ˇwith a space");
2612 cx.update_editor(|editor, window, cx| {
2613 editor.delete_to_previous_word_start(
2614 &DeleteToPreviousWordStart {
2615 ignore_newlines: true,
2616 ignore_brackets: false,
2617 },
2618 window,
2619 cx,
2620 );
2621 });
2622 cx.assert_editor_state("here ˇwith a space");
2623 cx.update_editor(|editor, window, cx| {
2624 editor.delete_to_previous_word_start(
2625 &DeleteToPreviousWordStart {
2626 ignore_newlines: true,
2627 ignore_brackets: false,
2628 },
2629 window,
2630 cx,
2631 );
2632 });
2633 cx.assert_editor_state("ˇwith a space");
2634 cx.update_editor(|editor, window, cx| {
2635 editor.delete_to_previous_word_start(
2636 &DeleteToPreviousWordStart {
2637 ignore_newlines: true,
2638 ignore_brackets: false,
2639 },
2640 window,
2641 cx,
2642 );
2643 });
2644 cx.assert_editor_state("ˇwith a space");
2645 cx.update_editor(|editor, window, cx| {
2646 editor.delete_to_next_word_end(
2647 &DeleteToNextWordEnd {
2648 ignore_newlines: true,
2649 ignore_brackets: false,
2650 },
2651 window,
2652 cx,
2653 );
2654 });
2655 // Same happens in the other direction.
2656 cx.assert_editor_state("ˇ a space");
2657 cx.update_editor(|editor, window, cx| {
2658 editor.delete_to_next_word_end(
2659 &DeleteToNextWordEnd {
2660 ignore_newlines: true,
2661 ignore_brackets: false,
2662 },
2663 window,
2664 cx,
2665 );
2666 });
2667 cx.assert_editor_state("ˇ space");
2668 cx.update_editor(|editor, window, cx| {
2669 editor.delete_to_next_word_end(
2670 &DeleteToNextWordEnd {
2671 ignore_newlines: true,
2672 ignore_brackets: false,
2673 },
2674 window,
2675 cx,
2676 );
2677 });
2678 cx.assert_editor_state("ˇ");
2679 cx.update_editor(|editor, window, cx| {
2680 editor.delete_to_next_word_end(
2681 &DeleteToNextWordEnd {
2682 ignore_newlines: true,
2683 ignore_brackets: false,
2684 },
2685 window,
2686 cx,
2687 );
2688 });
2689 cx.assert_editor_state("ˇ");
2690 cx.update_editor(|editor, window, cx| {
2691 editor.delete_to_previous_word_start(
2692 &DeleteToPreviousWordStart {
2693 ignore_newlines: true,
2694 ignore_brackets: false,
2695 },
2696 window,
2697 cx,
2698 );
2699 });
2700 cx.assert_editor_state("ˇ");
2701}
2702
2703#[gpui::test]
2704async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2705 init_test(cx, |_| {});
2706
2707 let language = Arc::new(
2708 Language::new(
2709 LanguageConfig {
2710 brackets: BracketPairConfig {
2711 pairs: vec![
2712 BracketPair {
2713 start: "\"".to_string(),
2714 end: "\"".to_string(),
2715 close: true,
2716 surround: true,
2717 newline: false,
2718 },
2719 BracketPair {
2720 start: "(".to_string(),
2721 end: ")".to_string(),
2722 close: true,
2723 surround: true,
2724 newline: true,
2725 },
2726 ],
2727 ..BracketPairConfig::default()
2728 },
2729 ..LanguageConfig::default()
2730 },
2731 Some(tree_sitter_rust::LANGUAGE.into()),
2732 )
2733 .with_brackets_query(
2734 r#"
2735 ("(" @open ")" @close)
2736 ("\"" @open "\"" @close)
2737 "#,
2738 )
2739 .unwrap(),
2740 );
2741
2742 let mut cx = EditorTestContext::new(cx).await;
2743 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2744
2745 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2746 cx.update_editor(|editor, window, cx| {
2747 editor.delete_to_previous_word_start(
2748 &DeleteToPreviousWordStart {
2749 ignore_newlines: true,
2750 ignore_brackets: false,
2751 },
2752 window,
2753 cx,
2754 );
2755 });
2756 // Deletion stops before brackets if asked to not ignore them.
2757 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2758 cx.update_editor(|editor, window, cx| {
2759 editor.delete_to_previous_word_start(
2760 &DeleteToPreviousWordStart {
2761 ignore_newlines: true,
2762 ignore_brackets: false,
2763 },
2764 window,
2765 cx,
2766 );
2767 });
2768 // Deletion has to remove a single bracket and then stop again.
2769 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2770
2771 cx.update_editor(|editor, window, cx| {
2772 editor.delete_to_previous_word_start(
2773 &DeleteToPreviousWordStart {
2774 ignore_newlines: true,
2775 ignore_brackets: false,
2776 },
2777 window,
2778 cx,
2779 );
2780 });
2781 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2782
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_previous_word_start(
2785 &DeleteToPreviousWordStart {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2794
2795 cx.update_editor(|editor, window, cx| {
2796 editor.delete_to_previous_word_start(
2797 &DeleteToPreviousWordStart {
2798 ignore_newlines: true,
2799 ignore_brackets: false,
2800 },
2801 window,
2802 cx,
2803 );
2804 });
2805 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2806
2807 cx.update_editor(|editor, window, cx| {
2808 editor.delete_to_next_word_end(
2809 &DeleteToNextWordEnd {
2810 ignore_newlines: true,
2811 ignore_brackets: false,
2812 },
2813 window,
2814 cx,
2815 );
2816 });
2817 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2818 cx.assert_editor_state(r#"ˇ");"#);
2819
2820 cx.update_editor(|editor, window, cx| {
2821 editor.delete_to_next_word_end(
2822 &DeleteToNextWordEnd {
2823 ignore_newlines: true,
2824 ignore_brackets: false,
2825 },
2826 window,
2827 cx,
2828 );
2829 });
2830 cx.assert_editor_state(r#"ˇ"#);
2831
2832 cx.update_editor(|editor, window, cx| {
2833 editor.delete_to_next_word_end(
2834 &DeleteToNextWordEnd {
2835 ignore_newlines: true,
2836 ignore_brackets: false,
2837 },
2838 window,
2839 cx,
2840 );
2841 });
2842 cx.assert_editor_state(r#"ˇ"#);
2843
2844 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2845 cx.update_editor(|editor, window, cx| {
2846 editor.delete_to_previous_word_start(
2847 &DeleteToPreviousWordStart {
2848 ignore_newlines: true,
2849 ignore_brackets: true,
2850 },
2851 window,
2852 cx,
2853 );
2854 });
2855 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2856}
2857
2858#[gpui::test]
2859fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2860 init_test(cx, |_| {});
2861
2862 let editor = cx.add_window(|window, cx| {
2863 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2864 build_editor(buffer, window, cx)
2865 });
2866 let del_to_prev_word_start = DeleteToPreviousWordStart {
2867 ignore_newlines: false,
2868 ignore_brackets: false,
2869 };
2870 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2871 ignore_newlines: true,
2872 ignore_brackets: false,
2873 };
2874
2875 _ = editor.update(cx, |editor, window, cx| {
2876 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2877 s.select_display_ranges([
2878 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2879 ])
2880 });
2881 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2882 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2883 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2884 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2889 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2890 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2891 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2892 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2893 });
2894}
2895
2896#[gpui::test]
2897fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2898 init_test(cx, |_| {});
2899
2900 let editor = cx.add_window(|window, cx| {
2901 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2902 build_editor(buffer, window, cx)
2903 });
2904 let del_to_next_word_end = DeleteToNextWordEnd {
2905 ignore_newlines: false,
2906 ignore_brackets: false,
2907 };
2908 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2909 ignore_newlines: true,
2910 ignore_brackets: false,
2911 };
2912
2913 _ = editor.update(cx, |editor, window, cx| {
2914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2915 s.select_display_ranges([
2916 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2917 ])
2918 });
2919 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2920 assert_eq!(
2921 editor.buffer.read(cx).read(cx).text(),
2922 "one\n two\nthree\n four"
2923 );
2924 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2925 assert_eq!(
2926 editor.buffer.read(cx).read(cx).text(),
2927 "\n two\nthree\n four"
2928 );
2929 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2930 assert_eq!(
2931 editor.buffer.read(cx).read(cx).text(),
2932 "two\nthree\n four"
2933 );
2934 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2935 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2936 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2938 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2939 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2940 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2941 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2942 });
2943}
2944
2945#[gpui::test]
2946fn test_newline(cx: &mut TestAppContext) {
2947 init_test(cx, |_| {});
2948
2949 let editor = cx.add_window(|window, cx| {
2950 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2951 build_editor(buffer, window, cx)
2952 });
2953
2954 _ = editor.update(cx, |editor, window, cx| {
2955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2956 s.select_display_ranges([
2957 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2958 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2959 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2960 ])
2961 });
2962
2963 editor.newline(&Newline, window, cx);
2964 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2965 });
2966}
2967
2968#[gpui::test]
2969fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2970 init_test(cx, |_| {});
2971
2972 let editor = cx.add_window(|window, cx| {
2973 let buffer = MultiBuffer::build_simple(
2974 "
2975 a
2976 b(
2977 X
2978 )
2979 c(
2980 X
2981 )
2982 "
2983 .unindent()
2984 .as_str(),
2985 cx,
2986 );
2987 let mut editor = build_editor(buffer, window, cx);
2988 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2989 s.select_ranges([
2990 Point::new(2, 4)..Point::new(2, 5),
2991 Point::new(5, 4)..Point::new(5, 5),
2992 ])
2993 });
2994 editor
2995 });
2996
2997 _ = editor.update(cx, |editor, window, cx| {
2998 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2999 editor.buffer.update(cx, |buffer, cx| {
3000 buffer.edit(
3001 [
3002 (Point::new(1, 2)..Point::new(3, 0), ""),
3003 (Point::new(4, 2)..Point::new(6, 0), ""),
3004 ],
3005 None,
3006 cx,
3007 );
3008 assert_eq!(
3009 buffer.read(cx).text(),
3010 "
3011 a
3012 b()
3013 c()
3014 "
3015 .unindent()
3016 );
3017 });
3018 assert_eq!(
3019 editor.selections.ranges(cx),
3020 &[
3021 Point::new(1, 2)..Point::new(1, 2),
3022 Point::new(2, 2)..Point::new(2, 2),
3023 ],
3024 );
3025
3026 editor.newline(&Newline, window, cx);
3027 assert_eq!(
3028 editor.text(cx),
3029 "
3030 a
3031 b(
3032 )
3033 c(
3034 )
3035 "
3036 .unindent()
3037 );
3038
3039 // The selections are moved after the inserted newlines
3040 assert_eq!(
3041 editor.selections.ranges(cx),
3042 &[
3043 Point::new(2, 0)..Point::new(2, 0),
3044 Point::new(4, 0)..Point::new(4, 0),
3045 ],
3046 );
3047 });
3048}
3049
3050#[gpui::test]
3051async fn test_newline_above(cx: &mut TestAppContext) {
3052 init_test(cx, |settings| {
3053 settings.defaults.tab_size = NonZeroU32::new(4)
3054 });
3055
3056 let language = Arc::new(
3057 Language::new(
3058 LanguageConfig::default(),
3059 Some(tree_sitter_rust::LANGUAGE.into()),
3060 )
3061 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3062 .unwrap(),
3063 );
3064
3065 let mut cx = EditorTestContext::new(cx).await;
3066 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3067 cx.set_state(indoc! {"
3068 const a: ˇA = (
3069 (ˇ
3070 «const_functionˇ»(ˇ),
3071 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3072 )ˇ
3073 ˇ);ˇ
3074 "});
3075
3076 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 ˇ
3079 const a: A = (
3080 ˇ
3081 (
3082 ˇ
3083 ˇ
3084 const_function(),
3085 ˇ
3086 ˇ
3087 ˇ
3088 ˇ
3089 something_else,
3090 ˇ
3091 )
3092 ˇ
3093 ˇ
3094 );
3095 "});
3096}
3097
3098#[gpui::test]
3099async fn test_newline_below(cx: &mut TestAppContext) {
3100 init_test(cx, |settings| {
3101 settings.defaults.tab_size = NonZeroU32::new(4)
3102 });
3103
3104 let language = Arc::new(
3105 Language::new(
3106 LanguageConfig::default(),
3107 Some(tree_sitter_rust::LANGUAGE.into()),
3108 )
3109 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3110 .unwrap(),
3111 );
3112
3113 let mut cx = EditorTestContext::new(cx).await;
3114 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3115 cx.set_state(indoc! {"
3116 const a: ˇA = (
3117 (ˇ
3118 «const_functionˇ»(ˇ),
3119 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3120 )ˇ
3121 ˇ);ˇ
3122 "});
3123
3124 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3125 cx.assert_editor_state(indoc! {"
3126 const a: A = (
3127 ˇ
3128 (
3129 ˇ
3130 const_function(),
3131 ˇ
3132 ˇ
3133 something_else,
3134 ˇ
3135 ˇ
3136 ˇ
3137 ˇ
3138 )
3139 ˇ
3140 );
3141 ˇ
3142 ˇ
3143 "});
3144}
3145
3146#[gpui::test]
3147async fn test_newline_comments(cx: &mut TestAppContext) {
3148 init_test(cx, |settings| {
3149 settings.defaults.tab_size = NonZeroU32::new(4)
3150 });
3151
3152 let language = Arc::new(Language::new(
3153 LanguageConfig {
3154 line_comments: vec!["// ".into()],
3155 ..LanguageConfig::default()
3156 },
3157 None,
3158 ));
3159 {
3160 let mut cx = EditorTestContext::new(cx).await;
3161 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3162 cx.set_state(indoc! {"
3163 // Fooˇ
3164 "});
3165
3166 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3167 cx.assert_editor_state(indoc! {"
3168 // Foo
3169 // ˇ
3170 "});
3171 // Ensure that we add comment prefix when existing line contains space
3172 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3173 cx.assert_editor_state(
3174 indoc! {"
3175 // Foo
3176 //s
3177 // ˇ
3178 "}
3179 .replace("s", " ") // s is used as space placeholder to prevent format on save
3180 .as_str(),
3181 );
3182 // Ensure that we add comment prefix when existing line does not contain space
3183 cx.set_state(indoc! {"
3184 // Foo
3185 //ˇ
3186 "});
3187 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3188 cx.assert_editor_state(indoc! {"
3189 // Foo
3190 //
3191 // ˇ
3192 "});
3193 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3194 cx.set_state(indoc! {"
3195 ˇ// Foo
3196 "});
3197 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3198 cx.assert_editor_state(indoc! {"
3199
3200 ˇ// Foo
3201 "});
3202 }
3203 // Ensure that comment continuations can be disabled.
3204 update_test_language_settings(cx, |settings| {
3205 settings.defaults.extend_comment_on_newline = Some(false);
3206 });
3207 let mut cx = EditorTestContext::new(cx).await;
3208 cx.set_state(indoc! {"
3209 // Fooˇ
3210 "});
3211 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3212 cx.assert_editor_state(indoc! {"
3213 // Foo
3214 ˇ
3215 "});
3216}
3217
3218#[gpui::test]
3219async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3220 init_test(cx, |settings| {
3221 settings.defaults.tab_size = NonZeroU32::new(4)
3222 });
3223
3224 let language = Arc::new(Language::new(
3225 LanguageConfig {
3226 line_comments: vec!["// ".into(), "/// ".into()],
3227 ..LanguageConfig::default()
3228 },
3229 None,
3230 ));
3231 {
3232 let mut cx = EditorTestContext::new(cx).await;
3233 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3234 cx.set_state(indoc! {"
3235 //ˇ
3236 "});
3237 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3238 cx.assert_editor_state(indoc! {"
3239 //
3240 // ˇ
3241 "});
3242
3243 cx.set_state(indoc! {"
3244 ///ˇ
3245 "});
3246 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3247 cx.assert_editor_state(indoc! {"
3248 ///
3249 /// ˇ
3250 "});
3251 }
3252}
3253
3254#[gpui::test]
3255async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3256 init_test(cx, |settings| {
3257 settings.defaults.tab_size = NonZeroU32::new(4)
3258 });
3259
3260 let language = Arc::new(
3261 Language::new(
3262 LanguageConfig {
3263 documentation_comment: Some(language::BlockCommentConfig {
3264 start: "/**".into(),
3265 end: "*/".into(),
3266 prefix: "* ".into(),
3267 tab_size: 1,
3268 }),
3269
3270 ..LanguageConfig::default()
3271 },
3272 Some(tree_sitter_rust::LANGUAGE.into()),
3273 )
3274 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3275 .unwrap(),
3276 );
3277
3278 {
3279 let mut cx = EditorTestContext::new(cx).await;
3280 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3281 cx.set_state(indoc! {"
3282 /**ˇ
3283 "});
3284
3285 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 /**
3288 * ˇ
3289 "});
3290 // Ensure that if cursor is before the comment start,
3291 // we do not actually insert a comment prefix.
3292 cx.set_state(indoc! {"
3293 ˇ/**
3294 "});
3295 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297
3298 ˇ/**
3299 "});
3300 // Ensure that if cursor is between it doesn't add comment prefix.
3301 cx.set_state(indoc! {"
3302 /*ˇ*
3303 "});
3304 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3305 cx.assert_editor_state(indoc! {"
3306 /*
3307 ˇ*
3308 "});
3309 // Ensure that if suffix exists on same line after cursor it adds new line.
3310 cx.set_state(indoc! {"
3311 /**ˇ*/
3312 "});
3313 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3314 cx.assert_editor_state(indoc! {"
3315 /**
3316 * ˇ
3317 */
3318 "});
3319 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3320 cx.set_state(indoc! {"
3321 /**ˇ */
3322 "});
3323 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3324 cx.assert_editor_state(indoc! {"
3325 /**
3326 * ˇ
3327 */
3328 "});
3329 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3330 cx.set_state(indoc! {"
3331 /** ˇ*/
3332 "});
3333 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3334 cx.assert_editor_state(
3335 indoc! {"
3336 /**s
3337 * ˇ
3338 */
3339 "}
3340 .replace("s", " ") // s is used as space placeholder to prevent format on save
3341 .as_str(),
3342 );
3343 // Ensure that delimiter space is preserved when newline on already
3344 // spaced delimiter.
3345 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3346 cx.assert_editor_state(
3347 indoc! {"
3348 /**s
3349 *s
3350 * ˇ
3351 */
3352 "}
3353 .replace("s", " ") // s is used as space placeholder to prevent format on save
3354 .as_str(),
3355 );
3356 // Ensure that delimiter space is preserved when space is not
3357 // on existing delimiter.
3358 cx.set_state(indoc! {"
3359 /**
3360 *ˇ
3361 */
3362 "});
3363 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3364 cx.assert_editor_state(indoc! {"
3365 /**
3366 *
3367 * ˇ
3368 */
3369 "});
3370 // Ensure that if suffix exists on same line after cursor it
3371 // doesn't add extra new line if prefix is not on same line.
3372 cx.set_state(indoc! {"
3373 /**
3374 ˇ*/
3375 "});
3376 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3377 cx.assert_editor_state(indoc! {"
3378 /**
3379
3380 ˇ*/
3381 "});
3382 // Ensure that it detects suffix after existing prefix.
3383 cx.set_state(indoc! {"
3384 /**ˇ/
3385 "});
3386 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3387 cx.assert_editor_state(indoc! {"
3388 /**
3389 ˇ/
3390 "});
3391 // Ensure that if suffix exists on same line before
3392 // cursor it does not add comment prefix.
3393 cx.set_state(indoc! {"
3394 /** */ˇ
3395 "});
3396 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3397 cx.assert_editor_state(indoc! {"
3398 /** */
3399 ˇ
3400 "});
3401 // Ensure that if suffix exists on same line before
3402 // cursor it does not add comment prefix.
3403 cx.set_state(indoc! {"
3404 /**
3405 *
3406 */ˇ
3407 "});
3408 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 /**
3411 *
3412 */
3413 ˇ
3414 "});
3415
3416 // Ensure that inline comment followed by code
3417 // doesn't add comment prefix on newline
3418 cx.set_state(indoc! {"
3419 /** */ textˇ
3420 "});
3421 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3422 cx.assert_editor_state(indoc! {"
3423 /** */ text
3424 ˇ
3425 "});
3426
3427 // Ensure that text after comment end tag
3428 // doesn't add comment prefix on newline
3429 cx.set_state(indoc! {"
3430 /**
3431 *
3432 */ˇtext
3433 "});
3434 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3435 cx.assert_editor_state(indoc! {"
3436 /**
3437 *
3438 */
3439 ˇtext
3440 "});
3441
3442 // Ensure if not comment block it doesn't
3443 // add comment prefix on newline
3444 cx.set_state(indoc! {"
3445 * textˇ
3446 "});
3447 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3448 cx.assert_editor_state(indoc! {"
3449 * text
3450 ˇ
3451 "});
3452 }
3453 // Ensure that comment continuations can be disabled.
3454 update_test_language_settings(cx, |settings| {
3455 settings.defaults.extend_comment_on_newline = Some(false);
3456 });
3457 let mut cx = EditorTestContext::new(cx).await;
3458 cx.set_state(indoc! {"
3459 /**ˇ
3460 "});
3461 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3462 cx.assert_editor_state(indoc! {"
3463 /**
3464 ˇ
3465 "});
3466}
3467
3468#[gpui::test]
3469async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3470 init_test(cx, |settings| {
3471 settings.defaults.tab_size = NonZeroU32::new(4)
3472 });
3473
3474 let lua_language = Arc::new(Language::new(
3475 LanguageConfig {
3476 line_comments: vec!["--".into()],
3477 block_comment: Some(language::BlockCommentConfig {
3478 start: "--[[".into(),
3479 prefix: "".into(),
3480 end: "]]".into(),
3481 tab_size: 0,
3482 }),
3483 ..LanguageConfig::default()
3484 },
3485 None,
3486 ));
3487
3488 let mut cx = EditorTestContext::new(cx).await;
3489 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3490
3491 // Line with line comment should extend
3492 cx.set_state(indoc! {"
3493 --ˇ
3494 "});
3495 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 --
3498 --ˇ
3499 "});
3500
3501 // Line with block comment that matches line comment should not extend
3502 cx.set_state(indoc! {"
3503 --[[ˇ
3504 "});
3505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3506 cx.assert_editor_state(indoc! {"
3507 --[[
3508 ˇ
3509 "});
3510}
3511
3512#[gpui::test]
3513fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3514 init_test(cx, |_| {});
3515
3516 let editor = cx.add_window(|window, cx| {
3517 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3518 let mut editor = build_editor(buffer, window, cx);
3519 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3520 s.select_ranges([3..4, 11..12, 19..20])
3521 });
3522 editor
3523 });
3524
3525 _ = editor.update(cx, |editor, window, cx| {
3526 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3527 editor.buffer.update(cx, |buffer, cx| {
3528 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3529 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3530 });
3531 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3532
3533 editor.insert("Z", window, cx);
3534 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3535
3536 // The selections are moved after the inserted characters
3537 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3538 });
3539}
3540
3541#[gpui::test]
3542async fn test_tab(cx: &mut TestAppContext) {
3543 init_test(cx, |settings| {
3544 settings.defaults.tab_size = NonZeroU32::new(3)
3545 });
3546
3547 let mut cx = EditorTestContext::new(cx).await;
3548 cx.set_state(indoc! {"
3549 ˇabˇc
3550 ˇ🏀ˇ🏀ˇefg
3551 dˇ
3552 "});
3553 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 ˇab ˇc
3556 ˇ🏀 ˇ🏀 ˇefg
3557 d ˇ
3558 "});
3559
3560 cx.set_state(indoc! {"
3561 a
3562 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3563 "});
3564 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3565 cx.assert_editor_state(indoc! {"
3566 a
3567 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3568 "});
3569}
3570
3571#[gpui::test]
3572async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3573 init_test(cx, |_| {});
3574
3575 let mut cx = EditorTestContext::new(cx).await;
3576 let language = Arc::new(
3577 Language::new(
3578 LanguageConfig::default(),
3579 Some(tree_sitter_rust::LANGUAGE.into()),
3580 )
3581 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3582 .unwrap(),
3583 );
3584 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3585
3586 // test when all cursors are not at suggested indent
3587 // then simply move to their suggested indent location
3588 cx.set_state(indoc! {"
3589 const a: B = (
3590 c(
3591 ˇ
3592 ˇ )
3593 );
3594 "});
3595 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3596 cx.assert_editor_state(indoc! {"
3597 const a: B = (
3598 c(
3599 ˇ
3600 ˇ)
3601 );
3602 "});
3603
3604 // test cursor already at suggested indent not moving when
3605 // other cursors are yet to reach their suggested indents
3606 cx.set_state(indoc! {"
3607 ˇ
3608 const a: B = (
3609 c(
3610 d(
3611 ˇ
3612 )
3613 ˇ
3614 ˇ )
3615 );
3616 "});
3617 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3618 cx.assert_editor_state(indoc! {"
3619 ˇ
3620 const a: B = (
3621 c(
3622 d(
3623 ˇ
3624 )
3625 ˇ
3626 ˇ)
3627 );
3628 "});
3629 // test when all cursors are at suggested indent then tab is inserted
3630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 ˇ
3633 const a: B = (
3634 c(
3635 d(
3636 ˇ
3637 )
3638 ˇ
3639 ˇ)
3640 );
3641 "});
3642
3643 // test when current indent is less than suggested indent,
3644 // we adjust line to match suggested indent and move cursor to it
3645 //
3646 // when no other cursor is at word boundary, all of them should move
3647 cx.set_state(indoc! {"
3648 const a: B = (
3649 c(
3650 d(
3651 ˇ
3652 ˇ )
3653 ˇ )
3654 );
3655 "});
3656 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 const a: B = (
3659 c(
3660 d(
3661 ˇ
3662 ˇ)
3663 ˇ)
3664 );
3665 "});
3666
3667 // test when current indent is less than suggested indent,
3668 // we adjust line to match suggested indent and move cursor to it
3669 //
3670 // when some other cursor is at word boundary, it should not move
3671 cx.set_state(indoc! {"
3672 const a: B = (
3673 c(
3674 d(
3675 ˇ
3676 ˇ )
3677 ˇ)
3678 );
3679 "});
3680 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3681 cx.assert_editor_state(indoc! {"
3682 const a: B = (
3683 c(
3684 d(
3685 ˇ
3686 ˇ)
3687 ˇ)
3688 );
3689 "});
3690
3691 // test when current indent is more than suggested indent,
3692 // we just move cursor to current indent instead of suggested indent
3693 //
3694 // when no other cursor is at word boundary, all of them should move
3695 cx.set_state(indoc! {"
3696 const a: B = (
3697 c(
3698 d(
3699 ˇ
3700 ˇ )
3701 ˇ )
3702 );
3703 "});
3704 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3705 cx.assert_editor_state(indoc! {"
3706 const a: B = (
3707 c(
3708 d(
3709 ˇ
3710 ˇ)
3711 ˇ)
3712 );
3713 "});
3714 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 const a: B = (
3717 c(
3718 d(
3719 ˇ
3720 ˇ)
3721 ˇ)
3722 );
3723 "});
3724
3725 // test when current indent is more than suggested indent,
3726 // we just move cursor to current indent instead of suggested indent
3727 //
3728 // when some other cursor is at word boundary, it doesn't move
3729 cx.set_state(indoc! {"
3730 const a: B = (
3731 c(
3732 d(
3733 ˇ
3734 ˇ )
3735 ˇ)
3736 );
3737 "});
3738 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3739 cx.assert_editor_state(indoc! {"
3740 const a: B = (
3741 c(
3742 d(
3743 ˇ
3744 ˇ)
3745 ˇ)
3746 );
3747 "});
3748
3749 // handle auto-indent when there are multiple cursors on the same line
3750 cx.set_state(indoc! {"
3751 const a: B = (
3752 c(
3753 ˇ ˇ
3754 ˇ )
3755 );
3756 "});
3757 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3758 cx.assert_editor_state(indoc! {"
3759 const a: B = (
3760 c(
3761 ˇ
3762 ˇ)
3763 );
3764 "});
3765}
3766
3767#[gpui::test]
3768async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3769 init_test(cx, |settings| {
3770 settings.defaults.tab_size = NonZeroU32::new(3)
3771 });
3772
3773 let mut cx = EditorTestContext::new(cx).await;
3774 cx.set_state(indoc! {"
3775 ˇ
3776 \t ˇ
3777 \t ˇ
3778 \t ˇ
3779 \t \t\t \t \t\t \t\t \t \t ˇ
3780 "});
3781
3782 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3783 cx.assert_editor_state(indoc! {"
3784 ˇ
3785 \t ˇ
3786 \t ˇ
3787 \t ˇ
3788 \t \t\t \t \t\t \t\t \t \t ˇ
3789 "});
3790}
3791
3792#[gpui::test]
3793async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3794 init_test(cx, |settings| {
3795 settings.defaults.tab_size = NonZeroU32::new(4)
3796 });
3797
3798 let language = Arc::new(
3799 Language::new(
3800 LanguageConfig::default(),
3801 Some(tree_sitter_rust::LANGUAGE.into()),
3802 )
3803 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3804 .unwrap(),
3805 );
3806
3807 let mut cx = EditorTestContext::new(cx).await;
3808 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3809 cx.set_state(indoc! {"
3810 fn a() {
3811 if b {
3812 \t ˇc
3813 }
3814 }
3815 "});
3816
3817 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3818 cx.assert_editor_state(indoc! {"
3819 fn a() {
3820 if b {
3821 ˇc
3822 }
3823 }
3824 "});
3825}
3826
3827#[gpui::test]
3828async fn test_indent_outdent(cx: &mut TestAppContext) {
3829 init_test(cx, |settings| {
3830 settings.defaults.tab_size = NonZeroU32::new(4);
3831 });
3832
3833 let mut cx = EditorTestContext::new(cx).await;
3834
3835 cx.set_state(indoc! {"
3836 «oneˇ» «twoˇ»
3837 three
3838 four
3839 "});
3840 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3841 cx.assert_editor_state(indoc! {"
3842 «oneˇ» «twoˇ»
3843 three
3844 four
3845 "});
3846
3847 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3848 cx.assert_editor_state(indoc! {"
3849 «oneˇ» «twoˇ»
3850 three
3851 four
3852 "});
3853
3854 // select across line ending
3855 cx.set_state(indoc! {"
3856 one two
3857 t«hree
3858 ˇ» four
3859 "});
3860 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3861 cx.assert_editor_state(indoc! {"
3862 one two
3863 t«hree
3864 ˇ» four
3865 "});
3866
3867 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3868 cx.assert_editor_state(indoc! {"
3869 one two
3870 t«hree
3871 ˇ» four
3872 "});
3873
3874 // Ensure that indenting/outdenting works when the cursor is at column 0.
3875 cx.set_state(indoc! {"
3876 one two
3877 ˇthree
3878 four
3879 "});
3880 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3881 cx.assert_editor_state(indoc! {"
3882 one two
3883 ˇthree
3884 four
3885 "});
3886
3887 cx.set_state(indoc! {"
3888 one two
3889 ˇ three
3890 four
3891 "});
3892 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3893 cx.assert_editor_state(indoc! {"
3894 one two
3895 ˇthree
3896 four
3897 "});
3898}
3899
3900#[gpui::test]
3901async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3902 // This is a regression test for issue #33761
3903 init_test(cx, |_| {});
3904
3905 let mut cx = EditorTestContext::new(cx).await;
3906 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3907 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3908
3909 cx.set_state(
3910 r#"ˇ# ingress:
3911ˇ# api:
3912ˇ# enabled: false
3913ˇ# pathType: Prefix
3914ˇ# console:
3915ˇ# enabled: false
3916ˇ# pathType: Prefix
3917"#,
3918 );
3919
3920 // Press tab to indent all lines
3921 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3922
3923 cx.assert_editor_state(
3924 r#" ˇ# ingress:
3925 ˇ# api:
3926 ˇ# enabled: false
3927 ˇ# pathType: Prefix
3928 ˇ# console:
3929 ˇ# enabled: false
3930 ˇ# pathType: Prefix
3931"#,
3932 );
3933}
3934
3935#[gpui::test]
3936async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3937 // This is a test to make sure our fix for issue #33761 didn't break anything
3938 init_test(cx, |_| {});
3939
3940 let mut cx = EditorTestContext::new(cx).await;
3941 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3942 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3943
3944 cx.set_state(
3945 r#"ˇingress:
3946ˇ api:
3947ˇ enabled: false
3948ˇ pathType: Prefix
3949"#,
3950 );
3951
3952 // Press tab to indent all lines
3953 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3954
3955 cx.assert_editor_state(
3956 r#"ˇingress:
3957 ˇapi:
3958 ˇenabled: false
3959 ˇpathType: Prefix
3960"#,
3961 );
3962}
3963
3964#[gpui::test]
3965async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3966 init_test(cx, |settings| {
3967 settings.defaults.hard_tabs = Some(true);
3968 });
3969
3970 let mut cx = EditorTestContext::new(cx).await;
3971
3972 // select two ranges on one line
3973 cx.set_state(indoc! {"
3974 «oneˇ» «twoˇ»
3975 three
3976 four
3977 "});
3978 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3979 cx.assert_editor_state(indoc! {"
3980 \t«oneˇ» «twoˇ»
3981 three
3982 four
3983 "});
3984 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3985 cx.assert_editor_state(indoc! {"
3986 \t\t«oneˇ» «twoˇ»
3987 three
3988 four
3989 "});
3990 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3991 cx.assert_editor_state(indoc! {"
3992 \t«oneˇ» «twoˇ»
3993 three
3994 four
3995 "});
3996 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3997 cx.assert_editor_state(indoc! {"
3998 «oneˇ» «twoˇ»
3999 three
4000 four
4001 "});
4002
4003 // select across a line ending
4004 cx.set_state(indoc! {"
4005 one two
4006 t«hree
4007 ˇ»four
4008 "});
4009 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4010 cx.assert_editor_state(indoc! {"
4011 one two
4012 \tt«hree
4013 ˇ»four
4014 "});
4015 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4016 cx.assert_editor_state(indoc! {"
4017 one two
4018 \t\tt«hree
4019 ˇ»four
4020 "});
4021 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4022 cx.assert_editor_state(indoc! {"
4023 one two
4024 \tt«hree
4025 ˇ»four
4026 "});
4027 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4028 cx.assert_editor_state(indoc! {"
4029 one two
4030 t«hree
4031 ˇ»four
4032 "});
4033
4034 // Ensure that indenting/outdenting works when the cursor is at column 0.
4035 cx.set_state(indoc! {"
4036 one two
4037 ˇthree
4038 four
4039 "});
4040 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4041 cx.assert_editor_state(indoc! {"
4042 one two
4043 ˇthree
4044 four
4045 "});
4046 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4047 cx.assert_editor_state(indoc! {"
4048 one two
4049 \tˇthree
4050 four
4051 "});
4052 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4053 cx.assert_editor_state(indoc! {"
4054 one two
4055 ˇthree
4056 four
4057 "});
4058}
4059
4060#[gpui::test]
4061fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4062 init_test(cx, |settings| {
4063 settings.languages.0.extend([
4064 (
4065 "TOML".into(),
4066 LanguageSettingsContent {
4067 tab_size: NonZeroU32::new(2),
4068 ..Default::default()
4069 },
4070 ),
4071 (
4072 "Rust".into(),
4073 LanguageSettingsContent {
4074 tab_size: NonZeroU32::new(4),
4075 ..Default::default()
4076 },
4077 ),
4078 ]);
4079 });
4080
4081 let toml_language = Arc::new(Language::new(
4082 LanguageConfig {
4083 name: "TOML".into(),
4084 ..Default::default()
4085 },
4086 None,
4087 ));
4088 let rust_language = Arc::new(Language::new(
4089 LanguageConfig {
4090 name: "Rust".into(),
4091 ..Default::default()
4092 },
4093 None,
4094 ));
4095
4096 let toml_buffer =
4097 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4098 let rust_buffer =
4099 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4100 let multibuffer = cx.new(|cx| {
4101 let mut multibuffer = MultiBuffer::new(ReadWrite);
4102 multibuffer.push_excerpts(
4103 toml_buffer.clone(),
4104 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4105 cx,
4106 );
4107 multibuffer.push_excerpts(
4108 rust_buffer.clone(),
4109 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4110 cx,
4111 );
4112 multibuffer
4113 });
4114
4115 cx.add_window(|window, cx| {
4116 let mut editor = build_editor(multibuffer, window, cx);
4117
4118 assert_eq!(
4119 editor.text(cx),
4120 indoc! {"
4121 a = 1
4122 b = 2
4123
4124 const c: usize = 3;
4125 "}
4126 );
4127
4128 select_ranges(
4129 &mut editor,
4130 indoc! {"
4131 «aˇ» = 1
4132 b = 2
4133
4134 «const c:ˇ» usize = 3;
4135 "},
4136 window,
4137 cx,
4138 );
4139
4140 editor.tab(&Tab, window, cx);
4141 assert_text_with_selections(
4142 &mut editor,
4143 indoc! {"
4144 «aˇ» = 1
4145 b = 2
4146
4147 «const c:ˇ» usize = 3;
4148 "},
4149 cx,
4150 );
4151 editor.backtab(&Backtab, window, cx);
4152 assert_text_with_selections(
4153 &mut editor,
4154 indoc! {"
4155 «aˇ» = 1
4156 b = 2
4157
4158 «const c:ˇ» usize = 3;
4159 "},
4160 cx,
4161 );
4162
4163 editor
4164 });
4165}
4166
4167#[gpui::test]
4168async fn test_backspace(cx: &mut TestAppContext) {
4169 init_test(cx, |_| {});
4170
4171 let mut cx = EditorTestContext::new(cx).await;
4172
4173 // Basic backspace
4174 cx.set_state(indoc! {"
4175 onˇe two three
4176 fou«rˇ» five six
4177 seven «ˇeight nine
4178 »ten
4179 "});
4180 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4181 cx.assert_editor_state(indoc! {"
4182 oˇe two three
4183 fouˇ five six
4184 seven ˇten
4185 "});
4186
4187 // Test backspace inside and around indents
4188 cx.set_state(indoc! {"
4189 zero
4190 ˇone
4191 ˇtwo
4192 ˇ ˇ ˇ three
4193 ˇ ˇ four
4194 "});
4195 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4196 cx.assert_editor_state(indoc! {"
4197 zero
4198 ˇone
4199 ˇtwo
4200 ˇ threeˇ four
4201 "});
4202}
4203
4204#[gpui::test]
4205async fn test_delete(cx: &mut TestAppContext) {
4206 init_test(cx, |_| {});
4207
4208 let mut cx = EditorTestContext::new(cx).await;
4209 cx.set_state(indoc! {"
4210 onˇe two three
4211 fou«rˇ» five six
4212 seven «ˇeight nine
4213 »ten
4214 "});
4215 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4216 cx.assert_editor_state(indoc! {"
4217 onˇ two three
4218 fouˇ five six
4219 seven ˇten
4220 "});
4221}
4222
4223#[gpui::test]
4224fn test_delete_line(cx: &mut TestAppContext) {
4225 init_test(cx, |_| {});
4226
4227 let editor = cx.add_window(|window, cx| {
4228 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4229 build_editor(buffer, window, cx)
4230 });
4231 _ = editor.update(cx, |editor, window, cx| {
4232 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4233 s.select_display_ranges([
4234 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4235 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4236 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4237 ])
4238 });
4239 editor.delete_line(&DeleteLine, window, cx);
4240 assert_eq!(editor.display_text(cx), "ghi");
4241 assert_eq!(
4242 editor.selections.display_ranges(cx),
4243 vec![
4244 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4245 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4246 ]
4247 );
4248 });
4249
4250 let editor = cx.add_window(|window, cx| {
4251 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4252 build_editor(buffer, window, cx)
4253 });
4254 _ = editor.update(cx, |editor, window, cx| {
4255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4256 s.select_display_ranges([
4257 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4258 ])
4259 });
4260 editor.delete_line(&DeleteLine, window, cx);
4261 assert_eq!(editor.display_text(cx), "ghi\n");
4262 assert_eq!(
4263 editor.selections.display_ranges(cx),
4264 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4265 );
4266 });
4267}
4268
4269#[gpui::test]
4270fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 cx.add_window(|window, cx| {
4274 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4275 let mut editor = build_editor(buffer.clone(), window, cx);
4276 let buffer = buffer.read(cx).as_singleton().unwrap();
4277
4278 assert_eq!(
4279 editor.selections.ranges::<Point>(cx),
4280 &[Point::new(0, 0)..Point::new(0, 0)]
4281 );
4282
4283 // When on single line, replace newline at end by space
4284 editor.join_lines(&JoinLines, window, cx);
4285 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4286 assert_eq!(
4287 editor.selections.ranges::<Point>(cx),
4288 &[Point::new(0, 3)..Point::new(0, 3)]
4289 );
4290
4291 // When multiple lines are selected, remove newlines that are spanned by the selection
4292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4293 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4294 });
4295 editor.join_lines(&JoinLines, window, cx);
4296 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4297 assert_eq!(
4298 editor.selections.ranges::<Point>(cx),
4299 &[Point::new(0, 11)..Point::new(0, 11)]
4300 );
4301
4302 // Undo should be transactional
4303 editor.undo(&Undo, window, cx);
4304 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4305 assert_eq!(
4306 editor.selections.ranges::<Point>(cx),
4307 &[Point::new(0, 5)..Point::new(2, 2)]
4308 );
4309
4310 // When joining an empty line don't insert a space
4311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4312 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4313 });
4314 editor.join_lines(&JoinLines, window, cx);
4315 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4316 assert_eq!(
4317 editor.selections.ranges::<Point>(cx),
4318 [Point::new(2, 3)..Point::new(2, 3)]
4319 );
4320
4321 // We can remove trailing newlines
4322 editor.join_lines(&JoinLines, window, cx);
4323 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4324 assert_eq!(
4325 editor.selections.ranges::<Point>(cx),
4326 [Point::new(2, 3)..Point::new(2, 3)]
4327 );
4328
4329 // We don't blow up on the last line
4330 editor.join_lines(&JoinLines, window, cx);
4331 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4332 assert_eq!(
4333 editor.selections.ranges::<Point>(cx),
4334 [Point::new(2, 3)..Point::new(2, 3)]
4335 );
4336
4337 // reset to test indentation
4338 editor.buffer.update(cx, |buffer, cx| {
4339 buffer.edit(
4340 [
4341 (Point::new(1, 0)..Point::new(1, 2), " "),
4342 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4343 ],
4344 None,
4345 cx,
4346 )
4347 });
4348
4349 // We remove any leading spaces
4350 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4351 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4352 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4353 });
4354 editor.join_lines(&JoinLines, window, cx);
4355 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4356
4357 // We don't insert a space for a line containing only spaces
4358 editor.join_lines(&JoinLines, window, cx);
4359 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4360
4361 // We ignore any leading tabs
4362 editor.join_lines(&JoinLines, window, cx);
4363 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4364
4365 editor
4366 });
4367}
4368
4369#[gpui::test]
4370fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4371 init_test(cx, |_| {});
4372
4373 cx.add_window(|window, cx| {
4374 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4375 let mut editor = build_editor(buffer.clone(), window, cx);
4376 let buffer = buffer.read(cx).as_singleton().unwrap();
4377
4378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4379 s.select_ranges([
4380 Point::new(0, 2)..Point::new(1, 1),
4381 Point::new(1, 2)..Point::new(1, 2),
4382 Point::new(3, 1)..Point::new(3, 2),
4383 ])
4384 });
4385
4386 editor.join_lines(&JoinLines, window, cx);
4387 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4388
4389 assert_eq!(
4390 editor.selections.ranges::<Point>(cx),
4391 [
4392 Point::new(0, 7)..Point::new(0, 7),
4393 Point::new(1, 3)..Point::new(1, 3)
4394 ]
4395 );
4396 editor
4397 });
4398}
4399
4400#[gpui::test]
4401async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let mut cx = EditorTestContext::new(cx).await;
4405
4406 let diff_base = r#"
4407 Line 0
4408 Line 1
4409 Line 2
4410 Line 3
4411 "#
4412 .unindent();
4413
4414 cx.set_state(
4415 &r#"
4416 ˇLine 0
4417 Line 1
4418 Line 2
4419 Line 3
4420 "#
4421 .unindent(),
4422 );
4423
4424 cx.set_head_text(&diff_base);
4425 executor.run_until_parked();
4426
4427 // Join lines
4428 cx.update_editor(|editor, window, cx| {
4429 editor.join_lines(&JoinLines, window, cx);
4430 });
4431 executor.run_until_parked();
4432
4433 cx.assert_editor_state(
4434 &r#"
4435 Line 0ˇ Line 1
4436 Line 2
4437 Line 3
4438 "#
4439 .unindent(),
4440 );
4441 // Join again
4442 cx.update_editor(|editor, window, cx| {
4443 editor.join_lines(&JoinLines, window, cx);
4444 });
4445 executor.run_until_parked();
4446
4447 cx.assert_editor_state(
4448 &r#"
4449 Line 0 Line 1ˇ Line 2
4450 Line 3
4451 "#
4452 .unindent(),
4453 );
4454}
4455
4456#[gpui::test]
4457async fn test_custom_newlines_cause_no_false_positive_diffs(
4458 executor: BackgroundExecutor,
4459 cx: &mut TestAppContext,
4460) {
4461 init_test(cx, |_| {});
4462 let mut cx = EditorTestContext::new(cx).await;
4463 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4464 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4465 executor.run_until_parked();
4466
4467 cx.update_editor(|editor, window, cx| {
4468 let snapshot = editor.snapshot(window, cx);
4469 assert_eq!(
4470 snapshot
4471 .buffer_snapshot
4472 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4473 .collect::<Vec<_>>(),
4474 Vec::new(),
4475 "Should not have any diffs for files with custom newlines"
4476 );
4477 });
4478}
4479
4480#[gpui::test]
4481async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4482 init_test(cx, |_| {});
4483
4484 let mut cx = EditorTestContext::new(cx).await;
4485
4486 // Test sort_lines_case_insensitive()
4487 cx.set_state(indoc! {"
4488 «z
4489 y
4490 x
4491 Z
4492 Y
4493 Xˇ»
4494 "});
4495 cx.update_editor(|e, window, cx| {
4496 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4497 });
4498 cx.assert_editor_state(indoc! {"
4499 «x
4500 X
4501 y
4502 Y
4503 z
4504 Zˇ»
4505 "});
4506
4507 // Test sort_lines_by_length()
4508 //
4509 // Demonstrates:
4510 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4511 // - sort is stable
4512 cx.set_state(indoc! {"
4513 «123
4514 æ
4515 12
4516 ∞
4517 1
4518 æˇ»
4519 "});
4520 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4521 cx.assert_editor_state(indoc! {"
4522 «æ
4523 ∞
4524 1
4525 æ
4526 12
4527 123ˇ»
4528 "});
4529
4530 // Test reverse_lines()
4531 cx.set_state(indoc! {"
4532 «5
4533 4
4534 3
4535 2
4536 1ˇ»
4537 "});
4538 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4539 cx.assert_editor_state(indoc! {"
4540 «1
4541 2
4542 3
4543 4
4544 5ˇ»
4545 "});
4546
4547 // Skip testing shuffle_line()
4548
4549 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4550 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4551
4552 // Don't manipulate when cursor is on single line, but expand the selection
4553 cx.set_state(indoc! {"
4554 ddˇdd
4555 ccc
4556 bb
4557 a
4558 "});
4559 cx.update_editor(|e, window, cx| {
4560 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4561 });
4562 cx.assert_editor_state(indoc! {"
4563 «ddddˇ»
4564 ccc
4565 bb
4566 a
4567 "});
4568
4569 // Basic manipulate case
4570 // Start selection moves to column 0
4571 // End of selection shrinks to fit shorter line
4572 cx.set_state(indoc! {"
4573 dd«d
4574 ccc
4575 bb
4576 aaaaaˇ»
4577 "});
4578 cx.update_editor(|e, window, cx| {
4579 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4580 });
4581 cx.assert_editor_state(indoc! {"
4582 «aaaaa
4583 bb
4584 ccc
4585 dddˇ»
4586 "});
4587
4588 // Manipulate case with newlines
4589 cx.set_state(indoc! {"
4590 dd«d
4591 ccc
4592
4593 bb
4594 aaaaa
4595
4596 ˇ»
4597 "});
4598 cx.update_editor(|e, window, cx| {
4599 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4600 });
4601 cx.assert_editor_state(indoc! {"
4602 «
4603
4604 aaaaa
4605 bb
4606 ccc
4607 dddˇ»
4608
4609 "});
4610
4611 // Adding new line
4612 cx.set_state(indoc! {"
4613 aa«a
4614 bbˇ»b
4615 "});
4616 cx.update_editor(|e, window, cx| {
4617 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4618 });
4619 cx.assert_editor_state(indoc! {"
4620 «aaa
4621 bbb
4622 added_lineˇ»
4623 "});
4624
4625 // Removing line
4626 cx.set_state(indoc! {"
4627 aa«a
4628 bbbˇ»
4629 "});
4630 cx.update_editor(|e, window, cx| {
4631 e.manipulate_immutable_lines(window, cx, |lines| {
4632 lines.pop();
4633 })
4634 });
4635 cx.assert_editor_state(indoc! {"
4636 «aaaˇ»
4637 "});
4638
4639 // Removing all lines
4640 cx.set_state(indoc! {"
4641 aa«a
4642 bbbˇ»
4643 "});
4644 cx.update_editor(|e, window, cx| {
4645 e.manipulate_immutable_lines(window, cx, |lines| {
4646 lines.drain(..);
4647 })
4648 });
4649 cx.assert_editor_state(indoc! {"
4650 ˇ
4651 "});
4652}
4653
4654#[gpui::test]
4655async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4656 init_test(cx, |_| {});
4657
4658 let mut cx = EditorTestContext::new(cx).await;
4659
4660 // Consider continuous selection as single selection
4661 cx.set_state(indoc! {"
4662 Aaa«aa
4663 cˇ»c«c
4664 bb
4665 aaaˇ»aa
4666 "});
4667 cx.update_editor(|e, window, cx| {
4668 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4669 });
4670 cx.assert_editor_state(indoc! {"
4671 «Aaaaa
4672 ccc
4673 bb
4674 aaaaaˇ»
4675 "});
4676
4677 cx.set_state(indoc! {"
4678 Aaa«aa
4679 cˇ»c«c
4680 bb
4681 aaaˇ»aa
4682 "});
4683 cx.update_editor(|e, window, cx| {
4684 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4685 });
4686 cx.assert_editor_state(indoc! {"
4687 «Aaaaa
4688 ccc
4689 bbˇ»
4690 "});
4691
4692 // Consider non continuous selection as distinct dedup operations
4693 cx.set_state(indoc! {"
4694 «aaaaa
4695 bb
4696 aaaaa
4697 aaaaaˇ»
4698
4699 aaa«aaˇ»
4700 "});
4701 cx.update_editor(|e, window, cx| {
4702 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4703 });
4704 cx.assert_editor_state(indoc! {"
4705 «aaaaa
4706 bbˇ»
4707
4708 «aaaaaˇ»
4709 "});
4710}
4711
4712#[gpui::test]
4713async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4714 init_test(cx, |_| {});
4715
4716 let mut cx = EditorTestContext::new(cx).await;
4717
4718 cx.set_state(indoc! {"
4719 «Aaa
4720 aAa
4721 Aaaˇ»
4722 "});
4723 cx.update_editor(|e, window, cx| {
4724 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4725 });
4726 cx.assert_editor_state(indoc! {"
4727 «Aaa
4728 aAaˇ»
4729 "});
4730
4731 cx.set_state(indoc! {"
4732 «Aaa
4733 aAa
4734 aaAˇ»
4735 "});
4736 cx.update_editor(|e, window, cx| {
4737 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4738 });
4739 cx.assert_editor_state(indoc! {"
4740 «Aaaˇ»
4741 "});
4742}
4743
4744#[gpui::test]
4745async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4746 init_test(cx, |_| {});
4747
4748 let mut cx = EditorTestContext::new(cx).await;
4749
4750 let js_language = Arc::new(Language::new(
4751 LanguageConfig {
4752 name: "JavaScript".into(),
4753 wrap_characters: Some(language::WrapCharactersConfig {
4754 start_prefix: "<".into(),
4755 start_suffix: ">".into(),
4756 end_prefix: "</".into(),
4757 end_suffix: ">".into(),
4758 }),
4759 ..LanguageConfig::default()
4760 },
4761 None,
4762 ));
4763
4764 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4765
4766 cx.set_state(indoc! {"
4767 «testˇ»
4768 "});
4769 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4770 cx.assert_editor_state(indoc! {"
4771 <«ˇ»>test</«ˇ»>
4772 "});
4773
4774 cx.set_state(indoc! {"
4775 «test
4776 testˇ»
4777 "});
4778 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4779 cx.assert_editor_state(indoc! {"
4780 <«ˇ»>test
4781 test</«ˇ»>
4782 "});
4783
4784 cx.set_state(indoc! {"
4785 teˇst
4786 "});
4787 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4788 cx.assert_editor_state(indoc! {"
4789 te<«ˇ»></«ˇ»>st
4790 "});
4791}
4792
4793#[gpui::test]
4794async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4795 init_test(cx, |_| {});
4796
4797 let mut cx = EditorTestContext::new(cx).await;
4798
4799 let js_language = Arc::new(Language::new(
4800 LanguageConfig {
4801 name: "JavaScript".into(),
4802 wrap_characters: Some(language::WrapCharactersConfig {
4803 start_prefix: "<".into(),
4804 start_suffix: ">".into(),
4805 end_prefix: "</".into(),
4806 end_suffix: ">".into(),
4807 }),
4808 ..LanguageConfig::default()
4809 },
4810 None,
4811 ));
4812
4813 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4814
4815 cx.set_state(indoc! {"
4816 «testˇ»
4817 «testˇ» «testˇ»
4818 «testˇ»
4819 "});
4820 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4821 cx.assert_editor_state(indoc! {"
4822 <«ˇ»>test</«ˇ»>
4823 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4824 <«ˇ»>test</«ˇ»>
4825 "});
4826
4827 cx.set_state(indoc! {"
4828 «test
4829 testˇ»
4830 «test
4831 testˇ»
4832 "});
4833 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4834 cx.assert_editor_state(indoc! {"
4835 <«ˇ»>test
4836 test</«ˇ»>
4837 <«ˇ»>test
4838 test</«ˇ»>
4839 "});
4840}
4841
4842#[gpui::test]
4843async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4844 init_test(cx, |_| {});
4845
4846 let mut cx = EditorTestContext::new(cx).await;
4847
4848 let plaintext_language = Arc::new(Language::new(
4849 LanguageConfig {
4850 name: "Plain Text".into(),
4851 ..LanguageConfig::default()
4852 },
4853 None,
4854 ));
4855
4856 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4857
4858 cx.set_state(indoc! {"
4859 «testˇ»
4860 "});
4861 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4862 cx.assert_editor_state(indoc! {"
4863 «testˇ»
4864 "});
4865}
4866
4867#[gpui::test]
4868async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4869 init_test(cx, |_| {});
4870
4871 let mut cx = EditorTestContext::new(cx).await;
4872
4873 // Manipulate with multiple selections on a single line
4874 cx.set_state(indoc! {"
4875 dd«dd
4876 cˇ»c«c
4877 bb
4878 aaaˇ»aa
4879 "});
4880 cx.update_editor(|e, window, cx| {
4881 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4882 });
4883 cx.assert_editor_state(indoc! {"
4884 «aaaaa
4885 bb
4886 ccc
4887 ddddˇ»
4888 "});
4889
4890 // Manipulate with multiple disjoin selections
4891 cx.set_state(indoc! {"
4892 5«
4893 4
4894 3
4895 2
4896 1ˇ»
4897
4898 dd«dd
4899 ccc
4900 bb
4901 aaaˇ»aa
4902 "});
4903 cx.update_editor(|e, window, cx| {
4904 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4905 });
4906 cx.assert_editor_state(indoc! {"
4907 «1
4908 2
4909 3
4910 4
4911 5ˇ»
4912
4913 «aaaaa
4914 bb
4915 ccc
4916 ddddˇ»
4917 "});
4918
4919 // Adding lines on each selection
4920 cx.set_state(indoc! {"
4921 2«
4922 1ˇ»
4923
4924 bb«bb
4925 aaaˇ»aa
4926 "});
4927 cx.update_editor(|e, window, cx| {
4928 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4929 });
4930 cx.assert_editor_state(indoc! {"
4931 «2
4932 1
4933 added lineˇ»
4934
4935 «bbbb
4936 aaaaa
4937 added lineˇ»
4938 "});
4939
4940 // Removing lines on each selection
4941 cx.set_state(indoc! {"
4942 2«
4943 1ˇ»
4944
4945 bb«bb
4946 aaaˇ»aa
4947 "});
4948 cx.update_editor(|e, window, cx| {
4949 e.manipulate_immutable_lines(window, cx, |lines| {
4950 lines.pop();
4951 })
4952 });
4953 cx.assert_editor_state(indoc! {"
4954 «2ˇ»
4955
4956 «bbbbˇ»
4957 "});
4958}
4959
4960#[gpui::test]
4961async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4962 init_test(cx, |settings| {
4963 settings.defaults.tab_size = NonZeroU32::new(3)
4964 });
4965
4966 let mut cx = EditorTestContext::new(cx).await;
4967
4968 // MULTI SELECTION
4969 // Ln.1 "«" tests empty lines
4970 // Ln.9 tests just leading whitespace
4971 cx.set_state(indoc! {"
4972 «
4973 abc // No indentationˇ»
4974 «\tabc // 1 tabˇ»
4975 \t\tabc « ˇ» // 2 tabs
4976 \t ab«c // Tab followed by space
4977 \tabc // Space followed by tab (3 spaces should be the result)
4978 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4979 abˇ»ˇc ˇ ˇ // Already space indented«
4980 \t
4981 \tabc\tdef // Only the leading tab is manipulatedˇ»
4982 "});
4983 cx.update_editor(|e, window, cx| {
4984 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4985 });
4986 cx.assert_editor_state(
4987 indoc! {"
4988 «
4989 abc // No indentation
4990 abc // 1 tab
4991 abc // 2 tabs
4992 abc // Tab followed by space
4993 abc // Space followed by tab (3 spaces should be the result)
4994 abc // Mixed indentation (tab conversion depends on the column)
4995 abc // Already space indented
4996 ·
4997 abc\tdef // Only the leading tab is manipulatedˇ»
4998 "}
4999 .replace("·", "")
5000 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5001 );
5002
5003 // Test on just a few lines, the others should remain unchanged
5004 // Only lines (3, 5, 10, 11) should change
5005 cx.set_state(
5006 indoc! {"
5007 ·
5008 abc // No indentation
5009 \tabcˇ // 1 tab
5010 \t\tabc // 2 tabs
5011 \t abcˇ // Tab followed by space
5012 \tabc // Space followed by tab (3 spaces should be the result)
5013 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5014 abc // Already space indented
5015 «\t
5016 \tabc\tdef // Only the leading tab is manipulatedˇ»
5017 "}
5018 .replace("·", "")
5019 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5020 );
5021 cx.update_editor(|e, window, cx| {
5022 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5023 });
5024 cx.assert_editor_state(
5025 indoc! {"
5026 ·
5027 abc // No indentation
5028 « abc // 1 tabˇ»
5029 \t\tabc // 2 tabs
5030 « abc // Tab followed by spaceˇ»
5031 \tabc // Space followed by tab (3 spaces should be the result)
5032 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5033 abc // Already space indented
5034 « ·
5035 abc\tdef // Only the leading tab is manipulatedˇ»
5036 "}
5037 .replace("·", "")
5038 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5039 );
5040
5041 // SINGLE SELECTION
5042 // Ln.1 "«" tests empty lines
5043 // Ln.9 tests just leading whitespace
5044 cx.set_state(indoc! {"
5045 «
5046 abc // No indentation
5047 \tabc // 1 tab
5048 \t\tabc // 2 tabs
5049 \t abc // Tab followed by space
5050 \tabc // Space followed by tab (3 spaces should be the result)
5051 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5052 abc // Already space indented
5053 \t
5054 \tabc\tdef // Only the leading tab is manipulatedˇ»
5055 "});
5056 cx.update_editor(|e, window, cx| {
5057 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5058 });
5059 cx.assert_editor_state(
5060 indoc! {"
5061 «
5062 abc // No indentation
5063 abc // 1 tab
5064 abc // 2 tabs
5065 abc // Tab followed by space
5066 abc // Space followed by tab (3 spaces should be the result)
5067 abc // Mixed indentation (tab conversion depends on the column)
5068 abc // Already space indented
5069 ·
5070 abc\tdef // Only the leading tab is manipulatedˇ»
5071 "}
5072 .replace("·", "")
5073 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5074 );
5075}
5076
5077#[gpui::test]
5078async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5079 init_test(cx, |settings| {
5080 settings.defaults.tab_size = NonZeroU32::new(3)
5081 });
5082
5083 let mut cx = EditorTestContext::new(cx).await;
5084
5085 // MULTI SELECTION
5086 // Ln.1 "«" tests empty lines
5087 // Ln.11 tests just leading whitespace
5088 cx.set_state(indoc! {"
5089 «
5090 abˇ»ˇc // No indentation
5091 abc ˇ ˇ // 1 space (< 3 so dont convert)
5092 abc « // 2 spaces (< 3 so dont convert)
5093 abc // 3 spaces (convert)
5094 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5095 «\tˇ»\t«\tˇ»abc // Already tab indented
5096 «\t abc // Tab followed by space
5097 \tabc // Space followed by tab (should be consumed due to tab)
5098 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5099 \tˇ» «\t
5100 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5101 "});
5102 cx.update_editor(|e, window, cx| {
5103 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5104 });
5105 cx.assert_editor_state(indoc! {"
5106 «
5107 abc // No indentation
5108 abc // 1 space (< 3 so dont convert)
5109 abc // 2 spaces (< 3 so dont convert)
5110 \tabc // 3 spaces (convert)
5111 \t abc // 5 spaces (1 tab + 2 spaces)
5112 \t\t\tabc // Already tab indented
5113 \t abc // Tab followed by space
5114 \tabc // Space followed by tab (should be consumed due to tab)
5115 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5116 \t\t\t
5117 \tabc \t // Only the leading spaces should be convertedˇ»
5118 "});
5119
5120 // Test on just a few lines, the other should remain unchanged
5121 // Only lines (4, 8, 11, 12) should change
5122 cx.set_state(
5123 indoc! {"
5124 ·
5125 abc // No indentation
5126 abc // 1 space (< 3 so dont convert)
5127 abc // 2 spaces (< 3 so dont convert)
5128 « abc // 3 spaces (convert)ˇ»
5129 abc // 5 spaces (1 tab + 2 spaces)
5130 \t\t\tabc // Already tab indented
5131 \t abc // Tab followed by space
5132 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5133 \t\t \tabc // Mixed indentation
5134 \t \t \t \tabc // Mixed indentation
5135 \t \tˇ
5136 « abc \t // Only the leading spaces should be convertedˇ»
5137 "}
5138 .replace("·", "")
5139 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5140 );
5141 cx.update_editor(|e, window, cx| {
5142 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5143 });
5144 cx.assert_editor_state(
5145 indoc! {"
5146 ·
5147 abc // No indentation
5148 abc // 1 space (< 3 so dont convert)
5149 abc // 2 spaces (< 3 so dont convert)
5150 «\tabc // 3 spaces (convert)ˇ»
5151 abc // 5 spaces (1 tab + 2 spaces)
5152 \t\t\tabc // Already tab indented
5153 \t abc // Tab followed by space
5154 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5155 \t\t \tabc // Mixed indentation
5156 \t \t \t \tabc // Mixed indentation
5157 «\t\t\t
5158 \tabc \t // Only the leading spaces should be convertedˇ»
5159 "}
5160 .replace("·", "")
5161 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5162 );
5163
5164 // SINGLE SELECTION
5165 // Ln.1 "«" tests empty lines
5166 // Ln.11 tests just leading whitespace
5167 cx.set_state(indoc! {"
5168 «
5169 abc // No indentation
5170 abc // 1 space (< 3 so dont convert)
5171 abc // 2 spaces (< 3 so dont convert)
5172 abc // 3 spaces (convert)
5173 abc // 5 spaces (1 tab + 2 spaces)
5174 \t\t\tabc // Already tab indented
5175 \t abc // Tab followed by space
5176 \tabc // Space followed by tab (should be consumed due to tab)
5177 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5178 \t \t
5179 abc \t // Only the leading spaces should be convertedˇ»
5180 "});
5181 cx.update_editor(|e, window, cx| {
5182 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5183 });
5184 cx.assert_editor_state(indoc! {"
5185 «
5186 abc // No indentation
5187 abc // 1 space (< 3 so dont convert)
5188 abc // 2 spaces (< 3 so dont convert)
5189 \tabc // 3 spaces (convert)
5190 \t abc // 5 spaces (1 tab + 2 spaces)
5191 \t\t\tabc // Already tab indented
5192 \t abc // Tab followed by space
5193 \tabc // Space followed by tab (should be consumed due to tab)
5194 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5195 \t\t\t
5196 \tabc \t // Only the leading spaces should be convertedˇ»
5197 "});
5198}
5199
5200#[gpui::test]
5201async fn test_toggle_case(cx: &mut TestAppContext) {
5202 init_test(cx, |_| {});
5203
5204 let mut cx = EditorTestContext::new(cx).await;
5205
5206 // If all lower case -> upper case
5207 cx.set_state(indoc! {"
5208 «hello worldˇ»
5209 "});
5210 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5211 cx.assert_editor_state(indoc! {"
5212 «HELLO WORLDˇ»
5213 "});
5214
5215 // If all upper case -> lower case
5216 cx.set_state(indoc! {"
5217 «HELLO WORLDˇ»
5218 "});
5219 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5220 cx.assert_editor_state(indoc! {"
5221 «hello worldˇ»
5222 "});
5223
5224 // If any upper case characters are identified -> lower case
5225 // This matches JetBrains IDEs
5226 cx.set_state(indoc! {"
5227 «hEllo worldˇ»
5228 "});
5229 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5230 cx.assert_editor_state(indoc! {"
5231 «hello worldˇ»
5232 "});
5233}
5234
5235#[gpui::test]
5236async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5237 init_test(cx, |_| {});
5238
5239 let mut cx = EditorTestContext::new(cx).await;
5240
5241 cx.set_state(indoc! {"
5242 «implement-windows-supportˇ»
5243 "});
5244 cx.update_editor(|e, window, cx| {
5245 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5246 });
5247 cx.assert_editor_state(indoc! {"
5248 «Implement windows supportˇ»
5249 "});
5250}
5251
5252#[gpui::test]
5253async fn test_manipulate_text(cx: &mut TestAppContext) {
5254 init_test(cx, |_| {});
5255
5256 let mut cx = EditorTestContext::new(cx).await;
5257
5258 // Test convert_to_upper_case()
5259 cx.set_state(indoc! {"
5260 «hello worldˇ»
5261 "});
5262 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5263 cx.assert_editor_state(indoc! {"
5264 «HELLO WORLDˇ»
5265 "});
5266
5267 // Test convert_to_lower_case()
5268 cx.set_state(indoc! {"
5269 «HELLO WORLDˇ»
5270 "});
5271 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5272 cx.assert_editor_state(indoc! {"
5273 «hello worldˇ»
5274 "});
5275
5276 // Test multiple line, single selection case
5277 cx.set_state(indoc! {"
5278 «The quick brown
5279 fox jumps over
5280 the lazy dogˇ»
5281 "});
5282 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5283 cx.assert_editor_state(indoc! {"
5284 «The Quick Brown
5285 Fox Jumps Over
5286 The Lazy Dogˇ»
5287 "});
5288
5289 // Test multiple line, single selection case
5290 cx.set_state(indoc! {"
5291 «The quick brown
5292 fox jumps over
5293 the lazy dogˇ»
5294 "});
5295 cx.update_editor(|e, window, cx| {
5296 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5297 });
5298 cx.assert_editor_state(indoc! {"
5299 «TheQuickBrown
5300 FoxJumpsOver
5301 TheLazyDogˇ»
5302 "});
5303
5304 // From here on out, test more complex cases of manipulate_text()
5305
5306 // Test no selection case - should affect words cursors are in
5307 // Cursor at beginning, middle, and end of word
5308 cx.set_state(indoc! {"
5309 ˇhello big beauˇtiful worldˇ
5310 "});
5311 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5312 cx.assert_editor_state(indoc! {"
5313 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5314 "});
5315
5316 // Test multiple selections on a single line and across multiple lines
5317 cx.set_state(indoc! {"
5318 «Theˇ» quick «brown
5319 foxˇ» jumps «overˇ»
5320 the «lazyˇ» dog
5321 "});
5322 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5323 cx.assert_editor_state(indoc! {"
5324 «THEˇ» quick «BROWN
5325 FOXˇ» jumps «OVERˇ»
5326 the «LAZYˇ» dog
5327 "});
5328
5329 // Test case where text length grows
5330 cx.set_state(indoc! {"
5331 «tschüߡ»
5332 "});
5333 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5334 cx.assert_editor_state(indoc! {"
5335 «TSCHÜSSˇ»
5336 "});
5337
5338 // Test to make sure we don't crash when text shrinks
5339 cx.set_state(indoc! {"
5340 aaa_bbbˇ
5341 "});
5342 cx.update_editor(|e, window, cx| {
5343 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5344 });
5345 cx.assert_editor_state(indoc! {"
5346 «aaaBbbˇ»
5347 "});
5348
5349 // Test to make sure we all aware of the fact that each word can grow and shrink
5350 // Final selections should be aware of this fact
5351 cx.set_state(indoc! {"
5352 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5353 "});
5354 cx.update_editor(|e, window, cx| {
5355 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5356 });
5357 cx.assert_editor_state(indoc! {"
5358 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5359 "});
5360
5361 cx.set_state(indoc! {"
5362 «hElLo, WoRld!ˇ»
5363 "});
5364 cx.update_editor(|e, window, cx| {
5365 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5366 });
5367 cx.assert_editor_state(indoc! {"
5368 «HeLlO, wOrLD!ˇ»
5369 "});
5370
5371 // Test selections with `line_mode = true`.
5372 cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
5373 cx.set_state(indoc! {"
5374 «The quick brown
5375 fox jumps over
5376 tˇ»he lazy dog
5377 "});
5378 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5379 cx.assert_editor_state(indoc! {"
5380 «THE QUICK BROWN
5381 FOX JUMPS OVER
5382 THE LAZY DOGˇ»
5383 "});
5384}
5385
5386#[gpui::test]
5387fn test_duplicate_line(cx: &mut TestAppContext) {
5388 init_test(cx, |_| {});
5389
5390 let editor = cx.add_window(|window, cx| {
5391 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5392 build_editor(buffer, window, cx)
5393 });
5394 _ = editor.update(cx, |editor, window, cx| {
5395 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5396 s.select_display_ranges([
5397 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5398 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5399 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5400 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5401 ])
5402 });
5403 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5404 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5405 assert_eq!(
5406 editor.selections.display_ranges(cx),
5407 vec![
5408 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5409 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5410 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5411 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5412 ]
5413 );
5414 });
5415
5416 let editor = cx.add_window(|window, cx| {
5417 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5418 build_editor(buffer, window, cx)
5419 });
5420 _ = editor.update(cx, |editor, window, cx| {
5421 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5422 s.select_display_ranges([
5423 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5424 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5425 ])
5426 });
5427 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5428 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5429 assert_eq!(
5430 editor.selections.display_ranges(cx),
5431 vec![
5432 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5433 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5434 ]
5435 );
5436 });
5437
5438 // With `move_upwards` the selections stay in place, except for
5439 // the lines inserted above them
5440 let editor = cx.add_window(|window, cx| {
5441 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5442 build_editor(buffer, window, cx)
5443 });
5444 _ = editor.update(cx, |editor, window, cx| {
5445 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5446 s.select_display_ranges([
5447 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5448 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5449 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5450 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5451 ])
5452 });
5453 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5454 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5455 assert_eq!(
5456 editor.selections.display_ranges(cx),
5457 vec![
5458 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5459 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5460 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5461 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5462 ]
5463 );
5464 });
5465
5466 let editor = cx.add_window(|window, cx| {
5467 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5468 build_editor(buffer, window, cx)
5469 });
5470 _ = editor.update(cx, |editor, window, cx| {
5471 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5472 s.select_display_ranges([
5473 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5474 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5475 ])
5476 });
5477 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5478 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5479 assert_eq!(
5480 editor.selections.display_ranges(cx),
5481 vec![
5482 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5483 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5484 ]
5485 );
5486 });
5487
5488 let editor = cx.add_window(|window, cx| {
5489 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5490 build_editor(buffer, window, cx)
5491 });
5492 _ = editor.update(cx, |editor, window, cx| {
5493 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5494 s.select_display_ranges([
5495 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5496 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5497 ])
5498 });
5499 editor.duplicate_selection(&DuplicateSelection, window, cx);
5500 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5501 assert_eq!(
5502 editor.selections.display_ranges(cx),
5503 vec![
5504 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5505 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5506 ]
5507 );
5508 });
5509}
5510
5511#[gpui::test]
5512fn test_move_line_up_down(cx: &mut TestAppContext) {
5513 init_test(cx, |_| {});
5514
5515 let editor = cx.add_window(|window, cx| {
5516 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5517 build_editor(buffer, window, cx)
5518 });
5519 _ = editor.update(cx, |editor, window, cx| {
5520 editor.fold_creases(
5521 vec![
5522 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5523 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5524 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5525 ],
5526 true,
5527 window,
5528 cx,
5529 );
5530 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5531 s.select_display_ranges([
5532 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5533 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5534 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5535 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5536 ])
5537 });
5538 assert_eq!(
5539 editor.display_text(cx),
5540 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5541 );
5542
5543 editor.move_line_up(&MoveLineUp, window, cx);
5544 assert_eq!(
5545 editor.display_text(cx),
5546 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5547 );
5548 assert_eq!(
5549 editor.selections.display_ranges(cx),
5550 vec![
5551 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5552 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5553 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5554 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5555 ]
5556 );
5557 });
5558
5559 _ = editor.update(cx, |editor, window, cx| {
5560 editor.move_line_down(&MoveLineDown, window, cx);
5561 assert_eq!(
5562 editor.display_text(cx),
5563 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5564 );
5565 assert_eq!(
5566 editor.selections.display_ranges(cx),
5567 vec![
5568 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5569 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5570 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5571 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5572 ]
5573 );
5574 });
5575
5576 _ = editor.update(cx, |editor, window, cx| {
5577 editor.move_line_down(&MoveLineDown, window, cx);
5578 assert_eq!(
5579 editor.display_text(cx),
5580 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5581 );
5582 assert_eq!(
5583 editor.selections.display_ranges(cx),
5584 vec![
5585 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5586 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5587 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5588 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5589 ]
5590 );
5591 });
5592
5593 _ = editor.update(cx, |editor, window, cx| {
5594 editor.move_line_up(&MoveLineUp, window, cx);
5595 assert_eq!(
5596 editor.display_text(cx),
5597 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5598 );
5599 assert_eq!(
5600 editor.selections.display_ranges(cx),
5601 vec![
5602 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5603 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5604 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5605 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5606 ]
5607 );
5608 });
5609}
5610
5611#[gpui::test]
5612fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5613 init_test(cx, |_| {});
5614 let editor = cx.add_window(|window, cx| {
5615 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5616 build_editor(buffer, window, cx)
5617 });
5618 _ = editor.update(cx, |editor, window, cx| {
5619 editor.fold_creases(
5620 vec![Crease::simple(
5621 Point::new(6, 4)..Point::new(7, 4),
5622 FoldPlaceholder::test(),
5623 )],
5624 true,
5625 window,
5626 cx,
5627 );
5628 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5629 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5630 });
5631 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5632 editor.move_line_up(&MoveLineUp, window, cx);
5633 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5634 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5635 });
5636}
5637
5638#[gpui::test]
5639fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5640 init_test(cx, |_| {});
5641
5642 let editor = cx.add_window(|window, cx| {
5643 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5644 build_editor(buffer, window, cx)
5645 });
5646 _ = editor.update(cx, |editor, window, cx| {
5647 let snapshot = editor.buffer.read(cx).snapshot(cx);
5648 editor.insert_blocks(
5649 [BlockProperties {
5650 style: BlockStyle::Fixed,
5651 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5652 height: Some(1),
5653 render: Arc::new(|_| div().into_any()),
5654 priority: 0,
5655 }],
5656 Some(Autoscroll::fit()),
5657 cx,
5658 );
5659 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5660 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5661 });
5662 editor.move_line_down(&MoveLineDown, window, cx);
5663 });
5664}
5665
5666#[gpui::test]
5667async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5668 init_test(cx, |_| {});
5669
5670 let mut cx = EditorTestContext::new(cx).await;
5671 cx.set_state(
5672 &"
5673 ˇzero
5674 one
5675 two
5676 three
5677 four
5678 five
5679 "
5680 .unindent(),
5681 );
5682
5683 // Create a four-line block that replaces three lines of text.
5684 cx.update_editor(|editor, window, cx| {
5685 let snapshot = editor.snapshot(window, cx);
5686 let snapshot = &snapshot.buffer_snapshot;
5687 let placement = BlockPlacement::Replace(
5688 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5689 );
5690 editor.insert_blocks(
5691 [BlockProperties {
5692 placement,
5693 height: Some(4),
5694 style: BlockStyle::Sticky,
5695 render: Arc::new(|_| gpui::div().into_any_element()),
5696 priority: 0,
5697 }],
5698 None,
5699 cx,
5700 );
5701 });
5702
5703 // Move down so that the cursor touches the block.
5704 cx.update_editor(|editor, window, cx| {
5705 editor.move_down(&Default::default(), window, cx);
5706 });
5707 cx.assert_editor_state(
5708 &"
5709 zero
5710 «one
5711 two
5712 threeˇ»
5713 four
5714 five
5715 "
5716 .unindent(),
5717 );
5718
5719 // Move down past the block.
5720 cx.update_editor(|editor, window, cx| {
5721 editor.move_down(&Default::default(), window, cx);
5722 });
5723 cx.assert_editor_state(
5724 &"
5725 zero
5726 one
5727 two
5728 three
5729 ˇfour
5730 five
5731 "
5732 .unindent(),
5733 );
5734}
5735
5736#[gpui::test]
5737fn test_transpose(cx: &mut TestAppContext) {
5738 init_test(cx, |_| {});
5739
5740 _ = cx.add_window(|window, cx| {
5741 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5742 editor.set_style(EditorStyle::default(), window, cx);
5743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5744 s.select_ranges([1..1])
5745 });
5746 editor.transpose(&Default::default(), window, cx);
5747 assert_eq!(editor.text(cx), "bac");
5748 assert_eq!(editor.selections.ranges(cx), [2..2]);
5749
5750 editor.transpose(&Default::default(), window, cx);
5751 assert_eq!(editor.text(cx), "bca");
5752 assert_eq!(editor.selections.ranges(cx), [3..3]);
5753
5754 editor.transpose(&Default::default(), window, cx);
5755 assert_eq!(editor.text(cx), "bac");
5756 assert_eq!(editor.selections.ranges(cx), [3..3]);
5757
5758 editor
5759 });
5760
5761 _ = cx.add_window(|window, cx| {
5762 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5763 editor.set_style(EditorStyle::default(), window, cx);
5764 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5765 s.select_ranges([3..3])
5766 });
5767 editor.transpose(&Default::default(), window, cx);
5768 assert_eq!(editor.text(cx), "acb\nde");
5769 assert_eq!(editor.selections.ranges(cx), [3..3]);
5770
5771 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5772 s.select_ranges([4..4])
5773 });
5774 editor.transpose(&Default::default(), window, cx);
5775 assert_eq!(editor.text(cx), "acbd\ne");
5776 assert_eq!(editor.selections.ranges(cx), [5..5]);
5777
5778 editor.transpose(&Default::default(), window, cx);
5779 assert_eq!(editor.text(cx), "acbde\n");
5780 assert_eq!(editor.selections.ranges(cx), [6..6]);
5781
5782 editor.transpose(&Default::default(), window, cx);
5783 assert_eq!(editor.text(cx), "acbd\ne");
5784 assert_eq!(editor.selections.ranges(cx), [6..6]);
5785
5786 editor
5787 });
5788
5789 _ = cx.add_window(|window, cx| {
5790 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5791 editor.set_style(EditorStyle::default(), window, cx);
5792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5793 s.select_ranges([1..1, 2..2, 4..4])
5794 });
5795 editor.transpose(&Default::default(), window, cx);
5796 assert_eq!(editor.text(cx), "bacd\ne");
5797 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5798
5799 editor.transpose(&Default::default(), window, cx);
5800 assert_eq!(editor.text(cx), "bcade\n");
5801 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5802
5803 editor.transpose(&Default::default(), window, cx);
5804 assert_eq!(editor.text(cx), "bcda\ne");
5805 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5806
5807 editor.transpose(&Default::default(), window, cx);
5808 assert_eq!(editor.text(cx), "bcade\n");
5809 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5810
5811 editor.transpose(&Default::default(), window, cx);
5812 assert_eq!(editor.text(cx), "bcaed\n");
5813 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5814
5815 editor
5816 });
5817
5818 _ = cx.add_window(|window, cx| {
5819 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5820 editor.set_style(EditorStyle::default(), window, cx);
5821 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5822 s.select_ranges([4..4])
5823 });
5824 editor.transpose(&Default::default(), window, cx);
5825 assert_eq!(editor.text(cx), "🏀🍐✋");
5826 assert_eq!(editor.selections.ranges(cx), [8..8]);
5827
5828 editor.transpose(&Default::default(), window, cx);
5829 assert_eq!(editor.text(cx), "🏀✋🍐");
5830 assert_eq!(editor.selections.ranges(cx), [11..11]);
5831
5832 editor.transpose(&Default::default(), window, cx);
5833 assert_eq!(editor.text(cx), "🏀🍐✋");
5834 assert_eq!(editor.selections.ranges(cx), [11..11]);
5835
5836 editor
5837 });
5838}
5839
5840#[gpui::test]
5841async fn test_rewrap(cx: &mut TestAppContext) {
5842 init_test(cx, |settings| {
5843 settings.languages.0.extend([
5844 (
5845 "Markdown".into(),
5846 LanguageSettingsContent {
5847 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5848 preferred_line_length: Some(40),
5849 ..Default::default()
5850 },
5851 ),
5852 (
5853 "Plain Text".into(),
5854 LanguageSettingsContent {
5855 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5856 preferred_line_length: Some(40),
5857 ..Default::default()
5858 },
5859 ),
5860 (
5861 "C++".into(),
5862 LanguageSettingsContent {
5863 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5864 preferred_line_length: Some(40),
5865 ..Default::default()
5866 },
5867 ),
5868 (
5869 "Python".into(),
5870 LanguageSettingsContent {
5871 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5872 preferred_line_length: Some(40),
5873 ..Default::default()
5874 },
5875 ),
5876 (
5877 "Rust".into(),
5878 LanguageSettingsContent {
5879 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5880 preferred_line_length: Some(40),
5881 ..Default::default()
5882 },
5883 ),
5884 ])
5885 });
5886
5887 let mut cx = EditorTestContext::new(cx).await;
5888
5889 let cpp_language = Arc::new(Language::new(
5890 LanguageConfig {
5891 name: "C++".into(),
5892 line_comments: vec!["// ".into()],
5893 ..LanguageConfig::default()
5894 },
5895 None,
5896 ));
5897 let python_language = Arc::new(Language::new(
5898 LanguageConfig {
5899 name: "Python".into(),
5900 line_comments: vec!["# ".into()],
5901 ..LanguageConfig::default()
5902 },
5903 None,
5904 ));
5905 let markdown_language = Arc::new(Language::new(
5906 LanguageConfig {
5907 name: "Markdown".into(),
5908 rewrap_prefixes: vec![
5909 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5910 regex::Regex::new("[-*+]\\s+").unwrap(),
5911 ],
5912 ..LanguageConfig::default()
5913 },
5914 None,
5915 ));
5916 let rust_language = Arc::new(
5917 Language::new(
5918 LanguageConfig {
5919 name: "Rust".into(),
5920 line_comments: vec!["// ".into(), "/// ".into()],
5921 ..LanguageConfig::default()
5922 },
5923 Some(tree_sitter_rust::LANGUAGE.into()),
5924 )
5925 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5926 .unwrap(),
5927 );
5928
5929 let plaintext_language = Arc::new(Language::new(
5930 LanguageConfig {
5931 name: "Plain Text".into(),
5932 ..LanguageConfig::default()
5933 },
5934 None,
5935 ));
5936
5937 // Test basic rewrapping of a long line with a cursor
5938 assert_rewrap(
5939 indoc! {"
5940 // ˇThis is a long comment that needs to be wrapped.
5941 "},
5942 indoc! {"
5943 // ˇThis is a long comment that needs to
5944 // be wrapped.
5945 "},
5946 cpp_language.clone(),
5947 &mut cx,
5948 );
5949
5950 // Test rewrapping a full selection
5951 assert_rewrap(
5952 indoc! {"
5953 «// This selected long comment needs to be wrapped.ˇ»"
5954 },
5955 indoc! {"
5956 «// This selected long comment needs to
5957 // be wrapped.ˇ»"
5958 },
5959 cpp_language.clone(),
5960 &mut cx,
5961 );
5962
5963 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5964 assert_rewrap(
5965 indoc! {"
5966 // ˇThis is the first line.
5967 // Thisˇ is the second line.
5968 // This is the thirdˇ line, all part of one paragraph.
5969 "},
5970 indoc! {"
5971 // ˇThis is the first line. Thisˇ is the
5972 // second line. This is the thirdˇ line,
5973 // all part of one paragraph.
5974 "},
5975 cpp_language.clone(),
5976 &mut cx,
5977 );
5978
5979 // Test multiple cursors in different paragraphs trigger separate rewraps
5980 assert_rewrap(
5981 indoc! {"
5982 // ˇThis is the first paragraph, first line.
5983 // ˇThis is the first paragraph, second line.
5984
5985 // ˇThis is the second paragraph, first line.
5986 // ˇThis is the second paragraph, second line.
5987 "},
5988 indoc! {"
5989 // ˇThis is the first paragraph, first
5990 // line. ˇThis is the first paragraph,
5991 // second line.
5992
5993 // ˇThis is the second paragraph, first
5994 // line. ˇThis is the second paragraph,
5995 // second line.
5996 "},
5997 cpp_language.clone(),
5998 &mut cx,
5999 );
6000
6001 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6002 assert_rewrap(
6003 indoc! {"
6004 «// A regular long long comment to be wrapped.
6005 /// A documentation long comment to be wrapped.ˇ»
6006 "},
6007 indoc! {"
6008 «// A regular long long comment to be
6009 // wrapped.
6010 /// A documentation long comment to be
6011 /// wrapped.ˇ»
6012 "},
6013 rust_language.clone(),
6014 &mut cx,
6015 );
6016
6017 // Test that change in indentation level trigger seperate rewraps
6018 assert_rewrap(
6019 indoc! {"
6020 fn foo() {
6021 «// This is a long comment at the base indent.
6022 // This is a long comment at the next indent.ˇ»
6023 }
6024 "},
6025 indoc! {"
6026 fn foo() {
6027 «// This is a long comment at the
6028 // base indent.
6029 // This is a long comment at the
6030 // next indent.ˇ»
6031 }
6032 "},
6033 rust_language.clone(),
6034 &mut cx,
6035 );
6036
6037 // Test that different comment prefix characters (e.g., '#') are handled correctly
6038 assert_rewrap(
6039 indoc! {"
6040 # ˇThis is a long comment using a pound sign.
6041 "},
6042 indoc! {"
6043 # ˇThis is a long comment using a pound
6044 # sign.
6045 "},
6046 python_language,
6047 &mut cx,
6048 );
6049
6050 // Test rewrapping only affects comments, not code even when selected
6051 assert_rewrap(
6052 indoc! {"
6053 «/// This doc comment is long and should be wrapped.
6054 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6055 "},
6056 indoc! {"
6057 «/// This doc comment is long and should
6058 /// be wrapped.
6059 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6060 "},
6061 rust_language.clone(),
6062 &mut cx,
6063 );
6064
6065 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6066 assert_rewrap(
6067 indoc! {"
6068 # Header
6069
6070 A long long long line of markdown text to wrap.ˇ
6071 "},
6072 indoc! {"
6073 # Header
6074
6075 A long long long line of markdown text
6076 to wrap.ˇ
6077 "},
6078 markdown_language.clone(),
6079 &mut cx,
6080 );
6081
6082 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6083 assert_rewrap(
6084 indoc! {"
6085 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6086 2. This is a numbered list item that is very long and needs to be wrapped properly.
6087 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6088 "},
6089 indoc! {"
6090 «1. This is a numbered list item that is
6091 very long and needs to be wrapped
6092 properly.
6093 2. This is a numbered list item that is
6094 very long and needs to be wrapped
6095 properly.
6096 - This is an unordered list item that is
6097 also very long and should not merge
6098 with the numbered item.ˇ»
6099 "},
6100 markdown_language.clone(),
6101 &mut cx,
6102 );
6103
6104 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6105 assert_rewrap(
6106 indoc! {"
6107 «1. This is a numbered list item that is
6108 very long and needs to be wrapped
6109 properly.
6110 2. This is a numbered list item that is
6111 very long and needs to be wrapped
6112 properly.
6113 - This is an unordered list item that is
6114 also very long and should not merge with
6115 the numbered item.ˇ»
6116 "},
6117 indoc! {"
6118 «1. This is a numbered list item that is
6119 very long and needs to be wrapped
6120 properly.
6121 2. This is a numbered list item that is
6122 very long and needs to be wrapped
6123 properly.
6124 - This is an unordered list item that is
6125 also very long and should not merge
6126 with the numbered item.ˇ»
6127 "},
6128 markdown_language.clone(),
6129 &mut cx,
6130 );
6131
6132 // Test that rewrapping maintain indents even when they already exists.
6133 assert_rewrap(
6134 indoc! {"
6135 «1. This is a numbered list
6136 item that is very long and needs to be wrapped properly.
6137 2. This is a numbered list
6138 item that is very long and needs to be wrapped properly.
6139 - This is an unordered list item that is also very long and
6140 should not merge with the numbered item.ˇ»
6141 "},
6142 indoc! {"
6143 «1. This is a numbered list item that is
6144 very long and needs to be wrapped
6145 properly.
6146 2. This is a numbered list item that is
6147 very long and needs to be wrapped
6148 properly.
6149 - This is an unordered list item that is
6150 also very long and should not merge
6151 with the numbered item.ˇ»
6152 "},
6153 markdown_language,
6154 &mut cx,
6155 );
6156
6157 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6158 assert_rewrap(
6159 indoc! {"
6160 ˇThis is a very long line of plain text that will be wrapped.
6161 "},
6162 indoc! {"
6163 ˇThis is a very long line of plain text
6164 that will be wrapped.
6165 "},
6166 plaintext_language.clone(),
6167 &mut cx,
6168 );
6169
6170 // Test that non-commented code acts as a paragraph boundary within a selection
6171 assert_rewrap(
6172 indoc! {"
6173 «// This is the first long comment block to be wrapped.
6174 fn my_func(a: u32);
6175 // This is the second long comment block to be wrapped.ˇ»
6176 "},
6177 indoc! {"
6178 «// This is the first long comment block
6179 // to be wrapped.
6180 fn my_func(a: u32);
6181 // This is the second long comment block
6182 // to be wrapped.ˇ»
6183 "},
6184 rust_language,
6185 &mut cx,
6186 );
6187
6188 // Test rewrapping multiple selections, including ones with blank lines or tabs
6189 assert_rewrap(
6190 indoc! {"
6191 «ˇThis is a very long line that will be wrapped.
6192
6193 This is another paragraph in the same selection.»
6194
6195 «\tThis is a very long indented line that will be wrapped.ˇ»
6196 "},
6197 indoc! {"
6198 «ˇThis is a very long line that will be
6199 wrapped.
6200
6201 This is another paragraph in the same
6202 selection.»
6203
6204 «\tThis is a very long indented line
6205 \tthat will be wrapped.ˇ»
6206 "},
6207 plaintext_language,
6208 &mut cx,
6209 );
6210
6211 // Test that an empty comment line acts as a paragraph boundary
6212 assert_rewrap(
6213 indoc! {"
6214 // ˇThis is a long comment that will be wrapped.
6215 //
6216 // And this is another long comment that will also be wrapped.ˇ
6217 "},
6218 indoc! {"
6219 // ˇThis is a long comment that will be
6220 // wrapped.
6221 //
6222 // And this is another long comment that
6223 // will also be wrapped.ˇ
6224 "},
6225 cpp_language,
6226 &mut cx,
6227 );
6228
6229 #[track_caller]
6230 fn assert_rewrap(
6231 unwrapped_text: &str,
6232 wrapped_text: &str,
6233 language: Arc<Language>,
6234 cx: &mut EditorTestContext,
6235 ) {
6236 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6237 cx.set_state(unwrapped_text);
6238 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6239 cx.assert_editor_state(wrapped_text);
6240 }
6241}
6242
6243#[gpui::test]
6244async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6245 init_test(cx, |settings| {
6246 settings.languages.0.extend([(
6247 "Rust".into(),
6248 LanguageSettingsContent {
6249 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6250 preferred_line_length: Some(40),
6251 ..Default::default()
6252 },
6253 )])
6254 });
6255
6256 let mut cx = EditorTestContext::new(cx).await;
6257
6258 let rust_lang = Arc::new(
6259 Language::new(
6260 LanguageConfig {
6261 name: "Rust".into(),
6262 line_comments: vec!["// ".into()],
6263 block_comment: Some(BlockCommentConfig {
6264 start: "/*".into(),
6265 end: "*/".into(),
6266 prefix: "* ".into(),
6267 tab_size: 1,
6268 }),
6269 documentation_comment: Some(BlockCommentConfig {
6270 start: "/**".into(),
6271 end: "*/".into(),
6272 prefix: "* ".into(),
6273 tab_size: 1,
6274 }),
6275
6276 ..LanguageConfig::default()
6277 },
6278 Some(tree_sitter_rust::LANGUAGE.into()),
6279 )
6280 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6281 .unwrap(),
6282 );
6283
6284 // regular block comment
6285 assert_rewrap(
6286 indoc! {"
6287 /*
6288 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6289 */
6290 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6291 "},
6292 indoc! {"
6293 /*
6294 *ˇ Lorem ipsum dolor sit amet,
6295 * consectetur adipiscing elit.
6296 */
6297 /*
6298 *ˇ Lorem ipsum dolor sit amet,
6299 * consectetur adipiscing elit.
6300 */
6301 "},
6302 rust_lang.clone(),
6303 &mut cx,
6304 );
6305
6306 // indent is respected
6307 assert_rewrap(
6308 indoc! {"
6309 {}
6310 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6311 "},
6312 indoc! {"
6313 {}
6314 /*
6315 *ˇ Lorem ipsum dolor sit amet,
6316 * consectetur adipiscing elit.
6317 */
6318 "},
6319 rust_lang.clone(),
6320 &mut cx,
6321 );
6322
6323 // short block comments with inline delimiters
6324 assert_rewrap(
6325 indoc! {"
6326 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6327 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6328 */
6329 /*
6330 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6331 "},
6332 indoc! {"
6333 /*
6334 *ˇ Lorem ipsum dolor sit amet,
6335 * consectetur adipiscing elit.
6336 */
6337 /*
6338 *ˇ Lorem ipsum dolor sit amet,
6339 * consectetur adipiscing elit.
6340 */
6341 /*
6342 *ˇ Lorem ipsum dolor sit amet,
6343 * consectetur adipiscing elit.
6344 */
6345 "},
6346 rust_lang.clone(),
6347 &mut cx,
6348 );
6349
6350 // multiline block comment with inline start/end delimiters
6351 assert_rewrap(
6352 indoc! {"
6353 /*ˇ Lorem ipsum dolor sit amet,
6354 * consectetur adipiscing elit. */
6355 "},
6356 indoc! {"
6357 /*
6358 *ˇ Lorem ipsum dolor sit amet,
6359 * consectetur adipiscing elit.
6360 */
6361 "},
6362 rust_lang.clone(),
6363 &mut cx,
6364 );
6365
6366 // block comment rewrap still respects paragraph bounds
6367 assert_rewrap(
6368 indoc! {"
6369 /*
6370 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6371 *
6372 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6373 */
6374 "},
6375 indoc! {"
6376 /*
6377 *ˇ Lorem ipsum dolor sit amet,
6378 * consectetur adipiscing elit.
6379 *
6380 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6381 */
6382 "},
6383 rust_lang.clone(),
6384 &mut cx,
6385 );
6386
6387 // documentation comments
6388 assert_rewrap(
6389 indoc! {"
6390 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6391 /**
6392 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6393 */
6394 "},
6395 indoc! {"
6396 /**
6397 *ˇ Lorem ipsum dolor sit amet,
6398 * consectetur adipiscing elit.
6399 */
6400 /**
6401 *ˇ Lorem ipsum dolor sit amet,
6402 * consectetur adipiscing elit.
6403 */
6404 "},
6405 rust_lang.clone(),
6406 &mut cx,
6407 );
6408
6409 // different, adjacent comments
6410 assert_rewrap(
6411 indoc! {"
6412 /**
6413 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6414 */
6415 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6416 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6417 "},
6418 indoc! {"
6419 /**
6420 *ˇ Lorem ipsum dolor sit amet,
6421 * consectetur adipiscing elit.
6422 */
6423 /*
6424 *ˇ Lorem ipsum dolor sit amet,
6425 * consectetur adipiscing elit.
6426 */
6427 //ˇ Lorem ipsum dolor sit amet,
6428 // consectetur adipiscing elit.
6429 "},
6430 rust_lang.clone(),
6431 &mut cx,
6432 );
6433
6434 // selection w/ single short block comment
6435 assert_rewrap(
6436 indoc! {"
6437 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6438 "},
6439 indoc! {"
6440 «/*
6441 * Lorem ipsum dolor sit amet,
6442 * consectetur adipiscing elit.
6443 */ˇ»
6444 "},
6445 rust_lang.clone(),
6446 &mut cx,
6447 );
6448
6449 // rewrapping a single comment w/ abutting comments
6450 assert_rewrap(
6451 indoc! {"
6452 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6453 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6454 "},
6455 indoc! {"
6456 /*
6457 * ˇLorem ipsum dolor sit amet,
6458 * consectetur adipiscing elit.
6459 */
6460 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6461 "},
6462 rust_lang.clone(),
6463 &mut cx,
6464 );
6465
6466 // selection w/ non-abutting short block comments
6467 assert_rewrap(
6468 indoc! {"
6469 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6470
6471 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6472 "},
6473 indoc! {"
6474 «/*
6475 * Lorem ipsum dolor sit amet,
6476 * consectetur adipiscing elit.
6477 */
6478
6479 /*
6480 * Lorem ipsum dolor sit amet,
6481 * consectetur adipiscing elit.
6482 */ˇ»
6483 "},
6484 rust_lang.clone(),
6485 &mut cx,
6486 );
6487
6488 // selection of multiline block comments
6489 assert_rewrap(
6490 indoc! {"
6491 «/* Lorem ipsum dolor sit amet,
6492 * consectetur adipiscing elit. */ˇ»
6493 "},
6494 indoc! {"
6495 «/*
6496 * Lorem ipsum dolor sit amet,
6497 * consectetur adipiscing elit.
6498 */ˇ»
6499 "},
6500 rust_lang.clone(),
6501 &mut cx,
6502 );
6503
6504 // partial selection of multiline block comments
6505 assert_rewrap(
6506 indoc! {"
6507 «/* Lorem ipsum dolor sit amet,ˇ»
6508 * consectetur adipiscing elit. */
6509 /* Lorem ipsum dolor sit amet,
6510 «* consectetur adipiscing elit. */ˇ»
6511 "},
6512 indoc! {"
6513 «/*
6514 * Lorem ipsum dolor sit amet,ˇ»
6515 * consectetur adipiscing elit. */
6516 /* Lorem ipsum dolor sit amet,
6517 «* consectetur adipiscing elit.
6518 */ˇ»
6519 "},
6520 rust_lang.clone(),
6521 &mut cx,
6522 );
6523
6524 // selection w/ abutting short block comments
6525 // TODO: should not be combined; should rewrap as 2 comments
6526 assert_rewrap(
6527 indoc! {"
6528 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6529 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6530 "},
6531 // desired behavior:
6532 // indoc! {"
6533 // «/*
6534 // * Lorem ipsum dolor sit amet,
6535 // * consectetur adipiscing elit.
6536 // */
6537 // /*
6538 // * Lorem ipsum dolor sit amet,
6539 // * consectetur adipiscing elit.
6540 // */ˇ»
6541 // "},
6542 // actual behaviour:
6543 indoc! {"
6544 «/*
6545 * Lorem ipsum dolor sit amet,
6546 * consectetur adipiscing elit. Lorem
6547 * ipsum dolor sit amet, consectetur
6548 * adipiscing elit.
6549 */ˇ»
6550 "},
6551 rust_lang.clone(),
6552 &mut cx,
6553 );
6554
6555 // TODO: same as above, but with delimiters on separate line
6556 // assert_rewrap(
6557 // indoc! {"
6558 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6559 // */
6560 // /*
6561 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6562 // "},
6563 // // desired:
6564 // // indoc! {"
6565 // // «/*
6566 // // * Lorem ipsum dolor sit amet,
6567 // // * consectetur adipiscing elit.
6568 // // */
6569 // // /*
6570 // // * Lorem ipsum dolor sit amet,
6571 // // * consectetur adipiscing elit.
6572 // // */ˇ»
6573 // // "},
6574 // // actual: (but with trailing w/s on the empty lines)
6575 // indoc! {"
6576 // «/*
6577 // * Lorem ipsum dolor sit amet,
6578 // * consectetur adipiscing elit.
6579 // *
6580 // */
6581 // /*
6582 // *
6583 // * Lorem ipsum dolor sit amet,
6584 // * consectetur adipiscing elit.
6585 // */ˇ»
6586 // "},
6587 // rust_lang.clone(),
6588 // &mut cx,
6589 // );
6590
6591 // TODO these are unhandled edge cases; not correct, just documenting known issues
6592 assert_rewrap(
6593 indoc! {"
6594 /*
6595 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6596 */
6597 /*
6598 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6599 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6600 "},
6601 // desired:
6602 // indoc! {"
6603 // /*
6604 // *ˇ Lorem ipsum dolor sit amet,
6605 // * consectetur adipiscing elit.
6606 // */
6607 // /*
6608 // *ˇ Lorem ipsum dolor sit amet,
6609 // * consectetur adipiscing elit.
6610 // */
6611 // /*
6612 // *ˇ Lorem ipsum dolor sit amet
6613 // */ /* consectetur adipiscing elit. */
6614 // "},
6615 // actual:
6616 indoc! {"
6617 /*
6618 //ˇ Lorem ipsum dolor sit amet,
6619 // consectetur adipiscing elit.
6620 */
6621 /*
6622 * //ˇ Lorem ipsum dolor sit amet,
6623 * consectetur adipiscing elit.
6624 */
6625 /*
6626 *ˇ Lorem ipsum dolor sit amet */ /*
6627 * consectetur adipiscing elit.
6628 */
6629 "},
6630 rust_lang,
6631 &mut cx,
6632 );
6633
6634 #[track_caller]
6635 fn assert_rewrap(
6636 unwrapped_text: &str,
6637 wrapped_text: &str,
6638 language: Arc<Language>,
6639 cx: &mut EditorTestContext,
6640 ) {
6641 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6642 cx.set_state(unwrapped_text);
6643 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6644 cx.assert_editor_state(wrapped_text);
6645 }
6646}
6647
6648#[gpui::test]
6649async fn test_hard_wrap(cx: &mut TestAppContext) {
6650 init_test(cx, |_| {});
6651 let mut cx = EditorTestContext::new(cx).await;
6652
6653 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6654 cx.update_editor(|editor, _, cx| {
6655 editor.set_hard_wrap(Some(14), cx);
6656 });
6657
6658 cx.set_state(indoc!(
6659 "
6660 one two three ˇ
6661 "
6662 ));
6663 cx.simulate_input("four");
6664 cx.run_until_parked();
6665
6666 cx.assert_editor_state(indoc!(
6667 "
6668 one two three
6669 fourˇ
6670 "
6671 ));
6672
6673 cx.update_editor(|editor, window, cx| {
6674 editor.newline(&Default::default(), window, cx);
6675 });
6676 cx.run_until_parked();
6677 cx.assert_editor_state(indoc!(
6678 "
6679 one two three
6680 four
6681 ˇ
6682 "
6683 ));
6684
6685 cx.simulate_input("five");
6686 cx.run_until_parked();
6687 cx.assert_editor_state(indoc!(
6688 "
6689 one two three
6690 four
6691 fiveˇ
6692 "
6693 ));
6694
6695 cx.update_editor(|editor, window, cx| {
6696 editor.newline(&Default::default(), window, cx);
6697 });
6698 cx.run_until_parked();
6699 cx.simulate_input("# ");
6700 cx.run_until_parked();
6701 cx.assert_editor_state(indoc!(
6702 "
6703 one two three
6704 four
6705 five
6706 # ˇ
6707 "
6708 ));
6709
6710 cx.update_editor(|editor, window, cx| {
6711 editor.newline(&Default::default(), window, cx);
6712 });
6713 cx.run_until_parked();
6714 cx.assert_editor_state(indoc!(
6715 "
6716 one two three
6717 four
6718 five
6719 #\x20
6720 #ˇ
6721 "
6722 ));
6723
6724 cx.simulate_input(" 6");
6725 cx.run_until_parked();
6726 cx.assert_editor_state(indoc!(
6727 "
6728 one two three
6729 four
6730 five
6731 #
6732 # 6ˇ
6733 "
6734 ));
6735}
6736
6737#[gpui::test]
6738async fn test_cut_line_ends(cx: &mut TestAppContext) {
6739 init_test(cx, |_| {});
6740
6741 let mut cx = EditorTestContext::new(cx).await;
6742
6743 cx.set_state(indoc! {"
6744 The quick« brownˇ»
6745 fox jumps overˇ
6746 the lazy dog"});
6747 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6748 cx.assert_editor_state(indoc! {"
6749 The quickˇ
6750 ˇthe lazy dog"});
6751
6752 cx.set_state(indoc! {"
6753 The quick« brownˇ»
6754 fox jumps overˇ
6755 the lazy dog"});
6756 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6757 cx.assert_editor_state(indoc! {"
6758 The quickˇ
6759 fox jumps overˇthe lazy dog"});
6760
6761 cx.set_state(indoc! {"
6762 The quick« brownˇ»
6763 fox jumps overˇ
6764 the lazy dog"});
6765 cx.update_editor(|e, window, cx| {
6766 e.cut_to_end_of_line(
6767 &CutToEndOfLine {
6768 stop_at_newlines: true,
6769 },
6770 window,
6771 cx,
6772 )
6773 });
6774 cx.assert_editor_state(indoc! {"
6775 The quickˇ
6776 fox jumps overˇ
6777 the lazy dog"});
6778
6779 cx.set_state(indoc! {"
6780 The quick« brownˇ»
6781 fox jumps overˇ
6782 the lazy dog"});
6783 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6784 cx.assert_editor_state(indoc! {"
6785 The quickˇ
6786 fox jumps overˇthe lazy dog"});
6787}
6788
6789#[gpui::test]
6790async fn test_clipboard(cx: &mut TestAppContext) {
6791 init_test(cx, |_| {});
6792
6793 let mut cx = EditorTestContext::new(cx).await;
6794
6795 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6796 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6797 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6798
6799 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6800 cx.set_state("two ˇfour ˇsix ˇ");
6801 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6802 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6803
6804 // Paste again but with only two cursors. Since the number of cursors doesn't
6805 // match the number of slices in the clipboard, the entire clipboard text
6806 // is pasted at each cursor.
6807 cx.set_state("ˇtwo one✅ four three six five ˇ");
6808 cx.update_editor(|e, window, cx| {
6809 e.handle_input("( ", window, cx);
6810 e.paste(&Paste, window, cx);
6811 e.handle_input(") ", window, cx);
6812 });
6813 cx.assert_editor_state(
6814 &([
6815 "( one✅ ",
6816 "three ",
6817 "five ) ˇtwo one✅ four three six five ( one✅ ",
6818 "three ",
6819 "five ) ˇ",
6820 ]
6821 .join("\n")),
6822 );
6823
6824 // Cut with three selections, one of which is full-line.
6825 cx.set_state(indoc! {"
6826 1«2ˇ»3
6827 4ˇ567
6828 «8ˇ»9"});
6829 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6830 cx.assert_editor_state(indoc! {"
6831 1ˇ3
6832 ˇ9"});
6833
6834 // Paste with three selections, noticing how the copied selection that was full-line
6835 // gets inserted before the second cursor.
6836 cx.set_state(indoc! {"
6837 1ˇ3
6838 9ˇ
6839 «oˇ»ne"});
6840 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6841 cx.assert_editor_state(indoc! {"
6842 12ˇ3
6843 4567
6844 9ˇ
6845 8ˇne"});
6846
6847 // Copy with a single cursor only, which writes the whole line into the clipboard.
6848 cx.set_state(indoc! {"
6849 The quick brown
6850 fox juˇmps over
6851 the lazy dog"});
6852 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6853 assert_eq!(
6854 cx.read_from_clipboard()
6855 .and_then(|item| item.text().as_deref().map(str::to_string)),
6856 Some("fox jumps over\n".to_string())
6857 );
6858
6859 // Paste with three selections, noticing how the copied full-line selection is inserted
6860 // before the empty selections but replaces the selection that is non-empty.
6861 cx.set_state(indoc! {"
6862 Tˇhe quick brown
6863 «foˇ»x jumps over
6864 tˇhe lazy dog"});
6865 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6866 cx.assert_editor_state(indoc! {"
6867 fox jumps over
6868 Tˇhe quick brown
6869 fox jumps over
6870 ˇx jumps over
6871 fox jumps over
6872 tˇhe lazy dog"});
6873}
6874
6875#[gpui::test]
6876async fn test_copy_trim(cx: &mut TestAppContext) {
6877 init_test(cx, |_| {});
6878
6879 let mut cx = EditorTestContext::new(cx).await;
6880 cx.set_state(
6881 r#" «for selection in selections.iter() {
6882 let mut start = selection.start;
6883 let mut end = selection.end;
6884 let is_entire_line = selection.is_empty();
6885 if is_entire_line {
6886 start = Point::new(start.row, 0);ˇ»
6887 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6888 }
6889 "#,
6890 );
6891 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6892 assert_eq!(
6893 cx.read_from_clipboard()
6894 .and_then(|item| item.text().as_deref().map(str::to_string)),
6895 Some(
6896 "for selection in selections.iter() {
6897 let mut start = selection.start;
6898 let mut end = selection.end;
6899 let is_entire_line = selection.is_empty();
6900 if is_entire_line {
6901 start = Point::new(start.row, 0);"
6902 .to_string()
6903 ),
6904 "Regular copying preserves all indentation selected",
6905 );
6906 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6907 assert_eq!(
6908 cx.read_from_clipboard()
6909 .and_then(|item| item.text().as_deref().map(str::to_string)),
6910 Some(
6911 "for selection in selections.iter() {
6912let mut start = selection.start;
6913let mut end = selection.end;
6914let is_entire_line = selection.is_empty();
6915if is_entire_line {
6916 start = Point::new(start.row, 0);"
6917 .to_string()
6918 ),
6919 "Copying with stripping should strip all leading whitespaces"
6920 );
6921
6922 cx.set_state(
6923 r#" « for selection in selections.iter() {
6924 let mut start = selection.start;
6925 let mut end = selection.end;
6926 let is_entire_line = selection.is_empty();
6927 if is_entire_line {
6928 start = Point::new(start.row, 0);ˇ»
6929 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6930 }
6931 "#,
6932 );
6933 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6934 assert_eq!(
6935 cx.read_from_clipboard()
6936 .and_then(|item| item.text().as_deref().map(str::to_string)),
6937 Some(
6938 " for selection in selections.iter() {
6939 let mut start = selection.start;
6940 let mut end = selection.end;
6941 let is_entire_line = selection.is_empty();
6942 if is_entire_line {
6943 start = Point::new(start.row, 0);"
6944 .to_string()
6945 ),
6946 "Regular copying preserves all indentation selected",
6947 );
6948 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6949 assert_eq!(
6950 cx.read_from_clipboard()
6951 .and_then(|item| item.text().as_deref().map(str::to_string)),
6952 Some(
6953 "for selection in selections.iter() {
6954let mut start = selection.start;
6955let mut end = selection.end;
6956let is_entire_line = selection.is_empty();
6957if is_entire_line {
6958 start = Point::new(start.row, 0);"
6959 .to_string()
6960 ),
6961 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6962 );
6963
6964 cx.set_state(
6965 r#" «ˇ for selection in selections.iter() {
6966 let mut start = selection.start;
6967 let mut end = selection.end;
6968 let is_entire_line = selection.is_empty();
6969 if is_entire_line {
6970 start = Point::new(start.row, 0);»
6971 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6972 }
6973 "#,
6974 );
6975 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6976 assert_eq!(
6977 cx.read_from_clipboard()
6978 .and_then(|item| item.text().as_deref().map(str::to_string)),
6979 Some(
6980 " for selection in selections.iter() {
6981 let mut start = selection.start;
6982 let mut end = selection.end;
6983 let is_entire_line = selection.is_empty();
6984 if is_entire_line {
6985 start = Point::new(start.row, 0);"
6986 .to_string()
6987 ),
6988 "Regular copying for reverse selection works the same",
6989 );
6990 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6991 assert_eq!(
6992 cx.read_from_clipboard()
6993 .and_then(|item| item.text().as_deref().map(str::to_string)),
6994 Some(
6995 "for selection in selections.iter() {
6996let mut start = selection.start;
6997let mut end = selection.end;
6998let is_entire_line = selection.is_empty();
6999if is_entire_line {
7000 start = Point::new(start.row, 0);"
7001 .to_string()
7002 ),
7003 "Copying with stripping for reverse selection works the same"
7004 );
7005
7006 cx.set_state(
7007 r#" for selection «in selections.iter() {
7008 let mut start = selection.start;
7009 let mut end = selection.end;
7010 let is_entire_line = selection.is_empty();
7011 if is_entire_line {
7012 start = Point::new(start.row, 0);ˇ»
7013 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7014 }
7015 "#,
7016 );
7017 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7018 assert_eq!(
7019 cx.read_from_clipboard()
7020 .and_then(|item| item.text().as_deref().map(str::to_string)),
7021 Some(
7022 "in selections.iter() {
7023 let mut start = selection.start;
7024 let mut end = selection.end;
7025 let is_entire_line = selection.is_empty();
7026 if is_entire_line {
7027 start = Point::new(start.row, 0);"
7028 .to_string()
7029 ),
7030 "When selecting past the indent, the copying works as usual",
7031 );
7032 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7033 assert_eq!(
7034 cx.read_from_clipboard()
7035 .and_then(|item| item.text().as_deref().map(str::to_string)),
7036 Some(
7037 "in selections.iter() {
7038 let mut start = selection.start;
7039 let mut end = selection.end;
7040 let is_entire_line = selection.is_empty();
7041 if is_entire_line {
7042 start = Point::new(start.row, 0);"
7043 .to_string()
7044 ),
7045 "When selecting past the indent, nothing is trimmed"
7046 );
7047
7048 cx.set_state(
7049 r#" «for selection in selections.iter() {
7050 let mut start = selection.start;
7051
7052 let mut end = selection.end;
7053 let is_entire_line = selection.is_empty();
7054 if is_entire_line {
7055 start = Point::new(start.row, 0);
7056ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7057 }
7058 "#,
7059 );
7060 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7061 assert_eq!(
7062 cx.read_from_clipboard()
7063 .and_then(|item| item.text().as_deref().map(str::to_string)),
7064 Some(
7065 "for selection in selections.iter() {
7066let mut start = selection.start;
7067
7068let mut end = selection.end;
7069let is_entire_line = selection.is_empty();
7070if is_entire_line {
7071 start = Point::new(start.row, 0);
7072"
7073 .to_string()
7074 ),
7075 "Copying with stripping should ignore empty lines"
7076 );
7077}
7078
7079#[gpui::test]
7080async fn test_paste_multiline(cx: &mut TestAppContext) {
7081 init_test(cx, |_| {});
7082
7083 let mut cx = EditorTestContext::new(cx).await;
7084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7085
7086 // Cut an indented block, without the leading whitespace.
7087 cx.set_state(indoc! {"
7088 const a: B = (
7089 c(),
7090 «d(
7091 e,
7092 f
7093 )ˇ»
7094 );
7095 "});
7096 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7097 cx.assert_editor_state(indoc! {"
7098 const a: B = (
7099 c(),
7100 ˇ
7101 );
7102 "});
7103
7104 // Paste it at the same position.
7105 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7106 cx.assert_editor_state(indoc! {"
7107 const a: B = (
7108 c(),
7109 d(
7110 e,
7111 f
7112 )ˇ
7113 );
7114 "});
7115
7116 // Paste it at a line with a lower indent level.
7117 cx.set_state(indoc! {"
7118 ˇ
7119 const a: B = (
7120 c(),
7121 );
7122 "});
7123 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7124 cx.assert_editor_state(indoc! {"
7125 d(
7126 e,
7127 f
7128 )ˇ
7129 const a: B = (
7130 c(),
7131 );
7132 "});
7133
7134 // Cut an indented block, with the leading whitespace.
7135 cx.set_state(indoc! {"
7136 const a: B = (
7137 c(),
7138 « d(
7139 e,
7140 f
7141 )
7142 ˇ»);
7143 "});
7144 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7145 cx.assert_editor_state(indoc! {"
7146 const a: B = (
7147 c(),
7148 ˇ);
7149 "});
7150
7151 // Paste it at the same position.
7152 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7153 cx.assert_editor_state(indoc! {"
7154 const a: B = (
7155 c(),
7156 d(
7157 e,
7158 f
7159 )
7160 ˇ);
7161 "});
7162
7163 // Paste it at a line with a higher indent level.
7164 cx.set_state(indoc! {"
7165 const a: B = (
7166 c(),
7167 d(
7168 e,
7169 fˇ
7170 )
7171 );
7172 "});
7173 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7174 cx.assert_editor_state(indoc! {"
7175 const a: B = (
7176 c(),
7177 d(
7178 e,
7179 f d(
7180 e,
7181 f
7182 )
7183 ˇ
7184 )
7185 );
7186 "});
7187
7188 // Copy an indented block, starting mid-line
7189 cx.set_state(indoc! {"
7190 const a: B = (
7191 c(),
7192 somethin«g(
7193 e,
7194 f
7195 )ˇ»
7196 );
7197 "});
7198 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7199
7200 // Paste it on a line with a lower indent level
7201 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7202 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7203 cx.assert_editor_state(indoc! {"
7204 const a: B = (
7205 c(),
7206 something(
7207 e,
7208 f
7209 )
7210 );
7211 g(
7212 e,
7213 f
7214 )ˇ"});
7215}
7216
7217#[gpui::test]
7218async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7219 init_test(cx, |_| {});
7220
7221 cx.write_to_clipboard(ClipboardItem::new_string(
7222 " d(\n e\n );\n".into(),
7223 ));
7224
7225 let mut cx = EditorTestContext::new(cx).await;
7226 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7227
7228 cx.set_state(indoc! {"
7229 fn a() {
7230 b();
7231 if c() {
7232 ˇ
7233 }
7234 }
7235 "});
7236
7237 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7238 cx.assert_editor_state(indoc! {"
7239 fn a() {
7240 b();
7241 if c() {
7242 d(
7243 e
7244 );
7245 ˇ
7246 }
7247 }
7248 "});
7249
7250 cx.set_state(indoc! {"
7251 fn a() {
7252 b();
7253 ˇ
7254 }
7255 "});
7256
7257 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7258 cx.assert_editor_state(indoc! {"
7259 fn a() {
7260 b();
7261 d(
7262 e
7263 );
7264 ˇ
7265 }
7266 "});
7267}
7268
7269#[gpui::test]
7270fn test_select_all(cx: &mut TestAppContext) {
7271 init_test(cx, |_| {});
7272
7273 let editor = cx.add_window(|window, cx| {
7274 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7275 build_editor(buffer, window, cx)
7276 });
7277 _ = editor.update(cx, |editor, window, cx| {
7278 editor.select_all(&SelectAll, window, cx);
7279 assert_eq!(
7280 editor.selections.display_ranges(cx),
7281 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7282 );
7283 });
7284}
7285
7286#[gpui::test]
7287fn test_select_line(cx: &mut TestAppContext) {
7288 init_test(cx, |_| {});
7289
7290 let editor = cx.add_window(|window, cx| {
7291 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7292 build_editor(buffer, window, cx)
7293 });
7294 _ = editor.update(cx, |editor, window, cx| {
7295 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7296 s.select_display_ranges([
7297 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7298 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7299 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7300 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7301 ])
7302 });
7303 editor.select_line(&SelectLine, window, cx);
7304 assert_eq!(
7305 editor.selections.display_ranges(cx),
7306 vec![
7307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7308 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7309 ]
7310 );
7311 });
7312
7313 _ = editor.update(cx, |editor, window, cx| {
7314 editor.select_line(&SelectLine, window, cx);
7315 assert_eq!(
7316 editor.selections.display_ranges(cx),
7317 vec![
7318 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7319 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7320 ]
7321 );
7322 });
7323
7324 _ = editor.update(cx, |editor, window, cx| {
7325 editor.select_line(&SelectLine, window, cx);
7326 assert_eq!(
7327 editor.selections.display_ranges(cx),
7328 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7329 );
7330 });
7331}
7332
7333#[gpui::test]
7334async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7335 init_test(cx, |_| {});
7336 let mut cx = EditorTestContext::new(cx).await;
7337
7338 #[track_caller]
7339 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7340 cx.set_state(initial_state);
7341 cx.update_editor(|e, window, cx| {
7342 e.split_selection_into_lines(&Default::default(), window, cx)
7343 });
7344 cx.assert_editor_state(expected_state);
7345 }
7346
7347 // Selection starts and ends at the middle of lines, left-to-right
7348 test(
7349 &mut cx,
7350 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7351 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7352 );
7353 // Same thing, right-to-left
7354 test(
7355 &mut cx,
7356 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7357 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7358 );
7359
7360 // Whole buffer, left-to-right, last line *doesn't* end with newline
7361 test(
7362 &mut cx,
7363 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7364 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7365 );
7366 // Same thing, right-to-left
7367 test(
7368 &mut cx,
7369 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7370 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7371 );
7372
7373 // Whole buffer, left-to-right, last line ends with newline
7374 test(
7375 &mut cx,
7376 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7377 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7378 );
7379 // Same thing, right-to-left
7380 test(
7381 &mut cx,
7382 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7383 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7384 );
7385
7386 // Starts at the end of a line, ends at the start of another
7387 test(
7388 &mut cx,
7389 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7390 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7391 );
7392}
7393
7394#[gpui::test]
7395async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7396 init_test(cx, |_| {});
7397
7398 let editor = cx.add_window(|window, cx| {
7399 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7400 build_editor(buffer, window, cx)
7401 });
7402
7403 // setup
7404 _ = editor.update(cx, |editor, window, cx| {
7405 editor.fold_creases(
7406 vec![
7407 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7408 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7409 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7410 ],
7411 true,
7412 window,
7413 cx,
7414 );
7415 assert_eq!(
7416 editor.display_text(cx),
7417 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7418 );
7419 });
7420
7421 _ = editor.update(cx, |editor, window, cx| {
7422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7423 s.select_display_ranges([
7424 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7425 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7426 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7427 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7428 ])
7429 });
7430 editor.split_selection_into_lines(&Default::default(), window, cx);
7431 assert_eq!(
7432 editor.display_text(cx),
7433 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7434 );
7435 });
7436 EditorTestContext::for_editor(editor, cx)
7437 .await
7438 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7439
7440 _ = editor.update(cx, |editor, window, cx| {
7441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7442 s.select_display_ranges([
7443 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7444 ])
7445 });
7446 editor.split_selection_into_lines(&Default::default(), window, cx);
7447 assert_eq!(
7448 editor.display_text(cx),
7449 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7450 );
7451 assert_eq!(
7452 editor.selections.display_ranges(cx),
7453 [
7454 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7455 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7456 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7457 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7458 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7459 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7460 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7461 ]
7462 );
7463 });
7464 EditorTestContext::for_editor(editor, cx)
7465 .await
7466 .assert_editor_state(
7467 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7468 );
7469}
7470
7471#[gpui::test]
7472async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7473 init_test(cx, |_| {});
7474
7475 let mut cx = EditorTestContext::new(cx).await;
7476
7477 cx.set_state(indoc!(
7478 r#"abc
7479 defˇghi
7480
7481 jk
7482 nlmo
7483 "#
7484 ));
7485
7486 cx.update_editor(|editor, window, cx| {
7487 editor.add_selection_above(&Default::default(), window, cx);
7488 });
7489
7490 cx.assert_editor_state(indoc!(
7491 r#"abcˇ
7492 defˇghi
7493
7494 jk
7495 nlmo
7496 "#
7497 ));
7498
7499 cx.update_editor(|editor, window, cx| {
7500 editor.add_selection_above(&Default::default(), window, cx);
7501 });
7502
7503 cx.assert_editor_state(indoc!(
7504 r#"abcˇ
7505 defˇghi
7506
7507 jk
7508 nlmo
7509 "#
7510 ));
7511
7512 cx.update_editor(|editor, window, cx| {
7513 editor.add_selection_below(&Default::default(), window, cx);
7514 });
7515
7516 cx.assert_editor_state(indoc!(
7517 r#"abc
7518 defˇghi
7519
7520 jk
7521 nlmo
7522 "#
7523 ));
7524
7525 cx.update_editor(|editor, window, cx| {
7526 editor.undo_selection(&Default::default(), window, cx);
7527 });
7528
7529 cx.assert_editor_state(indoc!(
7530 r#"abcˇ
7531 defˇghi
7532
7533 jk
7534 nlmo
7535 "#
7536 ));
7537
7538 cx.update_editor(|editor, window, cx| {
7539 editor.redo_selection(&Default::default(), window, cx);
7540 });
7541
7542 cx.assert_editor_state(indoc!(
7543 r#"abc
7544 defˇghi
7545
7546 jk
7547 nlmo
7548 "#
7549 ));
7550
7551 cx.update_editor(|editor, window, cx| {
7552 editor.add_selection_below(&Default::default(), window, cx);
7553 });
7554
7555 cx.assert_editor_state(indoc!(
7556 r#"abc
7557 defˇghi
7558 ˇ
7559 jk
7560 nlmo
7561 "#
7562 ));
7563
7564 cx.update_editor(|editor, window, cx| {
7565 editor.add_selection_below(&Default::default(), window, cx);
7566 });
7567
7568 cx.assert_editor_state(indoc!(
7569 r#"abc
7570 defˇghi
7571 ˇ
7572 jkˇ
7573 nlmo
7574 "#
7575 ));
7576
7577 cx.update_editor(|editor, window, cx| {
7578 editor.add_selection_below(&Default::default(), window, cx);
7579 });
7580
7581 cx.assert_editor_state(indoc!(
7582 r#"abc
7583 defˇghi
7584 ˇ
7585 jkˇ
7586 nlmˇo
7587 "#
7588 ));
7589
7590 cx.update_editor(|editor, window, cx| {
7591 editor.add_selection_below(&Default::default(), window, cx);
7592 });
7593
7594 cx.assert_editor_state(indoc!(
7595 r#"abc
7596 defˇghi
7597 ˇ
7598 jkˇ
7599 nlmˇo
7600 ˇ"#
7601 ));
7602
7603 // change selections
7604 cx.set_state(indoc!(
7605 r#"abc
7606 def«ˇg»hi
7607
7608 jk
7609 nlmo
7610 "#
7611 ));
7612
7613 cx.update_editor(|editor, window, cx| {
7614 editor.add_selection_below(&Default::default(), window, cx);
7615 });
7616
7617 cx.assert_editor_state(indoc!(
7618 r#"abc
7619 def«ˇg»hi
7620
7621 jk
7622 nlm«ˇo»
7623 "#
7624 ));
7625
7626 cx.update_editor(|editor, window, cx| {
7627 editor.add_selection_below(&Default::default(), window, cx);
7628 });
7629
7630 cx.assert_editor_state(indoc!(
7631 r#"abc
7632 def«ˇg»hi
7633
7634 jk
7635 nlm«ˇo»
7636 "#
7637 ));
7638
7639 cx.update_editor(|editor, window, cx| {
7640 editor.add_selection_above(&Default::default(), window, cx);
7641 });
7642
7643 cx.assert_editor_state(indoc!(
7644 r#"abc
7645 def«ˇg»hi
7646
7647 jk
7648 nlmo
7649 "#
7650 ));
7651
7652 cx.update_editor(|editor, window, cx| {
7653 editor.add_selection_above(&Default::default(), window, cx);
7654 });
7655
7656 cx.assert_editor_state(indoc!(
7657 r#"abc
7658 def«ˇg»hi
7659
7660 jk
7661 nlmo
7662 "#
7663 ));
7664
7665 // Change selections again
7666 cx.set_state(indoc!(
7667 r#"a«bc
7668 defgˇ»hi
7669
7670 jk
7671 nlmo
7672 "#
7673 ));
7674
7675 cx.update_editor(|editor, window, cx| {
7676 editor.add_selection_below(&Default::default(), window, cx);
7677 });
7678
7679 cx.assert_editor_state(indoc!(
7680 r#"a«bcˇ»
7681 d«efgˇ»hi
7682
7683 j«kˇ»
7684 nlmo
7685 "#
7686 ));
7687
7688 cx.update_editor(|editor, window, cx| {
7689 editor.add_selection_below(&Default::default(), window, cx);
7690 });
7691 cx.assert_editor_state(indoc!(
7692 r#"a«bcˇ»
7693 d«efgˇ»hi
7694
7695 j«kˇ»
7696 n«lmoˇ»
7697 "#
7698 ));
7699 cx.update_editor(|editor, window, cx| {
7700 editor.add_selection_above(&Default::default(), window, cx);
7701 });
7702
7703 cx.assert_editor_state(indoc!(
7704 r#"a«bcˇ»
7705 d«efgˇ»hi
7706
7707 j«kˇ»
7708 nlmo
7709 "#
7710 ));
7711
7712 // Change selections again
7713 cx.set_state(indoc!(
7714 r#"abc
7715 d«ˇefghi
7716
7717 jk
7718 nlm»o
7719 "#
7720 ));
7721
7722 cx.update_editor(|editor, window, cx| {
7723 editor.add_selection_above(&Default::default(), window, cx);
7724 });
7725
7726 cx.assert_editor_state(indoc!(
7727 r#"a«ˇbc»
7728 d«ˇef»ghi
7729
7730 j«ˇk»
7731 n«ˇlm»o
7732 "#
7733 ));
7734
7735 cx.update_editor(|editor, window, cx| {
7736 editor.add_selection_below(&Default::default(), window, cx);
7737 });
7738
7739 cx.assert_editor_state(indoc!(
7740 r#"abc
7741 d«ˇef»ghi
7742
7743 j«ˇk»
7744 n«ˇlm»o
7745 "#
7746 ));
7747}
7748
7749#[gpui::test]
7750async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7751 init_test(cx, |_| {});
7752 let mut cx = EditorTestContext::new(cx).await;
7753
7754 cx.set_state(indoc!(
7755 r#"line onˇe
7756 liˇne two
7757 line three
7758 line four"#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_below(&Default::default(), window, cx);
7763 });
7764
7765 // test multiple cursors expand in the same direction
7766 cx.assert_editor_state(indoc!(
7767 r#"line onˇe
7768 liˇne twˇo
7769 liˇne three
7770 line four"#
7771 ));
7772
7773 cx.update_editor(|editor, window, cx| {
7774 editor.add_selection_below(&Default::default(), window, cx);
7775 });
7776
7777 cx.update_editor(|editor, window, cx| {
7778 editor.add_selection_below(&Default::default(), window, cx);
7779 });
7780
7781 // test multiple cursors expand below overflow
7782 cx.assert_editor_state(indoc!(
7783 r#"line onˇe
7784 liˇne twˇo
7785 liˇne thˇree
7786 liˇne foˇur"#
7787 ));
7788
7789 cx.update_editor(|editor, window, cx| {
7790 editor.add_selection_above(&Default::default(), window, cx);
7791 });
7792
7793 // test multiple cursors retrieves back correctly
7794 cx.assert_editor_state(indoc!(
7795 r#"line onˇe
7796 liˇne twˇo
7797 liˇne thˇree
7798 line four"#
7799 ));
7800
7801 cx.update_editor(|editor, window, cx| {
7802 editor.add_selection_above(&Default::default(), window, cx);
7803 });
7804
7805 cx.update_editor(|editor, window, cx| {
7806 editor.add_selection_above(&Default::default(), window, cx);
7807 });
7808
7809 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7810 cx.assert_editor_state(indoc!(
7811 r#"liˇne onˇe
7812 liˇne two
7813 line three
7814 line four"#
7815 ));
7816
7817 cx.update_editor(|editor, window, cx| {
7818 editor.undo_selection(&Default::default(), window, cx);
7819 });
7820
7821 // test undo
7822 cx.assert_editor_state(indoc!(
7823 r#"line onˇe
7824 liˇne twˇo
7825 line three
7826 line four"#
7827 ));
7828
7829 cx.update_editor(|editor, window, cx| {
7830 editor.redo_selection(&Default::default(), window, cx);
7831 });
7832
7833 // test redo
7834 cx.assert_editor_state(indoc!(
7835 r#"liˇne onˇe
7836 liˇne two
7837 line three
7838 line four"#
7839 ));
7840
7841 cx.set_state(indoc!(
7842 r#"abcd
7843 ef«ghˇ»
7844 ijkl
7845 «mˇ»nop"#
7846 ));
7847
7848 cx.update_editor(|editor, window, cx| {
7849 editor.add_selection_above(&Default::default(), window, cx);
7850 });
7851
7852 // test multiple selections expand in the same direction
7853 cx.assert_editor_state(indoc!(
7854 r#"ab«cdˇ»
7855 ef«ghˇ»
7856 «iˇ»jkl
7857 «mˇ»nop"#
7858 ));
7859
7860 cx.update_editor(|editor, window, cx| {
7861 editor.add_selection_above(&Default::default(), window, cx);
7862 });
7863
7864 // test multiple selection upward overflow
7865 cx.assert_editor_state(indoc!(
7866 r#"ab«cdˇ»
7867 «eˇ»f«ghˇ»
7868 «iˇ»jkl
7869 «mˇ»nop"#
7870 ));
7871
7872 cx.update_editor(|editor, window, cx| {
7873 editor.add_selection_below(&Default::default(), window, cx);
7874 });
7875
7876 // test multiple selection retrieves back correctly
7877 cx.assert_editor_state(indoc!(
7878 r#"abcd
7879 ef«ghˇ»
7880 «iˇ»jkl
7881 «mˇ»nop"#
7882 ));
7883
7884 cx.update_editor(|editor, window, cx| {
7885 editor.add_selection_below(&Default::default(), window, cx);
7886 });
7887
7888 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7889 cx.assert_editor_state(indoc!(
7890 r#"abcd
7891 ef«ghˇ»
7892 ij«klˇ»
7893 «mˇ»nop"#
7894 ));
7895
7896 cx.update_editor(|editor, window, cx| {
7897 editor.undo_selection(&Default::default(), window, cx);
7898 });
7899
7900 // test undo
7901 cx.assert_editor_state(indoc!(
7902 r#"abcd
7903 ef«ghˇ»
7904 «iˇ»jkl
7905 «mˇ»nop"#
7906 ));
7907
7908 cx.update_editor(|editor, window, cx| {
7909 editor.redo_selection(&Default::default(), window, cx);
7910 });
7911
7912 // test redo
7913 cx.assert_editor_state(indoc!(
7914 r#"abcd
7915 ef«ghˇ»
7916 ij«klˇ»
7917 «mˇ»nop"#
7918 ));
7919}
7920
7921#[gpui::test]
7922async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7923 init_test(cx, |_| {});
7924 let mut cx = EditorTestContext::new(cx).await;
7925
7926 cx.set_state(indoc!(
7927 r#"line onˇe
7928 liˇne two
7929 line three
7930 line four"#
7931 ));
7932
7933 cx.update_editor(|editor, window, cx| {
7934 editor.add_selection_below(&Default::default(), window, cx);
7935 editor.add_selection_below(&Default::default(), window, cx);
7936 editor.add_selection_below(&Default::default(), window, cx);
7937 });
7938
7939 // initial state with two multi cursor groups
7940 cx.assert_editor_state(indoc!(
7941 r#"line onˇe
7942 liˇne twˇo
7943 liˇne thˇree
7944 liˇne foˇur"#
7945 ));
7946
7947 // add single cursor in middle - simulate opt click
7948 cx.update_editor(|editor, window, cx| {
7949 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7950 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7951 editor.end_selection(window, cx);
7952 });
7953
7954 cx.assert_editor_state(indoc!(
7955 r#"line onˇe
7956 liˇne twˇo
7957 liˇneˇ thˇree
7958 liˇne foˇur"#
7959 ));
7960
7961 cx.update_editor(|editor, window, cx| {
7962 editor.add_selection_above(&Default::default(), window, cx);
7963 });
7964
7965 // test new added selection expands above and existing selection shrinks
7966 cx.assert_editor_state(indoc!(
7967 r#"line onˇe
7968 liˇneˇ twˇo
7969 liˇneˇ thˇree
7970 line four"#
7971 ));
7972
7973 cx.update_editor(|editor, window, cx| {
7974 editor.add_selection_above(&Default::default(), window, cx);
7975 });
7976
7977 // test new added selection expands above and existing selection shrinks
7978 cx.assert_editor_state(indoc!(
7979 r#"lineˇ onˇe
7980 liˇneˇ twˇo
7981 lineˇ three
7982 line four"#
7983 ));
7984
7985 // intial state with two selection groups
7986 cx.set_state(indoc!(
7987 r#"abcd
7988 ef«ghˇ»
7989 ijkl
7990 «mˇ»nop"#
7991 ));
7992
7993 cx.update_editor(|editor, window, cx| {
7994 editor.add_selection_above(&Default::default(), window, cx);
7995 editor.add_selection_above(&Default::default(), window, cx);
7996 });
7997
7998 cx.assert_editor_state(indoc!(
7999 r#"ab«cdˇ»
8000 «eˇ»f«ghˇ»
8001 «iˇ»jkl
8002 «mˇ»nop"#
8003 ));
8004
8005 // add single selection in middle - simulate opt drag
8006 cx.update_editor(|editor, window, cx| {
8007 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8008 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8009 editor.update_selection(
8010 DisplayPoint::new(DisplayRow(2), 4),
8011 0,
8012 gpui::Point::<f32>::default(),
8013 window,
8014 cx,
8015 );
8016 editor.end_selection(window, cx);
8017 });
8018
8019 cx.assert_editor_state(indoc!(
8020 r#"ab«cdˇ»
8021 «eˇ»f«ghˇ»
8022 «iˇ»jk«lˇ»
8023 «mˇ»nop"#
8024 ));
8025
8026 cx.update_editor(|editor, window, cx| {
8027 editor.add_selection_below(&Default::default(), window, cx);
8028 });
8029
8030 // test new added selection expands below, others shrinks from above
8031 cx.assert_editor_state(indoc!(
8032 r#"abcd
8033 ef«ghˇ»
8034 «iˇ»jk«lˇ»
8035 «mˇ»no«pˇ»"#
8036 ));
8037}
8038
8039#[gpui::test]
8040async fn test_select_next(cx: &mut TestAppContext) {
8041 init_test(cx, |_| {});
8042
8043 let mut cx = EditorTestContext::new(cx).await;
8044 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8045
8046 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8047 .unwrap();
8048 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8049
8050 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8051 .unwrap();
8052 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8053
8054 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8055 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8056
8057 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8058 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8059
8060 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8061 .unwrap();
8062 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8063
8064 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8065 .unwrap();
8066 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8067
8068 // Test selection direction should be preserved
8069 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8070
8071 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8072 .unwrap();
8073 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8074}
8075
8076#[gpui::test]
8077async fn test_select_all_matches(cx: &mut TestAppContext) {
8078 init_test(cx, |_| {});
8079
8080 let mut cx = EditorTestContext::new(cx).await;
8081
8082 // Test caret-only selections
8083 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8084 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8085 .unwrap();
8086 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8087
8088 // Test left-to-right selections
8089 cx.set_state("abc\n«abcˇ»\nabc");
8090 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8091 .unwrap();
8092 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8093
8094 // Test right-to-left selections
8095 cx.set_state("abc\n«ˇabc»\nabc");
8096 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8097 .unwrap();
8098 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8099
8100 // Test selecting whitespace with caret selection
8101 cx.set_state("abc\nˇ abc\nabc");
8102 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8103 .unwrap();
8104 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8105
8106 // Test selecting whitespace with left-to-right selection
8107 cx.set_state("abc\n«ˇ »abc\nabc");
8108 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8109 .unwrap();
8110 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8111
8112 // Test no matches with right-to-left selection
8113 cx.set_state("abc\n« ˇ»abc\nabc");
8114 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8115 .unwrap();
8116 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8117
8118 // Test with a single word and clip_at_line_ends=true (#29823)
8119 cx.set_state("aˇbc");
8120 cx.update_editor(|e, window, cx| {
8121 e.set_clip_at_line_ends(true, cx);
8122 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8123 e.set_clip_at_line_ends(false, cx);
8124 });
8125 cx.assert_editor_state("«abcˇ»");
8126}
8127
8128#[gpui::test]
8129async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8130 init_test(cx, |_| {});
8131
8132 let mut cx = EditorTestContext::new(cx).await;
8133
8134 let large_body_1 = "\nd".repeat(200);
8135 let large_body_2 = "\ne".repeat(200);
8136
8137 cx.set_state(&format!(
8138 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8139 ));
8140 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8141 let scroll_position = editor.scroll_position(cx);
8142 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8143 scroll_position
8144 });
8145
8146 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8147 .unwrap();
8148 cx.assert_editor_state(&format!(
8149 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8150 ));
8151 let scroll_position_after_selection =
8152 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8153 assert_eq!(
8154 initial_scroll_position, scroll_position_after_selection,
8155 "Scroll position should not change after selecting all matches"
8156 );
8157}
8158
8159#[gpui::test]
8160async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8161 init_test(cx, |_| {});
8162
8163 let mut cx = EditorLspTestContext::new_rust(
8164 lsp::ServerCapabilities {
8165 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8166 ..Default::default()
8167 },
8168 cx,
8169 )
8170 .await;
8171
8172 cx.set_state(indoc! {"
8173 line 1
8174 line 2
8175 linˇe 3
8176 line 4
8177 line 5
8178 "});
8179
8180 // Make an edit
8181 cx.update_editor(|editor, window, cx| {
8182 editor.handle_input("X", window, cx);
8183 });
8184
8185 // Move cursor to a different position
8186 cx.update_editor(|editor, window, cx| {
8187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8188 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8189 });
8190 });
8191
8192 cx.assert_editor_state(indoc! {"
8193 line 1
8194 line 2
8195 linXe 3
8196 line 4
8197 liˇne 5
8198 "});
8199
8200 cx.lsp
8201 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8202 Ok(Some(vec![lsp::TextEdit::new(
8203 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8204 "PREFIX ".to_string(),
8205 )]))
8206 });
8207
8208 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8209 .unwrap()
8210 .await
8211 .unwrap();
8212
8213 cx.assert_editor_state(indoc! {"
8214 PREFIX line 1
8215 line 2
8216 linXe 3
8217 line 4
8218 liˇne 5
8219 "});
8220
8221 // Undo formatting
8222 cx.update_editor(|editor, window, cx| {
8223 editor.undo(&Default::default(), window, cx);
8224 });
8225
8226 // Verify cursor moved back to position after edit
8227 cx.assert_editor_state(indoc! {"
8228 line 1
8229 line 2
8230 linXˇe 3
8231 line 4
8232 line 5
8233 "});
8234}
8235
8236#[gpui::test]
8237async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8238 init_test(cx, |_| {});
8239
8240 let mut cx = EditorTestContext::new(cx).await;
8241
8242 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8243 cx.update_editor(|editor, window, cx| {
8244 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8245 });
8246
8247 cx.set_state(indoc! {"
8248 line 1
8249 line 2
8250 linˇe 3
8251 line 4
8252 line 5
8253 line 6
8254 line 7
8255 line 8
8256 line 9
8257 line 10
8258 "});
8259
8260 let snapshot = cx.buffer_snapshot();
8261 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8262
8263 cx.update(|_, cx| {
8264 provider.update(cx, |provider, _| {
8265 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8266 id: None,
8267 edits: vec![(edit_position..edit_position, "X".into())],
8268 edit_preview: None,
8269 }))
8270 })
8271 });
8272
8273 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8274 cx.update_editor(|editor, window, cx| {
8275 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8276 });
8277
8278 cx.assert_editor_state(indoc! {"
8279 line 1
8280 line 2
8281 lineXˇ 3
8282 line 4
8283 line 5
8284 line 6
8285 line 7
8286 line 8
8287 line 9
8288 line 10
8289 "});
8290
8291 cx.update_editor(|editor, window, cx| {
8292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8293 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8294 });
8295 });
8296
8297 cx.assert_editor_state(indoc! {"
8298 line 1
8299 line 2
8300 lineX 3
8301 line 4
8302 line 5
8303 line 6
8304 line 7
8305 line 8
8306 line 9
8307 liˇne 10
8308 "});
8309
8310 cx.update_editor(|editor, window, cx| {
8311 editor.undo(&Default::default(), window, cx);
8312 });
8313
8314 cx.assert_editor_state(indoc! {"
8315 line 1
8316 line 2
8317 lineˇ 3
8318 line 4
8319 line 5
8320 line 6
8321 line 7
8322 line 8
8323 line 9
8324 line 10
8325 "});
8326}
8327
8328#[gpui::test]
8329async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8330 init_test(cx, |_| {});
8331
8332 let mut cx = EditorTestContext::new(cx).await;
8333 cx.set_state(
8334 r#"let foo = 2;
8335lˇet foo = 2;
8336let fooˇ = 2;
8337let foo = 2;
8338let foo = ˇ2;"#,
8339 );
8340
8341 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8342 .unwrap();
8343 cx.assert_editor_state(
8344 r#"let foo = 2;
8345«letˇ» foo = 2;
8346let «fooˇ» = 2;
8347let foo = 2;
8348let foo = «2ˇ»;"#,
8349 );
8350
8351 // noop for multiple selections with different contents
8352 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8353 .unwrap();
8354 cx.assert_editor_state(
8355 r#"let foo = 2;
8356«letˇ» foo = 2;
8357let «fooˇ» = 2;
8358let foo = 2;
8359let foo = «2ˇ»;"#,
8360 );
8361
8362 // Test last selection direction should be preserved
8363 cx.set_state(
8364 r#"let foo = 2;
8365let foo = 2;
8366let «fooˇ» = 2;
8367let «ˇfoo» = 2;
8368let foo = 2;"#,
8369 );
8370
8371 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8372 .unwrap();
8373 cx.assert_editor_state(
8374 r#"let foo = 2;
8375let foo = 2;
8376let «fooˇ» = 2;
8377let «ˇfoo» = 2;
8378let «ˇfoo» = 2;"#,
8379 );
8380}
8381
8382#[gpui::test]
8383async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8384 init_test(cx, |_| {});
8385
8386 let mut cx =
8387 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8388
8389 cx.assert_editor_state(indoc! {"
8390 ˇbbb
8391 ccc
8392
8393 bbb
8394 ccc
8395 "});
8396 cx.dispatch_action(SelectPrevious::default());
8397 cx.assert_editor_state(indoc! {"
8398 «bbbˇ»
8399 ccc
8400
8401 bbb
8402 ccc
8403 "});
8404 cx.dispatch_action(SelectPrevious::default());
8405 cx.assert_editor_state(indoc! {"
8406 «bbbˇ»
8407 ccc
8408
8409 «bbbˇ»
8410 ccc
8411 "});
8412}
8413
8414#[gpui::test]
8415async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8416 init_test(cx, |_| {});
8417
8418 let mut cx = EditorTestContext::new(cx).await;
8419 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8420
8421 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8422 .unwrap();
8423 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8424
8425 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8426 .unwrap();
8427 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8428
8429 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8430 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8431
8432 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8433 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8434
8435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8436 .unwrap();
8437 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8438
8439 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8440 .unwrap();
8441 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8442}
8443
8444#[gpui::test]
8445async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8446 init_test(cx, |_| {});
8447
8448 let mut cx = EditorTestContext::new(cx).await;
8449 cx.set_state("aˇ");
8450
8451 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8452 .unwrap();
8453 cx.assert_editor_state("«aˇ»");
8454 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8455 .unwrap();
8456 cx.assert_editor_state("«aˇ»");
8457}
8458
8459#[gpui::test]
8460async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8461 init_test(cx, |_| {});
8462
8463 let mut cx = EditorTestContext::new(cx).await;
8464 cx.set_state(
8465 r#"let foo = 2;
8466lˇet foo = 2;
8467let fooˇ = 2;
8468let foo = 2;
8469let foo = ˇ2;"#,
8470 );
8471
8472 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8473 .unwrap();
8474 cx.assert_editor_state(
8475 r#"let foo = 2;
8476«letˇ» foo = 2;
8477let «fooˇ» = 2;
8478let foo = 2;
8479let foo = «2ˇ»;"#,
8480 );
8481
8482 // noop for multiple selections with different contents
8483 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8484 .unwrap();
8485 cx.assert_editor_state(
8486 r#"let foo = 2;
8487«letˇ» foo = 2;
8488let «fooˇ» = 2;
8489let foo = 2;
8490let foo = «2ˇ»;"#,
8491 );
8492}
8493
8494#[gpui::test]
8495async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8496 init_test(cx, |_| {});
8497
8498 let mut cx = EditorTestContext::new(cx).await;
8499 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8500
8501 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8502 .unwrap();
8503 // selection direction is preserved
8504 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8505
8506 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8507 .unwrap();
8508 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8509
8510 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8511 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8512
8513 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8514 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8515
8516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8517 .unwrap();
8518 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8519
8520 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8521 .unwrap();
8522 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8523}
8524
8525#[gpui::test]
8526async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8527 init_test(cx, |_| {});
8528
8529 let language = Arc::new(Language::new(
8530 LanguageConfig::default(),
8531 Some(tree_sitter_rust::LANGUAGE.into()),
8532 ));
8533
8534 let text = r#"
8535 use mod1::mod2::{mod3, mod4};
8536
8537 fn fn_1(param1: bool, param2: &str) {
8538 let var1 = "text";
8539 }
8540 "#
8541 .unindent();
8542
8543 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8544 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8545 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8546
8547 editor
8548 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8549 .await;
8550
8551 editor.update_in(cx, |editor, window, cx| {
8552 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8553 s.select_display_ranges([
8554 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8555 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8556 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8557 ]);
8558 });
8559 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8560 });
8561 editor.update(cx, |editor, cx| {
8562 assert_text_with_selections(
8563 editor,
8564 indoc! {r#"
8565 use mod1::mod2::{mod3, «mod4ˇ»};
8566
8567 fn fn_1«ˇ(param1: bool, param2: &str)» {
8568 let var1 = "«ˇtext»";
8569 }
8570 "#},
8571 cx,
8572 );
8573 });
8574
8575 editor.update_in(cx, |editor, window, cx| {
8576 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8577 });
8578 editor.update(cx, |editor, cx| {
8579 assert_text_with_selections(
8580 editor,
8581 indoc! {r#"
8582 use mod1::mod2::«{mod3, mod4}ˇ»;
8583
8584 «ˇfn fn_1(param1: bool, param2: &str) {
8585 let var1 = "text";
8586 }»
8587 "#},
8588 cx,
8589 );
8590 });
8591
8592 editor.update_in(cx, |editor, window, cx| {
8593 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8594 });
8595 assert_eq!(
8596 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8597 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8598 );
8599
8600 // Trying to expand the selected syntax node one more time has no effect.
8601 editor.update_in(cx, |editor, window, cx| {
8602 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8603 });
8604 assert_eq!(
8605 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8606 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8607 );
8608
8609 editor.update_in(cx, |editor, window, cx| {
8610 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8611 });
8612 editor.update(cx, |editor, cx| {
8613 assert_text_with_selections(
8614 editor,
8615 indoc! {r#"
8616 use mod1::mod2::«{mod3, mod4}ˇ»;
8617
8618 «ˇfn fn_1(param1: bool, param2: &str) {
8619 let var1 = "text";
8620 }»
8621 "#},
8622 cx,
8623 );
8624 });
8625
8626 editor.update_in(cx, |editor, window, cx| {
8627 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8628 });
8629 editor.update(cx, |editor, cx| {
8630 assert_text_with_selections(
8631 editor,
8632 indoc! {r#"
8633 use mod1::mod2::{mod3, «mod4ˇ»};
8634
8635 fn fn_1«ˇ(param1: bool, param2: &str)» {
8636 let var1 = "«ˇtext»";
8637 }
8638 "#},
8639 cx,
8640 );
8641 });
8642
8643 editor.update_in(cx, |editor, window, cx| {
8644 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8645 });
8646 editor.update(cx, |editor, cx| {
8647 assert_text_with_selections(
8648 editor,
8649 indoc! {r#"
8650 use mod1::mod2::{mod3, moˇd4};
8651
8652 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8653 let var1 = "teˇxt";
8654 }
8655 "#},
8656 cx,
8657 );
8658 });
8659
8660 // Trying to shrink the selected syntax node one more time has no effect.
8661 editor.update_in(cx, |editor, window, cx| {
8662 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8663 });
8664 editor.update_in(cx, |editor, _, cx| {
8665 assert_text_with_selections(
8666 editor,
8667 indoc! {r#"
8668 use mod1::mod2::{mod3, moˇd4};
8669
8670 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8671 let var1 = "teˇxt";
8672 }
8673 "#},
8674 cx,
8675 );
8676 });
8677
8678 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8679 // a fold.
8680 editor.update_in(cx, |editor, window, cx| {
8681 editor.fold_creases(
8682 vec![
8683 Crease::simple(
8684 Point::new(0, 21)..Point::new(0, 24),
8685 FoldPlaceholder::test(),
8686 ),
8687 Crease::simple(
8688 Point::new(3, 20)..Point::new(3, 22),
8689 FoldPlaceholder::test(),
8690 ),
8691 ],
8692 true,
8693 window,
8694 cx,
8695 );
8696 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8697 });
8698 editor.update(cx, |editor, cx| {
8699 assert_text_with_selections(
8700 editor,
8701 indoc! {r#"
8702 use mod1::mod2::«{mod3, mod4}ˇ»;
8703
8704 fn fn_1«ˇ(param1: bool, param2: &str)» {
8705 let var1 = "«ˇtext»";
8706 }
8707 "#},
8708 cx,
8709 );
8710 });
8711}
8712
8713#[gpui::test]
8714async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8715 init_test(cx, |_| {});
8716
8717 let language = Arc::new(Language::new(
8718 LanguageConfig::default(),
8719 Some(tree_sitter_rust::LANGUAGE.into()),
8720 ));
8721
8722 let text = "let a = 2;";
8723
8724 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8725 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8726 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8727
8728 editor
8729 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8730 .await;
8731
8732 // Test case 1: Cursor at end of word
8733 editor.update_in(cx, |editor, window, cx| {
8734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8735 s.select_display_ranges([
8736 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8737 ]);
8738 });
8739 });
8740 editor.update(cx, |editor, cx| {
8741 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8742 });
8743 editor.update_in(cx, |editor, window, cx| {
8744 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8745 });
8746 editor.update(cx, |editor, cx| {
8747 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8748 });
8749 editor.update_in(cx, |editor, window, cx| {
8750 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8751 });
8752 editor.update(cx, |editor, cx| {
8753 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8754 });
8755
8756 // Test case 2: Cursor at end of statement
8757 editor.update_in(cx, |editor, window, cx| {
8758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8759 s.select_display_ranges([
8760 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8761 ]);
8762 });
8763 });
8764 editor.update(cx, |editor, cx| {
8765 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8766 });
8767 editor.update_in(cx, |editor, window, cx| {
8768 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8769 });
8770 editor.update(cx, |editor, cx| {
8771 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8772 });
8773}
8774
8775#[gpui::test]
8776async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8777 init_test(cx, |_| {});
8778
8779 let language = Arc::new(Language::new(
8780 LanguageConfig {
8781 name: "JavaScript".into(),
8782 ..Default::default()
8783 },
8784 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8785 ));
8786
8787 let text = r#"
8788 let a = {
8789 key: "value",
8790 };
8791 "#
8792 .unindent();
8793
8794 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8795 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8796 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8797
8798 editor
8799 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8800 .await;
8801
8802 // Test case 1: Cursor after '{'
8803 editor.update_in(cx, |editor, window, cx| {
8804 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8805 s.select_display_ranges([
8806 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8807 ]);
8808 });
8809 });
8810 editor.update(cx, |editor, cx| {
8811 assert_text_with_selections(
8812 editor,
8813 indoc! {r#"
8814 let a = {ˇ
8815 key: "value",
8816 };
8817 "#},
8818 cx,
8819 );
8820 });
8821 editor.update_in(cx, |editor, window, cx| {
8822 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8823 });
8824 editor.update(cx, |editor, cx| {
8825 assert_text_with_selections(
8826 editor,
8827 indoc! {r#"
8828 let a = «ˇ{
8829 key: "value",
8830 }»;
8831 "#},
8832 cx,
8833 );
8834 });
8835
8836 // Test case 2: Cursor after ':'
8837 editor.update_in(cx, |editor, window, cx| {
8838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8839 s.select_display_ranges([
8840 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8841 ]);
8842 });
8843 });
8844 editor.update(cx, |editor, cx| {
8845 assert_text_with_selections(
8846 editor,
8847 indoc! {r#"
8848 let a = {
8849 key:ˇ "value",
8850 };
8851 "#},
8852 cx,
8853 );
8854 });
8855 editor.update_in(cx, |editor, window, cx| {
8856 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8857 });
8858 editor.update(cx, |editor, cx| {
8859 assert_text_with_selections(
8860 editor,
8861 indoc! {r#"
8862 let a = {
8863 «ˇkey: "value"»,
8864 };
8865 "#},
8866 cx,
8867 );
8868 });
8869 editor.update_in(cx, |editor, window, cx| {
8870 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8871 });
8872 editor.update(cx, |editor, cx| {
8873 assert_text_with_selections(
8874 editor,
8875 indoc! {r#"
8876 let a = «ˇ{
8877 key: "value",
8878 }»;
8879 "#},
8880 cx,
8881 );
8882 });
8883
8884 // Test case 3: Cursor after ','
8885 editor.update_in(cx, |editor, window, cx| {
8886 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8887 s.select_display_ranges([
8888 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8889 ]);
8890 });
8891 });
8892 editor.update(cx, |editor, cx| {
8893 assert_text_with_selections(
8894 editor,
8895 indoc! {r#"
8896 let a = {
8897 key: "value",ˇ
8898 };
8899 "#},
8900 cx,
8901 );
8902 });
8903 editor.update_in(cx, |editor, window, cx| {
8904 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8905 });
8906 editor.update(cx, |editor, cx| {
8907 assert_text_with_selections(
8908 editor,
8909 indoc! {r#"
8910 let a = «ˇ{
8911 key: "value",
8912 }»;
8913 "#},
8914 cx,
8915 );
8916 });
8917
8918 // Test case 4: Cursor after ';'
8919 editor.update_in(cx, |editor, window, cx| {
8920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8921 s.select_display_ranges([
8922 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8923 ]);
8924 });
8925 });
8926 editor.update(cx, |editor, cx| {
8927 assert_text_with_selections(
8928 editor,
8929 indoc! {r#"
8930 let a = {
8931 key: "value",
8932 };ˇ
8933 "#},
8934 cx,
8935 );
8936 });
8937 editor.update_in(cx, |editor, window, cx| {
8938 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8939 });
8940 editor.update(cx, |editor, cx| {
8941 assert_text_with_selections(
8942 editor,
8943 indoc! {r#"
8944 «ˇlet a = {
8945 key: "value",
8946 };
8947 »"#},
8948 cx,
8949 );
8950 });
8951}
8952
8953#[gpui::test]
8954async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8955 init_test(cx, |_| {});
8956
8957 let language = Arc::new(Language::new(
8958 LanguageConfig::default(),
8959 Some(tree_sitter_rust::LANGUAGE.into()),
8960 ));
8961
8962 let text = r#"
8963 use mod1::mod2::{mod3, mod4};
8964
8965 fn fn_1(param1: bool, param2: &str) {
8966 let var1 = "hello world";
8967 }
8968 "#
8969 .unindent();
8970
8971 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8972 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8973 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8974
8975 editor
8976 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8977 .await;
8978
8979 // Test 1: Cursor on a letter of a string word
8980 editor.update_in(cx, |editor, window, cx| {
8981 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8982 s.select_display_ranges([
8983 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8984 ]);
8985 });
8986 });
8987 editor.update_in(cx, |editor, window, cx| {
8988 assert_text_with_selections(
8989 editor,
8990 indoc! {r#"
8991 use mod1::mod2::{mod3, mod4};
8992
8993 fn fn_1(param1: bool, param2: &str) {
8994 let var1 = "hˇello world";
8995 }
8996 "#},
8997 cx,
8998 );
8999 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9000 assert_text_with_selections(
9001 editor,
9002 indoc! {r#"
9003 use mod1::mod2::{mod3, mod4};
9004
9005 fn fn_1(param1: bool, param2: &str) {
9006 let var1 = "«ˇhello» world";
9007 }
9008 "#},
9009 cx,
9010 );
9011 });
9012
9013 // Test 2: Partial selection within a word
9014 editor.update_in(cx, |editor, window, cx| {
9015 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9016 s.select_display_ranges([
9017 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9018 ]);
9019 });
9020 });
9021 editor.update_in(cx, |editor, window, cx| {
9022 assert_text_with_selections(
9023 editor,
9024 indoc! {r#"
9025 use mod1::mod2::{mod3, mod4};
9026
9027 fn fn_1(param1: bool, param2: &str) {
9028 let var1 = "h«elˇ»lo world";
9029 }
9030 "#},
9031 cx,
9032 );
9033 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9034 assert_text_with_selections(
9035 editor,
9036 indoc! {r#"
9037 use mod1::mod2::{mod3, mod4};
9038
9039 fn fn_1(param1: bool, param2: &str) {
9040 let var1 = "«ˇhello» world";
9041 }
9042 "#},
9043 cx,
9044 );
9045 });
9046
9047 // Test 3: Complete word already selected
9048 editor.update_in(cx, |editor, window, cx| {
9049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9050 s.select_display_ranges([
9051 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9052 ]);
9053 });
9054 });
9055 editor.update_in(cx, |editor, window, cx| {
9056 assert_text_with_selections(
9057 editor,
9058 indoc! {r#"
9059 use mod1::mod2::{mod3, mod4};
9060
9061 fn fn_1(param1: bool, param2: &str) {
9062 let var1 = "«helloˇ» world";
9063 }
9064 "#},
9065 cx,
9066 );
9067 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9068 assert_text_with_selections(
9069 editor,
9070 indoc! {r#"
9071 use mod1::mod2::{mod3, mod4};
9072
9073 fn fn_1(param1: bool, param2: &str) {
9074 let var1 = "«hello worldˇ»";
9075 }
9076 "#},
9077 cx,
9078 );
9079 });
9080
9081 // Test 4: Selection spanning across words
9082 editor.update_in(cx, |editor, window, cx| {
9083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9084 s.select_display_ranges([
9085 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9086 ]);
9087 });
9088 });
9089 editor.update_in(cx, |editor, window, cx| {
9090 assert_text_with_selections(
9091 editor,
9092 indoc! {r#"
9093 use mod1::mod2::{mod3, mod4};
9094
9095 fn fn_1(param1: bool, param2: &str) {
9096 let var1 = "hel«lo woˇ»rld";
9097 }
9098 "#},
9099 cx,
9100 );
9101 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9102 assert_text_with_selections(
9103 editor,
9104 indoc! {r#"
9105 use mod1::mod2::{mod3, mod4};
9106
9107 fn fn_1(param1: bool, param2: &str) {
9108 let var1 = "«ˇhello world»";
9109 }
9110 "#},
9111 cx,
9112 );
9113 });
9114
9115 // Test 5: Expansion beyond string
9116 editor.update_in(cx, |editor, window, cx| {
9117 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9118 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9119 assert_text_with_selections(
9120 editor,
9121 indoc! {r#"
9122 use mod1::mod2::{mod3, mod4};
9123
9124 fn fn_1(param1: bool, param2: &str) {
9125 «ˇlet var1 = "hello world";»
9126 }
9127 "#},
9128 cx,
9129 );
9130 });
9131}
9132
9133#[gpui::test]
9134async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9135 init_test(cx, |_| {});
9136
9137 let mut cx = EditorTestContext::new(cx).await;
9138
9139 let language = Arc::new(Language::new(
9140 LanguageConfig::default(),
9141 Some(tree_sitter_rust::LANGUAGE.into()),
9142 ));
9143
9144 cx.update_buffer(|buffer, cx| {
9145 buffer.set_language(Some(language), cx);
9146 });
9147
9148 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9149 cx.update_editor(|editor, window, cx| {
9150 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9151 });
9152
9153 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9154}
9155
9156#[gpui::test]
9157async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9158 init_test(cx, |_| {});
9159
9160 let base_text = r#"
9161 impl A {
9162 // this is an uncommitted comment
9163
9164 fn b() {
9165 c();
9166 }
9167
9168 // this is another uncommitted comment
9169
9170 fn d() {
9171 // e
9172 // f
9173 }
9174 }
9175
9176 fn g() {
9177 // h
9178 }
9179 "#
9180 .unindent();
9181
9182 let text = r#"
9183 ˇimpl A {
9184
9185 fn b() {
9186 c();
9187 }
9188
9189 fn d() {
9190 // e
9191 // f
9192 }
9193 }
9194
9195 fn g() {
9196 // h
9197 }
9198 "#
9199 .unindent();
9200
9201 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9202 cx.set_state(&text);
9203 cx.set_head_text(&base_text);
9204 cx.update_editor(|editor, window, cx| {
9205 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9206 });
9207
9208 cx.assert_state_with_diff(
9209 "
9210 ˇimpl A {
9211 - // this is an uncommitted comment
9212
9213 fn b() {
9214 c();
9215 }
9216
9217 - // this is another uncommitted comment
9218 -
9219 fn d() {
9220 // e
9221 // f
9222 }
9223 }
9224
9225 fn g() {
9226 // h
9227 }
9228 "
9229 .unindent(),
9230 );
9231
9232 let expected_display_text = "
9233 impl A {
9234 // this is an uncommitted comment
9235
9236 fn b() {
9237 ⋯
9238 }
9239
9240 // this is another uncommitted comment
9241
9242 fn d() {
9243 ⋯
9244 }
9245 }
9246
9247 fn g() {
9248 ⋯
9249 }
9250 "
9251 .unindent();
9252
9253 cx.update_editor(|editor, window, cx| {
9254 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9255 assert_eq!(editor.display_text(cx), expected_display_text);
9256 });
9257}
9258
9259#[gpui::test]
9260async fn test_autoindent(cx: &mut TestAppContext) {
9261 init_test(cx, |_| {});
9262
9263 let language = Arc::new(
9264 Language::new(
9265 LanguageConfig {
9266 brackets: BracketPairConfig {
9267 pairs: vec![
9268 BracketPair {
9269 start: "{".to_string(),
9270 end: "}".to_string(),
9271 close: false,
9272 surround: false,
9273 newline: true,
9274 },
9275 BracketPair {
9276 start: "(".to_string(),
9277 end: ")".to_string(),
9278 close: false,
9279 surround: false,
9280 newline: true,
9281 },
9282 ],
9283 ..Default::default()
9284 },
9285 ..Default::default()
9286 },
9287 Some(tree_sitter_rust::LANGUAGE.into()),
9288 )
9289 .with_indents_query(
9290 r#"
9291 (_ "(" ")" @end) @indent
9292 (_ "{" "}" @end) @indent
9293 "#,
9294 )
9295 .unwrap(),
9296 );
9297
9298 let text = "fn a() {}";
9299
9300 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9301 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9302 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9303 editor
9304 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9305 .await;
9306
9307 editor.update_in(cx, |editor, window, cx| {
9308 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9309 s.select_ranges([5..5, 8..8, 9..9])
9310 });
9311 editor.newline(&Newline, window, cx);
9312 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9313 assert_eq!(
9314 editor.selections.ranges(cx),
9315 &[
9316 Point::new(1, 4)..Point::new(1, 4),
9317 Point::new(3, 4)..Point::new(3, 4),
9318 Point::new(5, 0)..Point::new(5, 0)
9319 ]
9320 );
9321 });
9322}
9323
9324#[gpui::test]
9325async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9326 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9327
9328 let language = Arc::new(
9329 Language::new(
9330 LanguageConfig {
9331 brackets: BracketPairConfig {
9332 pairs: vec![
9333 BracketPair {
9334 start: "{".to_string(),
9335 end: "}".to_string(),
9336 close: false,
9337 surround: false,
9338 newline: true,
9339 },
9340 BracketPair {
9341 start: "(".to_string(),
9342 end: ")".to_string(),
9343 close: false,
9344 surround: false,
9345 newline: true,
9346 },
9347 ],
9348 ..Default::default()
9349 },
9350 ..Default::default()
9351 },
9352 Some(tree_sitter_rust::LANGUAGE.into()),
9353 )
9354 .with_indents_query(
9355 r#"
9356 (_ "(" ")" @end) @indent
9357 (_ "{" "}" @end) @indent
9358 "#,
9359 )
9360 .unwrap(),
9361 );
9362
9363 let text = "fn a() {}";
9364
9365 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9366 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9367 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9368 editor
9369 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9370 .await;
9371
9372 editor.update_in(cx, |editor, window, cx| {
9373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9374 s.select_ranges([5..5, 8..8, 9..9])
9375 });
9376 editor.newline(&Newline, window, cx);
9377 assert_eq!(
9378 editor.text(cx),
9379 indoc!(
9380 "
9381 fn a(
9382
9383 ) {
9384
9385 }
9386 "
9387 )
9388 );
9389 assert_eq!(
9390 editor.selections.ranges(cx),
9391 &[
9392 Point::new(1, 0)..Point::new(1, 0),
9393 Point::new(3, 0)..Point::new(3, 0),
9394 Point::new(5, 0)..Point::new(5, 0)
9395 ]
9396 );
9397 });
9398}
9399
9400#[gpui::test]
9401async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9402 init_test(cx, |settings| {
9403 settings.defaults.auto_indent = Some(true);
9404 settings.languages.0.insert(
9405 "python".into(),
9406 LanguageSettingsContent {
9407 auto_indent: Some(false),
9408 ..Default::default()
9409 },
9410 );
9411 });
9412
9413 let mut cx = EditorTestContext::new(cx).await;
9414
9415 let injected_language = Arc::new(
9416 Language::new(
9417 LanguageConfig {
9418 brackets: BracketPairConfig {
9419 pairs: vec![
9420 BracketPair {
9421 start: "{".to_string(),
9422 end: "}".to_string(),
9423 close: false,
9424 surround: false,
9425 newline: true,
9426 },
9427 BracketPair {
9428 start: "(".to_string(),
9429 end: ")".to_string(),
9430 close: true,
9431 surround: false,
9432 newline: true,
9433 },
9434 ],
9435 ..Default::default()
9436 },
9437 name: "python".into(),
9438 ..Default::default()
9439 },
9440 Some(tree_sitter_python::LANGUAGE.into()),
9441 )
9442 .with_indents_query(
9443 r#"
9444 (_ "(" ")" @end) @indent
9445 (_ "{" "}" @end) @indent
9446 "#,
9447 )
9448 .unwrap(),
9449 );
9450
9451 let language = Arc::new(
9452 Language::new(
9453 LanguageConfig {
9454 brackets: BracketPairConfig {
9455 pairs: vec![
9456 BracketPair {
9457 start: "{".to_string(),
9458 end: "}".to_string(),
9459 close: false,
9460 surround: false,
9461 newline: true,
9462 },
9463 BracketPair {
9464 start: "(".to_string(),
9465 end: ")".to_string(),
9466 close: true,
9467 surround: false,
9468 newline: true,
9469 },
9470 ],
9471 ..Default::default()
9472 },
9473 name: LanguageName::new("rust"),
9474 ..Default::default()
9475 },
9476 Some(tree_sitter_rust::LANGUAGE.into()),
9477 )
9478 .with_indents_query(
9479 r#"
9480 (_ "(" ")" @end) @indent
9481 (_ "{" "}" @end) @indent
9482 "#,
9483 )
9484 .unwrap()
9485 .with_injection_query(
9486 r#"
9487 (macro_invocation
9488 macro: (identifier) @_macro_name
9489 (token_tree) @injection.content
9490 (#set! injection.language "python"))
9491 "#,
9492 )
9493 .unwrap(),
9494 );
9495
9496 cx.language_registry().add(injected_language);
9497 cx.language_registry().add(language.clone());
9498
9499 cx.update_buffer(|buffer, cx| {
9500 buffer.set_language(Some(language), cx);
9501 });
9502
9503 cx.set_state(r#"struct A {ˇ}"#);
9504
9505 cx.update_editor(|editor, window, cx| {
9506 editor.newline(&Default::default(), window, cx);
9507 });
9508
9509 cx.assert_editor_state(indoc!(
9510 "struct A {
9511 ˇ
9512 }"
9513 ));
9514
9515 cx.set_state(r#"select_biased!(ˇ)"#);
9516
9517 cx.update_editor(|editor, window, cx| {
9518 editor.newline(&Default::default(), window, cx);
9519 editor.handle_input("def ", window, cx);
9520 editor.handle_input("(", window, cx);
9521 editor.newline(&Default::default(), window, cx);
9522 editor.handle_input("a", window, cx);
9523 });
9524
9525 cx.assert_editor_state(indoc!(
9526 "select_biased!(
9527 def (
9528 aˇ
9529 )
9530 )"
9531 ));
9532}
9533
9534#[gpui::test]
9535async fn test_autoindent_selections(cx: &mut TestAppContext) {
9536 init_test(cx, |_| {});
9537
9538 {
9539 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9540 cx.set_state(indoc! {"
9541 impl A {
9542
9543 fn b() {}
9544
9545 «fn c() {
9546
9547 }ˇ»
9548 }
9549 "});
9550
9551 cx.update_editor(|editor, window, cx| {
9552 editor.autoindent(&Default::default(), window, cx);
9553 });
9554
9555 cx.assert_editor_state(indoc! {"
9556 impl A {
9557
9558 fn b() {}
9559
9560 «fn c() {
9561
9562 }ˇ»
9563 }
9564 "});
9565 }
9566
9567 {
9568 let mut cx = EditorTestContext::new_multibuffer(
9569 cx,
9570 [indoc! { "
9571 impl A {
9572 «
9573 // a
9574 fn b(){}
9575 »
9576 «
9577 }
9578 fn c(){}
9579 »
9580 "}],
9581 );
9582
9583 let buffer = cx.update_editor(|editor, _, cx| {
9584 let buffer = editor.buffer().update(cx, |buffer, _| {
9585 buffer.all_buffers().iter().next().unwrap().clone()
9586 });
9587 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9588 buffer
9589 });
9590
9591 cx.run_until_parked();
9592 cx.update_editor(|editor, window, cx| {
9593 editor.select_all(&Default::default(), window, cx);
9594 editor.autoindent(&Default::default(), window, cx)
9595 });
9596 cx.run_until_parked();
9597
9598 cx.update(|_, cx| {
9599 assert_eq!(
9600 buffer.read(cx).text(),
9601 indoc! { "
9602 impl A {
9603
9604 // a
9605 fn b(){}
9606
9607
9608 }
9609 fn c(){}
9610
9611 " }
9612 )
9613 });
9614 }
9615}
9616
9617#[gpui::test]
9618async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9619 init_test(cx, |_| {});
9620
9621 let mut cx = EditorTestContext::new(cx).await;
9622
9623 let language = Arc::new(Language::new(
9624 LanguageConfig {
9625 brackets: BracketPairConfig {
9626 pairs: vec![
9627 BracketPair {
9628 start: "{".to_string(),
9629 end: "}".to_string(),
9630 close: true,
9631 surround: true,
9632 newline: true,
9633 },
9634 BracketPair {
9635 start: "(".to_string(),
9636 end: ")".to_string(),
9637 close: true,
9638 surround: true,
9639 newline: true,
9640 },
9641 BracketPair {
9642 start: "/*".to_string(),
9643 end: " */".to_string(),
9644 close: true,
9645 surround: true,
9646 newline: true,
9647 },
9648 BracketPair {
9649 start: "[".to_string(),
9650 end: "]".to_string(),
9651 close: false,
9652 surround: false,
9653 newline: true,
9654 },
9655 BracketPair {
9656 start: "\"".to_string(),
9657 end: "\"".to_string(),
9658 close: true,
9659 surround: true,
9660 newline: false,
9661 },
9662 BracketPair {
9663 start: "<".to_string(),
9664 end: ">".to_string(),
9665 close: false,
9666 surround: true,
9667 newline: true,
9668 },
9669 ],
9670 ..Default::default()
9671 },
9672 autoclose_before: "})]".to_string(),
9673 ..Default::default()
9674 },
9675 Some(tree_sitter_rust::LANGUAGE.into()),
9676 ));
9677
9678 cx.language_registry().add(language.clone());
9679 cx.update_buffer(|buffer, cx| {
9680 buffer.set_language(Some(language), cx);
9681 });
9682
9683 cx.set_state(
9684 &r#"
9685 🏀ˇ
9686 εˇ
9687 ❤️ˇ
9688 "#
9689 .unindent(),
9690 );
9691
9692 // autoclose multiple nested brackets at multiple cursors
9693 cx.update_editor(|editor, window, cx| {
9694 editor.handle_input("{", window, cx);
9695 editor.handle_input("{", window, cx);
9696 editor.handle_input("{", window, cx);
9697 });
9698 cx.assert_editor_state(
9699 &"
9700 🏀{{{ˇ}}}
9701 ε{{{ˇ}}}
9702 ❤️{{{ˇ}}}
9703 "
9704 .unindent(),
9705 );
9706
9707 // insert a different closing bracket
9708 cx.update_editor(|editor, window, cx| {
9709 editor.handle_input(")", window, cx);
9710 });
9711 cx.assert_editor_state(
9712 &"
9713 🏀{{{)ˇ}}}
9714 ε{{{)ˇ}}}
9715 ❤️{{{)ˇ}}}
9716 "
9717 .unindent(),
9718 );
9719
9720 // skip over the auto-closed brackets when typing a closing bracket
9721 cx.update_editor(|editor, window, cx| {
9722 editor.move_right(&MoveRight, window, cx);
9723 editor.handle_input("}", window, cx);
9724 editor.handle_input("}", window, cx);
9725 editor.handle_input("}", window, cx);
9726 });
9727 cx.assert_editor_state(
9728 &"
9729 🏀{{{)}}}}ˇ
9730 ε{{{)}}}}ˇ
9731 ❤️{{{)}}}}ˇ
9732 "
9733 .unindent(),
9734 );
9735
9736 // autoclose multi-character pairs
9737 cx.set_state(
9738 &"
9739 ˇ
9740 ˇ
9741 "
9742 .unindent(),
9743 );
9744 cx.update_editor(|editor, window, cx| {
9745 editor.handle_input("/", window, cx);
9746 editor.handle_input("*", window, cx);
9747 });
9748 cx.assert_editor_state(
9749 &"
9750 /*ˇ */
9751 /*ˇ */
9752 "
9753 .unindent(),
9754 );
9755
9756 // one cursor autocloses a multi-character pair, one cursor
9757 // does not autoclose.
9758 cx.set_state(
9759 &"
9760 /ˇ
9761 ˇ
9762 "
9763 .unindent(),
9764 );
9765 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9766 cx.assert_editor_state(
9767 &"
9768 /*ˇ */
9769 *ˇ
9770 "
9771 .unindent(),
9772 );
9773
9774 // Don't autoclose if the next character isn't whitespace and isn't
9775 // listed in the language's "autoclose_before" section.
9776 cx.set_state("ˇa b");
9777 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9778 cx.assert_editor_state("{ˇa b");
9779
9780 // Don't autoclose if `close` is false for the bracket pair
9781 cx.set_state("ˇ");
9782 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9783 cx.assert_editor_state("[ˇ");
9784
9785 // Surround with brackets if text is selected
9786 cx.set_state("«aˇ» b");
9787 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9788 cx.assert_editor_state("{«aˇ»} b");
9789
9790 // Autoclose when not immediately after a word character
9791 cx.set_state("a ˇ");
9792 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9793 cx.assert_editor_state("a \"ˇ\"");
9794
9795 // Autoclose pair where the start and end characters are the same
9796 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9797 cx.assert_editor_state("a \"\"ˇ");
9798
9799 // Don't autoclose when immediately after a word character
9800 cx.set_state("aˇ");
9801 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9802 cx.assert_editor_state("a\"ˇ");
9803
9804 // Do autoclose when after a non-word character
9805 cx.set_state("{ˇ");
9806 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9807 cx.assert_editor_state("{\"ˇ\"");
9808
9809 // Non identical pairs autoclose regardless of preceding character
9810 cx.set_state("aˇ");
9811 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9812 cx.assert_editor_state("a{ˇ}");
9813
9814 // Don't autoclose pair if autoclose is disabled
9815 cx.set_state("ˇ");
9816 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9817 cx.assert_editor_state("<ˇ");
9818
9819 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9820 cx.set_state("«aˇ» b");
9821 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9822 cx.assert_editor_state("<«aˇ»> b");
9823}
9824
9825#[gpui::test]
9826async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9827 init_test(cx, |settings| {
9828 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9829 });
9830
9831 let mut cx = EditorTestContext::new(cx).await;
9832
9833 let language = Arc::new(Language::new(
9834 LanguageConfig {
9835 brackets: BracketPairConfig {
9836 pairs: vec![
9837 BracketPair {
9838 start: "{".to_string(),
9839 end: "}".to_string(),
9840 close: true,
9841 surround: true,
9842 newline: true,
9843 },
9844 BracketPair {
9845 start: "(".to_string(),
9846 end: ")".to_string(),
9847 close: true,
9848 surround: true,
9849 newline: true,
9850 },
9851 BracketPair {
9852 start: "[".to_string(),
9853 end: "]".to_string(),
9854 close: false,
9855 surround: false,
9856 newline: true,
9857 },
9858 ],
9859 ..Default::default()
9860 },
9861 autoclose_before: "})]".to_string(),
9862 ..Default::default()
9863 },
9864 Some(tree_sitter_rust::LANGUAGE.into()),
9865 ));
9866
9867 cx.language_registry().add(language.clone());
9868 cx.update_buffer(|buffer, cx| {
9869 buffer.set_language(Some(language), cx);
9870 });
9871
9872 cx.set_state(
9873 &"
9874 ˇ
9875 ˇ
9876 ˇ
9877 "
9878 .unindent(),
9879 );
9880
9881 // ensure only matching closing brackets are skipped over
9882 cx.update_editor(|editor, window, cx| {
9883 editor.handle_input("}", window, cx);
9884 editor.move_left(&MoveLeft, window, cx);
9885 editor.handle_input(")", window, cx);
9886 editor.move_left(&MoveLeft, window, cx);
9887 });
9888 cx.assert_editor_state(
9889 &"
9890 ˇ)}
9891 ˇ)}
9892 ˇ)}
9893 "
9894 .unindent(),
9895 );
9896
9897 // skip-over closing brackets at multiple cursors
9898 cx.update_editor(|editor, window, cx| {
9899 editor.handle_input(")", window, cx);
9900 editor.handle_input("}", window, cx);
9901 });
9902 cx.assert_editor_state(
9903 &"
9904 )}ˇ
9905 )}ˇ
9906 )}ˇ
9907 "
9908 .unindent(),
9909 );
9910
9911 // ignore non-close brackets
9912 cx.update_editor(|editor, window, cx| {
9913 editor.handle_input("]", window, cx);
9914 editor.move_left(&MoveLeft, window, cx);
9915 editor.handle_input("]", window, cx);
9916 });
9917 cx.assert_editor_state(
9918 &"
9919 )}]ˇ]
9920 )}]ˇ]
9921 )}]ˇ]
9922 "
9923 .unindent(),
9924 );
9925}
9926
9927#[gpui::test]
9928async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9929 init_test(cx, |_| {});
9930
9931 let mut cx = EditorTestContext::new(cx).await;
9932
9933 let html_language = Arc::new(
9934 Language::new(
9935 LanguageConfig {
9936 name: "HTML".into(),
9937 brackets: BracketPairConfig {
9938 pairs: vec![
9939 BracketPair {
9940 start: "<".into(),
9941 end: ">".into(),
9942 close: true,
9943 ..Default::default()
9944 },
9945 BracketPair {
9946 start: "{".into(),
9947 end: "}".into(),
9948 close: true,
9949 ..Default::default()
9950 },
9951 BracketPair {
9952 start: "(".into(),
9953 end: ")".into(),
9954 close: true,
9955 ..Default::default()
9956 },
9957 ],
9958 ..Default::default()
9959 },
9960 autoclose_before: "})]>".into(),
9961 ..Default::default()
9962 },
9963 Some(tree_sitter_html::LANGUAGE.into()),
9964 )
9965 .with_injection_query(
9966 r#"
9967 (script_element
9968 (raw_text) @injection.content
9969 (#set! injection.language "javascript"))
9970 "#,
9971 )
9972 .unwrap(),
9973 );
9974
9975 let javascript_language = Arc::new(Language::new(
9976 LanguageConfig {
9977 name: "JavaScript".into(),
9978 brackets: BracketPairConfig {
9979 pairs: vec![
9980 BracketPair {
9981 start: "/*".into(),
9982 end: " */".into(),
9983 close: true,
9984 ..Default::default()
9985 },
9986 BracketPair {
9987 start: "{".into(),
9988 end: "}".into(),
9989 close: true,
9990 ..Default::default()
9991 },
9992 BracketPair {
9993 start: "(".into(),
9994 end: ")".into(),
9995 close: true,
9996 ..Default::default()
9997 },
9998 ],
9999 ..Default::default()
10000 },
10001 autoclose_before: "})]>".into(),
10002 ..Default::default()
10003 },
10004 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10005 ));
10006
10007 cx.language_registry().add(html_language.clone());
10008 cx.language_registry().add(javascript_language);
10009 cx.executor().run_until_parked();
10010
10011 cx.update_buffer(|buffer, cx| {
10012 buffer.set_language(Some(html_language), cx);
10013 });
10014
10015 cx.set_state(
10016 &r#"
10017 <body>ˇ
10018 <script>
10019 var x = 1;ˇ
10020 </script>
10021 </body>ˇ
10022 "#
10023 .unindent(),
10024 );
10025
10026 // Precondition: different languages are active at different locations.
10027 cx.update_editor(|editor, window, cx| {
10028 let snapshot = editor.snapshot(window, cx);
10029 let cursors = editor.selections.ranges::<usize>(cx);
10030 let languages = cursors
10031 .iter()
10032 .map(|c| snapshot.language_at(c.start).unwrap().name())
10033 .collect::<Vec<_>>();
10034 assert_eq!(
10035 languages,
10036 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10037 );
10038 });
10039
10040 // Angle brackets autoclose in HTML, but not JavaScript.
10041 cx.update_editor(|editor, window, cx| {
10042 editor.handle_input("<", window, cx);
10043 editor.handle_input("a", window, cx);
10044 });
10045 cx.assert_editor_state(
10046 &r#"
10047 <body><aˇ>
10048 <script>
10049 var x = 1;<aˇ
10050 </script>
10051 </body><aˇ>
10052 "#
10053 .unindent(),
10054 );
10055
10056 // Curly braces and parens autoclose in both HTML and JavaScript.
10057 cx.update_editor(|editor, window, cx| {
10058 editor.handle_input(" b=", window, cx);
10059 editor.handle_input("{", window, cx);
10060 editor.handle_input("c", window, cx);
10061 editor.handle_input("(", window, cx);
10062 });
10063 cx.assert_editor_state(
10064 &r#"
10065 <body><a b={c(ˇ)}>
10066 <script>
10067 var x = 1;<a b={c(ˇ)}
10068 </script>
10069 </body><a b={c(ˇ)}>
10070 "#
10071 .unindent(),
10072 );
10073
10074 // Brackets that were already autoclosed are skipped.
10075 cx.update_editor(|editor, window, cx| {
10076 editor.handle_input(")", window, cx);
10077 editor.handle_input("d", window, cx);
10078 editor.handle_input("}", window, cx);
10079 });
10080 cx.assert_editor_state(
10081 &r#"
10082 <body><a b={c()d}ˇ>
10083 <script>
10084 var x = 1;<a b={c()d}ˇ
10085 </script>
10086 </body><a b={c()d}ˇ>
10087 "#
10088 .unindent(),
10089 );
10090 cx.update_editor(|editor, window, cx| {
10091 editor.handle_input(">", window, cx);
10092 });
10093 cx.assert_editor_state(
10094 &r#"
10095 <body><a b={c()d}>ˇ
10096 <script>
10097 var x = 1;<a b={c()d}>ˇ
10098 </script>
10099 </body><a b={c()d}>ˇ
10100 "#
10101 .unindent(),
10102 );
10103
10104 // Reset
10105 cx.set_state(
10106 &r#"
10107 <body>ˇ
10108 <script>
10109 var x = 1;ˇ
10110 </script>
10111 </body>ˇ
10112 "#
10113 .unindent(),
10114 );
10115
10116 cx.update_editor(|editor, window, cx| {
10117 editor.handle_input("<", window, cx);
10118 });
10119 cx.assert_editor_state(
10120 &r#"
10121 <body><ˇ>
10122 <script>
10123 var x = 1;<ˇ
10124 </script>
10125 </body><ˇ>
10126 "#
10127 .unindent(),
10128 );
10129
10130 // When backspacing, the closing angle brackets are removed.
10131 cx.update_editor(|editor, window, cx| {
10132 editor.backspace(&Backspace, window, cx);
10133 });
10134 cx.assert_editor_state(
10135 &r#"
10136 <body>ˇ
10137 <script>
10138 var x = 1;ˇ
10139 </script>
10140 </body>ˇ
10141 "#
10142 .unindent(),
10143 );
10144
10145 // Block comments autoclose in JavaScript, but not HTML.
10146 cx.update_editor(|editor, window, cx| {
10147 editor.handle_input("/", window, cx);
10148 editor.handle_input("*", window, cx);
10149 });
10150 cx.assert_editor_state(
10151 &r#"
10152 <body>/*ˇ
10153 <script>
10154 var x = 1;/*ˇ */
10155 </script>
10156 </body>/*ˇ
10157 "#
10158 .unindent(),
10159 );
10160}
10161
10162#[gpui::test]
10163async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10164 init_test(cx, |_| {});
10165
10166 let mut cx = EditorTestContext::new(cx).await;
10167
10168 let rust_language = Arc::new(
10169 Language::new(
10170 LanguageConfig {
10171 name: "Rust".into(),
10172 brackets: serde_json::from_value(json!([
10173 { "start": "{", "end": "}", "close": true, "newline": true },
10174 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10175 ]))
10176 .unwrap(),
10177 autoclose_before: "})]>".into(),
10178 ..Default::default()
10179 },
10180 Some(tree_sitter_rust::LANGUAGE.into()),
10181 )
10182 .with_override_query("(string_literal) @string")
10183 .unwrap(),
10184 );
10185
10186 cx.language_registry().add(rust_language.clone());
10187 cx.update_buffer(|buffer, cx| {
10188 buffer.set_language(Some(rust_language), cx);
10189 });
10190
10191 cx.set_state(
10192 &r#"
10193 let x = ˇ
10194 "#
10195 .unindent(),
10196 );
10197
10198 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10199 cx.update_editor(|editor, window, cx| {
10200 editor.handle_input("\"", window, cx);
10201 });
10202 cx.assert_editor_state(
10203 &r#"
10204 let x = "ˇ"
10205 "#
10206 .unindent(),
10207 );
10208
10209 // Inserting another quotation mark. The cursor moves across the existing
10210 // automatically-inserted quotation mark.
10211 cx.update_editor(|editor, window, cx| {
10212 editor.handle_input("\"", window, cx);
10213 });
10214 cx.assert_editor_state(
10215 &r#"
10216 let x = ""ˇ
10217 "#
10218 .unindent(),
10219 );
10220
10221 // Reset
10222 cx.set_state(
10223 &r#"
10224 let x = ˇ
10225 "#
10226 .unindent(),
10227 );
10228
10229 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10230 cx.update_editor(|editor, window, cx| {
10231 editor.handle_input("\"", window, cx);
10232 editor.handle_input(" ", window, cx);
10233 editor.move_left(&Default::default(), window, cx);
10234 editor.handle_input("\\", window, cx);
10235 editor.handle_input("\"", window, cx);
10236 });
10237 cx.assert_editor_state(
10238 &r#"
10239 let x = "\"ˇ "
10240 "#
10241 .unindent(),
10242 );
10243
10244 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10245 // mark. Nothing is inserted.
10246 cx.update_editor(|editor, window, cx| {
10247 editor.move_right(&Default::default(), window, cx);
10248 editor.handle_input("\"", window, cx);
10249 });
10250 cx.assert_editor_state(
10251 &r#"
10252 let x = "\" "ˇ
10253 "#
10254 .unindent(),
10255 );
10256}
10257
10258#[gpui::test]
10259async fn test_surround_with_pair(cx: &mut TestAppContext) {
10260 init_test(cx, |_| {});
10261
10262 let language = Arc::new(Language::new(
10263 LanguageConfig {
10264 brackets: BracketPairConfig {
10265 pairs: vec![
10266 BracketPair {
10267 start: "{".to_string(),
10268 end: "}".to_string(),
10269 close: true,
10270 surround: true,
10271 newline: true,
10272 },
10273 BracketPair {
10274 start: "/* ".to_string(),
10275 end: "*/".to_string(),
10276 close: true,
10277 surround: true,
10278 ..Default::default()
10279 },
10280 ],
10281 ..Default::default()
10282 },
10283 ..Default::default()
10284 },
10285 Some(tree_sitter_rust::LANGUAGE.into()),
10286 ));
10287
10288 let text = r#"
10289 a
10290 b
10291 c
10292 "#
10293 .unindent();
10294
10295 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10296 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10297 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10298 editor
10299 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10300 .await;
10301
10302 editor.update_in(cx, |editor, window, cx| {
10303 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10304 s.select_display_ranges([
10305 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10306 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10307 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10308 ])
10309 });
10310
10311 editor.handle_input("{", window, cx);
10312 editor.handle_input("{", window, cx);
10313 editor.handle_input("{", window, cx);
10314 assert_eq!(
10315 editor.text(cx),
10316 "
10317 {{{a}}}
10318 {{{b}}}
10319 {{{c}}}
10320 "
10321 .unindent()
10322 );
10323 assert_eq!(
10324 editor.selections.display_ranges(cx),
10325 [
10326 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10327 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10328 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10329 ]
10330 );
10331
10332 editor.undo(&Undo, window, cx);
10333 editor.undo(&Undo, window, cx);
10334 editor.undo(&Undo, window, cx);
10335 assert_eq!(
10336 editor.text(cx),
10337 "
10338 a
10339 b
10340 c
10341 "
10342 .unindent()
10343 );
10344 assert_eq!(
10345 editor.selections.display_ranges(cx),
10346 [
10347 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10348 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10349 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10350 ]
10351 );
10352
10353 // Ensure inserting the first character of a multi-byte bracket pair
10354 // doesn't surround the selections with the bracket.
10355 editor.handle_input("/", window, cx);
10356 assert_eq!(
10357 editor.text(cx),
10358 "
10359 /
10360 /
10361 /
10362 "
10363 .unindent()
10364 );
10365 assert_eq!(
10366 editor.selections.display_ranges(cx),
10367 [
10368 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10369 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10370 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10371 ]
10372 );
10373
10374 editor.undo(&Undo, window, cx);
10375 assert_eq!(
10376 editor.text(cx),
10377 "
10378 a
10379 b
10380 c
10381 "
10382 .unindent()
10383 );
10384 assert_eq!(
10385 editor.selections.display_ranges(cx),
10386 [
10387 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10388 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10389 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10390 ]
10391 );
10392
10393 // Ensure inserting the last character of a multi-byte bracket pair
10394 // doesn't surround the selections with the bracket.
10395 editor.handle_input("*", window, cx);
10396 assert_eq!(
10397 editor.text(cx),
10398 "
10399 *
10400 *
10401 *
10402 "
10403 .unindent()
10404 );
10405 assert_eq!(
10406 editor.selections.display_ranges(cx),
10407 [
10408 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10409 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10410 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10411 ]
10412 );
10413 });
10414}
10415
10416#[gpui::test]
10417async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10418 init_test(cx, |_| {});
10419
10420 let language = Arc::new(Language::new(
10421 LanguageConfig {
10422 brackets: BracketPairConfig {
10423 pairs: vec![BracketPair {
10424 start: "{".to_string(),
10425 end: "}".to_string(),
10426 close: true,
10427 surround: true,
10428 newline: true,
10429 }],
10430 ..Default::default()
10431 },
10432 autoclose_before: "}".to_string(),
10433 ..Default::default()
10434 },
10435 Some(tree_sitter_rust::LANGUAGE.into()),
10436 ));
10437
10438 let text = r#"
10439 a
10440 b
10441 c
10442 "#
10443 .unindent();
10444
10445 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10446 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10447 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10448 editor
10449 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10450 .await;
10451
10452 editor.update_in(cx, |editor, window, cx| {
10453 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10454 s.select_ranges([
10455 Point::new(0, 1)..Point::new(0, 1),
10456 Point::new(1, 1)..Point::new(1, 1),
10457 Point::new(2, 1)..Point::new(2, 1),
10458 ])
10459 });
10460
10461 editor.handle_input("{", window, cx);
10462 editor.handle_input("{", window, cx);
10463 editor.handle_input("_", window, cx);
10464 assert_eq!(
10465 editor.text(cx),
10466 "
10467 a{{_}}
10468 b{{_}}
10469 c{{_}}
10470 "
10471 .unindent()
10472 );
10473 assert_eq!(
10474 editor.selections.ranges::<Point>(cx),
10475 [
10476 Point::new(0, 4)..Point::new(0, 4),
10477 Point::new(1, 4)..Point::new(1, 4),
10478 Point::new(2, 4)..Point::new(2, 4)
10479 ]
10480 );
10481
10482 editor.backspace(&Default::default(), window, cx);
10483 editor.backspace(&Default::default(), window, cx);
10484 assert_eq!(
10485 editor.text(cx),
10486 "
10487 a{}
10488 b{}
10489 c{}
10490 "
10491 .unindent()
10492 );
10493 assert_eq!(
10494 editor.selections.ranges::<Point>(cx),
10495 [
10496 Point::new(0, 2)..Point::new(0, 2),
10497 Point::new(1, 2)..Point::new(1, 2),
10498 Point::new(2, 2)..Point::new(2, 2)
10499 ]
10500 );
10501
10502 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10503 assert_eq!(
10504 editor.text(cx),
10505 "
10506 a
10507 b
10508 c
10509 "
10510 .unindent()
10511 );
10512 assert_eq!(
10513 editor.selections.ranges::<Point>(cx),
10514 [
10515 Point::new(0, 1)..Point::new(0, 1),
10516 Point::new(1, 1)..Point::new(1, 1),
10517 Point::new(2, 1)..Point::new(2, 1)
10518 ]
10519 );
10520 });
10521}
10522
10523#[gpui::test]
10524async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10525 init_test(cx, |settings| {
10526 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10527 });
10528
10529 let mut cx = EditorTestContext::new(cx).await;
10530
10531 let language = Arc::new(Language::new(
10532 LanguageConfig {
10533 brackets: BracketPairConfig {
10534 pairs: vec![
10535 BracketPair {
10536 start: "{".to_string(),
10537 end: "}".to_string(),
10538 close: true,
10539 surround: true,
10540 newline: true,
10541 },
10542 BracketPair {
10543 start: "(".to_string(),
10544 end: ")".to_string(),
10545 close: true,
10546 surround: true,
10547 newline: true,
10548 },
10549 BracketPair {
10550 start: "[".to_string(),
10551 end: "]".to_string(),
10552 close: false,
10553 surround: true,
10554 newline: true,
10555 },
10556 ],
10557 ..Default::default()
10558 },
10559 autoclose_before: "})]".to_string(),
10560 ..Default::default()
10561 },
10562 Some(tree_sitter_rust::LANGUAGE.into()),
10563 ));
10564
10565 cx.language_registry().add(language.clone());
10566 cx.update_buffer(|buffer, cx| {
10567 buffer.set_language(Some(language), cx);
10568 });
10569
10570 cx.set_state(
10571 &"
10572 {(ˇ)}
10573 [[ˇ]]
10574 {(ˇ)}
10575 "
10576 .unindent(),
10577 );
10578
10579 cx.update_editor(|editor, window, cx| {
10580 editor.backspace(&Default::default(), window, cx);
10581 editor.backspace(&Default::default(), window, cx);
10582 });
10583
10584 cx.assert_editor_state(
10585 &"
10586 ˇ
10587 ˇ]]
10588 ˇ
10589 "
10590 .unindent(),
10591 );
10592
10593 cx.update_editor(|editor, window, cx| {
10594 editor.handle_input("{", window, cx);
10595 editor.handle_input("{", window, cx);
10596 editor.move_right(&MoveRight, window, cx);
10597 editor.move_right(&MoveRight, window, cx);
10598 editor.move_left(&MoveLeft, window, cx);
10599 editor.move_left(&MoveLeft, window, cx);
10600 editor.backspace(&Default::default(), window, cx);
10601 });
10602
10603 cx.assert_editor_state(
10604 &"
10605 {ˇ}
10606 {ˇ}]]
10607 {ˇ}
10608 "
10609 .unindent(),
10610 );
10611
10612 cx.update_editor(|editor, window, cx| {
10613 editor.backspace(&Default::default(), window, cx);
10614 });
10615
10616 cx.assert_editor_state(
10617 &"
10618 ˇ
10619 ˇ]]
10620 ˇ
10621 "
10622 .unindent(),
10623 );
10624}
10625
10626#[gpui::test]
10627async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10628 init_test(cx, |_| {});
10629
10630 let language = Arc::new(Language::new(
10631 LanguageConfig::default(),
10632 Some(tree_sitter_rust::LANGUAGE.into()),
10633 ));
10634
10635 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10636 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10637 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10638 editor
10639 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10640 .await;
10641
10642 editor.update_in(cx, |editor, window, cx| {
10643 editor.set_auto_replace_emoji_shortcode(true);
10644
10645 editor.handle_input("Hello ", window, cx);
10646 editor.handle_input(":wave", window, cx);
10647 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10648
10649 editor.handle_input(":", window, cx);
10650 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10651
10652 editor.handle_input(" :smile", window, cx);
10653 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10654
10655 editor.handle_input(":", window, cx);
10656 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10657
10658 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10659 editor.handle_input(":wave", window, cx);
10660 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10661
10662 editor.handle_input(":", window, cx);
10663 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10664
10665 editor.handle_input(":1", window, cx);
10666 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10667
10668 editor.handle_input(":", window, cx);
10669 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10670
10671 // Ensure shortcode does not get replaced when it is part of a word
10672 editor.handle_input(" Test:wave", window, cx);
10673 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10674
10675 editor.handle_input(":", window, cx);
10676 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10677
10678 editor.set_auto_replace_emoji_shortcode(false);
10679
10680 // Ensure shortcode does not get replaced when auto replace is off
10681 editor.handle_input(" :wave", window, cx);
10682 assert_eq!(
10683 editor.text(cx),
10684 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10685 );
10686
10687 editor.handle_input(":", window, cx);
10688 assert_eq!(
10689 editor.text(cx),
10690 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10691 );
10692 });
10693}
10694
10695#[gpui::test]
10696async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10697 init_test(cx, |_| {});
10698
10699 let (text, insertion_ranges) = marked_text_ranges(
10700 indoc! {"
10701 ˇ
10702 "},
10703 false,
10704 );
10705
10706 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10707 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10708
10709 _ = editor.update_in(cx, |editor, window, cx| {
10710 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10711
10712 editor
10713 .insert_snippet(&insertion_ranges, snippet, window, cx)
10714 .unwrap();
10715
10716 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10717 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10718 assert_eq!(editor.text(cx), expected_text);
10719 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10720 }
10721
10722 assert(
10723 editor,
10724 cx,
10725 indoc! {"
10726 type «» =•
10727 "},
10728 );
10729
10730 assert!(editor.context_menu_visible(), "There should be a matches");
10731 });
10732}
10733
10734#[gpui::test]
10735async fn test_snippets(cx: &mut TestAppContext) {
10736 init_test(cx, |_| {});
10737
10738 let mut cx = EditorTestContext::new(cx).await;
10739
10740 cx.set_state(indoc! {"
10741 a.ˇ b
10742 a.ˇ b
10743 a.ˇ b
10744 "});
10745
10746 cx.update_editor(|editor, window, cx| {
10747 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10748 let insertion_ranges = editor
10749 .selections
10750 .all(cx)
10751 .iter()
10752 .map(|s| s.range())
10753 .collect::<Vec<_>>();
10754 editor
10755 .insert_snippet(&insertion_ranges, snippet, window, cx)
10756 .unwrap();
10757 });
10758
10759 cx.assert_editor_state(indoc! {"
10760 a.f(«oneˇ», two, «threeˇ») b
10761 a.f(«oneˇ», two, «threeˇ») b
10762 a.f(«oneˇ», two, «threeˇ») b
10763 "});
10764
10765 // Can't move earlier than the first tab stop
10766 cx.update_editor(|editor, window, cx| {
10767 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10768 });
10769 cx.assert_editor_state(indoc! {"
10770 a.f(«oneˇ», two, «threeˇ») b
10771 a.f(«oneˇ», two, «threeˇ») b
10772 a.f(«oneˇ», two, «threeˇ») b
10773 "});
10774
10775 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10776 cx.assert_editor_state(indoc! {"
10777 a.f(one, «twoˇ», three) b
10778 a.f(one, «twoˇ», three) b
10779 a.f(one, «twoˇ», three) b
10780 "});
10781
10782 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10783 cx.assert_editor_state(indoc! {"
10784 a.f(«oneˇ», two, «threeˇ») b
10785 a.f(«oneˇ», two, «threeˇ») b
10786 a.f(«oneˇ», two, «threeˇ») b
10787 "});
10788
10789 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10790 cx.assert_editor_state(indoc! {"
10791 a.f(one, «twoˇ», three) b
10792 a.f(one, «twoˇ», three) b
10793 a.f(one, «twoˇ», three) b
10794 "});
10795 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10796 cx.assert_editor_state(indoc! {"
10797 a.f(one, two, three)ˇ b
10798 a.f(one, two, three)ˇ b
10799 a.f(one, two, three)ˇ b
10800 "});
10801
10802 // As soon as the last tab stop is reached, snippet state is gone
10803 cx.update_editor(|editor, window, cx| {
10804 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10805 });
10806 cx.assert_editor_state(indoc! {"
10807 a.f(one, two, three)ˇ b
10808 a.f(one, two, three)ˇ b
10809 a.f(one, two, three)ˇ b
10810 "});
10811}
10812
10813#[gpui::test]
10814async fn test_snippet_indentation(cx: &mut TestAppContext) {
10815 init_test(cx, |_| {});
10816
10817 let mut cx = EditorTestContext::new(cx).await;
10818
10819 cx.update_editor(|editor, window, cx| {
10820 let snippet = Snippet::parse(indoc! {"
10821 /*
10822 * Multiline comment with leading indentation
10823 *
10824 * $1
10825 */
10826 $0"})
10827 .unwrap();
10828 let insertion_ranges = editor
10829 .selections
10830 .all(cx)
10831 .iter()
10832 .map(|s| s.range())
10833 .collect::<Vec<_>>();
10834 editor
10835 .insert_snippet(&insertion_ranges, snippet, window, cx)
10836 .unwrap();
10837 });
10838
10839 cx.assert_editor_state(indoc! {"
10840 /*
10841 * Multiline comment with leading indentation
10842 *
10843 * ˇ
10844 */
10845 "});
10846
10847 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10848 cx.assert_editor_state(indoc! {"
10849 /*
10850 * Multiline comment with leading indentation
10851 *
10852 *•
10853 */
10854 ˇ"});
10855}
10856
10857#[gpui::test]
10858async fn test_document_format_during_save(cx: &mut TestAppContext) {
10859 init_test(cx, |_| {});
10860
10861 let fs = FakeFs::new(cx.executor());
10862 fs.insert_file(path!("/file.rs"), Default::default()).await;
10863
10864 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10865
10866 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10867 language_registry.add(rust_lang());
10868 let mut fake_servers = language_registry.register_fake_lsp(
10869 "Rust",
10870 FakeLspAdapter {
10871 capabilities: lsp::ServerCapabilities {
10872 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10873 ..Default::default()
10874 },
10875 ..Default::default()
10876 },
10877 );
10878
10879 let buffer = project
10880 .update(cx, |project, cx| {
10881 project.open_local_buffer(path!("/file.rs"), cx)
10882 })
10883 .await
10884 .unwrap();
10885
10886 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10887 let (editor, cx) = cx.add_window_view(|window, cx| {
10888 build_editor_with_project(project.clone(), buffer, window, cx)
10889 });
10890 editor.update_in(cx, |editor, window, cx| {
10891 editor.set_text("one\ntwo\nthree\n", window, cx)
10892 });
10893 assert!(cx.read(|cx| editor.is_dirty(cx)));
10894
10895 cx.executor().start_waiting();
10896 let fake_server = fake_servers.next().await.unwrap();
10897
10898 {
10899 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10900 move |params, _| async move {
10901 assert_eq!(
10902 params.text_document.uri,
10903 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10904 );
10905 assert_eq!(params.options.tab_size, 4);
10906 Ok(Some(vec![lsp::TextEdit::new(
10907 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10908 ", ".to_string(),
10909 )]))
10910 },
10911 );
10912 let save = editor
10913 .update_in(cx, |editor, window, cx| {
10914 editor.save(
10915 SaveOptions {
10916 format: true,
10917 autosave: false,
10918 },
10919 project.clone(),
10920 window,
10921 cx,
10922 )
10923 })
10924 .unwrap();
10925 cx.executor().start_waiting();
10926 save.await;
10927
10928 assert_eq!(
10929 editor.update(cx, |editor, cx| editor.text(cx)),
10930 "one, two\nthree\n"
10931 );
10932 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10933 }
10934
10935 {
10936 editor.update_in(cx, |editor, window, cx| {
10937 editor.set_text("one\ntwo\nthree\n", window, cx)
10938 });
10939 assert!(cx.read(|cx| editor.is_dirty(cx)));
10940
10941 // Ensure we can still save even if formatting hangs.
10942 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10943 move |params, _| async move {
10944 assert_eq!(
10945 params.text_document.uri,
10946 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10947 );
10948 futures::future::pending::<()>().await;
10949 unreachable!()
10950 },
10951 );
10952 let save = editor
10953 .update_in(cx, |editor, window, cx| {
10954 editor.save(
10955 SaveOptions {
10956 format: true,
10957 autosave: false,
10958 },
10959 project.clone(),
10960 window,
10961 cx,
10962 )
10963 })
10964 .unwrap();
10965 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10966 cx.executor().start_waiting();
10967 save.await;
10968 assert_eq!(
10969 editor.update(cx, |editor, cx| editor.text(cx)),
10970 "one\ntwo\nthree\n"
10971 );
10972 }
10973
10974 // Set rust language override and assert overridden tabsize is sent to language server
10975 update_test_language_settings(cx, |settings| {
10976 settings.languages.0.insert(
10977 "Rust".into(),
10978 LanguageSettingsContent {
10979 tab_size: NonZeroU32::new(8),
10980 ..Default::default()
10981 },
10982 );
10983 });
10984
10985 {
10986 editor.update_in(cx, |editor, window, cx| {
10987 editor.set_text("somehting_new\n", window, cx)
10988 });
10989 assert!(cx.read(|cx| editor.is_dirty(cx)));
10990 let _formatting_request_signal = fake_server
10991 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10992 assert_eq!(
10993 params.text_document.uri,
10994 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10995 );
10996 assert_eq!(params.options.tab_size, 8);
10997 Ok(Some(vec![]))
10998 });
10999 let save = editor
11000 .update_in(cx, |editor, window, cx| {
11001 editor.save(
11002 SaveOptions {
11003 format: true,
11004 autosave: false,
11005 },
11006 project.clone(),
11007 window,
11008 cx,
11009 )
11010 })
11011 .unwrap();
11012 cx.executor().start_waiting();
11013 save.await;
11014 }
11015}
11016
11017#[gpui::test]
11018async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11019 init_test(cx, |settings| {
11020 settings.defaults.ensure_final_newline_on_save = Some(false);
11021 });
11022
11023 let fs = FakeFs::new(cx.executor());
11024 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11025
11026 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11027
11028 let buffer = project
11029 .update(cx, |project, cx| {
11030 project.open_local_buffer(path!("/file.txt"), cx)
11031 })
11032 .await
11033 .unwrap();
11034
11035 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11036 let (editor, cx) = cx.add_window_view(|window, cx| {
11037 build_editor_with_project(project.clone(), buffer, window, cx)
11038 });
11039 editor.update_in(cx, |editor, window, cx| {
11040 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11041 s.select_ranges([0..0])
11042 });
11043 });
11044 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11045
11046 editor.update_in(cx, |editor, window, cx| {
11047 editor.handle_input("\n", window, cx)
11048 });
11049 cx.run_until_parked();
11050 save(&editor, &project, cx).await;
11051 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11052
11053 editor.update_in(cx, |editor, window, cx| {
11054 editor.undo(&Default::default(), window, cx);
11055 });
11056 save(&editor, &project, cx).await;
11057 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11058
11059 editor.update_in(cx, |editor, window, cx| {
11060 editor.redo(&Default::default(), window, cx);
11061 });
11062 cx.run_until_parked();
11063 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11064
11065 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11066 let save = editor
11067 .update_in(cx, |editor, window, cx| {
11068 editor.save(
11069 SaveOptions {
11070 format: true,
11071 autosave: false,
11072 },
11073 project.clone(),
11074 window,
11075 cx,
11076 )
11077 })
11078 .unwrap();
11079 cx.executor().start_waiting();
11080 save.await;
11081 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11082 }
11083}
11084
11085#[gpui::test]
11086async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11087 init_test(cx, |_| {});
11088
11089 let cols = 4;
11090 let rows = 10;
11091 let sample_text_1 = sample_text(rows, cols, 'a');
11092 assert_eq!(
11093 sample_text_1,
11094 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11095 );
11096 let sample_text_2 = sample_text(rows, cols, 'l');
11097 assert_eq!(
11098 sample_text_2,
11099 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11100 );
11101 let sample_text_3 = sample_text(rows, cols, 'v');
11102 assert_eq!(
11103 sample_text_3,
11104 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11105 );
11106
11107 let fs = FakeFs::new(cx.executor());
11108 fs.insert_tree(
11109 path!("/a"),
11110 json!({
11111 "main.rs": sample_text_1,
11112 "other.rs": sample_text_2,
11113 "lib.rs": sample_text_3,
11114 }),
11115 )
11116 .await;
11117
11118 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11119 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11120 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11121
11122 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11123 language_registry.add(rust_lang());
11124 let mut fake_servers = language_registry.register_fake_lsp(
11125 "Rust",
11126 FakeLspAdapter {
11127 capabilities: lsp::ServerCapabilities {
11128 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11129 ..Default::default()
11130 },
11131 ..Default::default()
11132 },
11133 );
11134
11135 let worktree = project.update(cx, |project, cx| {
11136 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11137 assert_eq!(worktrees.len(), 1);
11138 worktrees.pop().unwrap()
11139 });
11140 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11141
11142 let buffer_1 = project
11143 .update(cx, |project, cx| {
11144 project.open_buffer((worktree_id, "main.rs"), cx)
11145 })
11146 .await
11147 .unwrap();
11148 let buffer_2 = project
11149 .update(cx, |project, cx| {
11150 project.open_buffer((worktree_id, "other.rs"), cx)
11151 })
11152 .await
11153 .unwrap();
11154 let buffer_3 = project
11155 .update(cx, |project, cx| {
11156 project.open_buffer((worktree_id, "lib.rs"), cx)
11157 })
11158 .await
11159 .unwrap();
11160
11161 let multi_buffer = cx.new(|cx| {
11162 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11163 multi_buffer.push_excerpts(
11164 buffer_1.clone(),
11165 [
11166 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11167 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11168 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11169 ],
11170 cx,
11171 );
11172 multi_buffer.push_excerpts(
11173 buffer_2.clone(),
11174 [
11175 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11176 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11177 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11178 ],
11179 cx,
11180 );
11181 multi_buffer.push_excerpts(
11182 buffer_3.clone(),
11183 [
11184 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11185 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11186 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11187 ],
11188 cx,
11189 );
11190 multi_buffer
11191 });
11192 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11193 Editor::new(
11194 EditorMode::full(),
11195 multi_buffer,
11196 Some(project.clone()),
11197 window,
11198 cx,
11199 )
11200 });
11201
11202 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11203 editor.change_selections(
11204 SelectionEffects::scroll(Autoscroll::Next),
11205 window,
11206 cx,
11207 |s| s.select_ranges(Some(1..2)),
11208 );
11209 editor.insert("|one|two|three|", window, cx);
11210 });
11211 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11212 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11213 editor.change_selections(
11214 SelectionEffects::scroll(Autoscroll::Next),
11215 window,
11216 cx,
11217 |s| s.select_ranges(Some(60..70)),
11218 );
11219 editor.insert("|four|five|six|", window, cx);
11220 });
11221 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11222
11223 // First two buffers should be edited, but not the third one.
11224 assert_eq!(
11225 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11226 "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}",
11227 );
11228 buffer_1.update(cx, |buffer, _| {
11229 assert!(buffer.is_dirty());
11230 assert_eq!(
11231 buffer.text(),
11232 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11233 )
11234 });
11235 buffer_2.update(cx, |buffer, _| {
11236 assert!(buffer.is_dirty());
11237 assert_eq!(
11238 buffer.text(),
11239 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11240 )
11241 });
11242 buffer_3.update(cx, |buffer, _| {
11243 assert!(!buffer.is_dirty());
11244 assert_eq!(buffer.text(), sample_text_3,)
11245 });
11246 cx.executor().run_until_parked();
11247
11248 cx.executor().start_waiting();
11249 let save = multi_buffer_editor
11250 .update_in(cx, |editor, window, cx| {
11251 editor.save(
11252 SaveOptions {
11253 format: true,
11254 autosave: false,
11255 },
11256 project.clone(),
11257 window,
11258 cx,
11259 )
11260 })
11261 .unwrap();
11262
11263 let fake_server = fake_servers.next().await.unwrap();
11264 fake_server
11265 .server
11266 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11267 Ok(Some(vec![lsp::TextEdit::new(
11268 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11269 format!("[{} formatted]", params.text_document.uri),
11270 )]))
11271 })
11272 .detach();
11273 save.await;
11274
11275 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11276 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11277 assert_eq!(
11278 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11279 uri!(
11280 "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}"
11281 ),
11282 );
11283 buffer_1.update(cx, |buffer, _| {
11284 assert!(!buffer.is_dirty());
11285 assert_eq!(
11286 buffer.text(),
11287 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11288 )
11289 });
11290 buffer_2.update(cx, |buffer, _| {
11291 assert!(!buffer.is_dirty());
11292 assert_eq!(
11293 buffer.text(),
11294 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11295 )
11296 });
11297 buffer_3.update(cx, |buffer, _| {
11298 assert!(!buffer.is_dirty());
11299 assert_eq!(buffer.text(), sample_text_3,)
11300 });
11301}
11302
11303#[gpui::test]
11304async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11305 init_test(cx, |_| {});
11306
11307 let fs = FakeFs::new(cx.executor());
11308 fs.insert_tree(
11309 path!("/dir"),
11310 json!({
11311 "file1.rs": "fn main() { println!(\"hello\"); }",
11312 "file2.rs": "fn test() { println!(\"test\"); }",
11313 "file3.rs": "fn other() { println!(\"other\"); }\n",
11314 }),
11315 )
11316 .await;
11317
11318 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11319 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11320 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11321
11322 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11323 language_registry.add(rust_lang());
11324
11325 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11326 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11327
11328 // Open three buffers
11329 let buffer_1 = project
11330 .update(cx, |project, cx| {
11331 project.open_buffer((worktree_id, "file1.rs"), cx)
11332 })
11333 .await
11334 .unwrap();
11335 let buffer_2 = project
11336 .update(cx, |project, cx| {
11337 project.open_buffer((worktree_id, "file2.rs"), cx)
11338 })
11339 .await
11340 .unwrap();
11341 let buffer_3 = project
11342 .update(cx, |project, cx| {
11343 project.open_buffer((worktree_id, "file3.rs"), cx)
11344 })
11345 .await
11346 .unwrap();
11347
11348 // Create a multi-buffer with all three buffers
11349 let multi_buffer = cx.new(|cx| {
11350 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11351 multi_buffer.push_excerpts(
11352 buffer_1.clone(),
11353 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11354 cx,
11355 );
11356 multi_buffer.push_excerpts(
11357 buffer_2.clone(),
11358 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11359 cx,
11360 );
11361 multi_buffer.push_excerpts(
11362 buffer_3.clone(),
11363 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11364 cx,
11365 );
11366 multi_buffer
11367 });
11368
11369 let editor = cx.new_window_entity(|window, cx| {
11370 Editor::new(
11371 EditorMode::full(),
11372 multi_buffer,
11373 Some(project.clone()),
11374 window,
11375 cx,
11376 )
11377 });
11378
11379 // Edit only the first buffer
11380 editor.update_in(cx, |editor, window, cx| {
11381 editor.change_selections(
11382 SelectionEffects::scroll(Autoscroll::Next),
11383 window,
11384 cx,
11385 |s| s.select_ranges(Some(10..10)),
11386 );
11387 editor.insert("// edited", window, cx);
11388 });
11389
11390 // Verify that only buffer 1 is dirty
11391 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11392 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11393 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11394
11395 // Get write counts after file creation (files were created with initial content)
11396 // We expect each file to have been written once during creation
11397 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11398 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11399 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11400
11401 // Perform autosave
11402 let save_task = editor.update_in(cx, |editor, window, cx| {
11403 editor.save(
11404 SaveOptions {
11405 format: true,
11406 autosave: true,
11407 },
11408 project.clone(),
11409 window,
11410 cx,
11411 )
11412 });
11413 save_task.await.unwrap();
11414
11415 // Only the dirty buffer should have been saved
11416 assert_eq!(
11417 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11418 1,
11419 "Buffer 1 was dirty, so it should have been written once during autosave"
11420 );
11421 assert_eq!(
11422 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11423 0,
11424 "Buffer 2 was clean, so it should not have been written during autosave"
11425 );
11426 assert_eq!(
11427 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11428 0,
11429 "Buffer 3 was clean, so it should not have been written during autosave"
11430 );
11431
11432 // Verify buffer states after autosave
11433 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11434 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11435 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11436
11437 // Now perform a manual save (format = true)
11438 let save_task = editor.update_in(cx, |editor, window, cx| {
11439 editor.save(
11440 SaveOptions {
11441 format: true,
11442 autosave: false,
11443 },
11444 project.clone(),
11445 window,
11446 cx,
11447 )
11448 });
11449 save_task.await.unwrap();
11450
11451 // During manual save, clean buffers don't get written to disk
11452 // They just get did_save called for language server notifications
11453 assert_eq!(
11454 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11455 1,
11456 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11457 );
11458 assert_eq!(
11459 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11460 0,
11461 "Buffer 2 should not have been written at all"
11462 );
11463 assert_eq!(
11464 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11465 0,
11466 "Buffer 3 should not have been written at all"
11467 );
11468}
11469
11470async fn setup_range_format_test(
11471 cx: &mut TestAppContext,
11472) -> (
11473 Entity<Project>,
11474 Entity<Editor>,
11475 &mut gpui::VisualTestContext,
11476 lsp::FakeLanguageServer,
11477) {
11478 init_test(cx, |_| {});
11479
11480 let fs = FakeFs::new(cx.executor());
11481 fs.insert_file(path!("/file.rs"), Default::default()).await;
11482
11483 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11484
11485 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11486 language_registry.add(rust_lang());
11487 let mut fake_servers = language_registry.register_fake_lsp(
11488 "Rust",
11489 FakeLspAdapter {
11490 capabilities: lsp::ServerCapabilities {
11491 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11492 ..lsp::ServerCapabilities::default()
11493 },
11494 ..FakeLspAdapter::default()
11495 },
11496 );
11497
11498 let buffer = project
11499 .update(cx, |project, cx| {
11500 project.open_local_buffer(path!("/file.rs"), cx)
11501 })
11502 .await
11503 .unwrap();
11504
11505 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11506 let (editor, cx) = cx.add_window_view(|window, cx| {
11507 build_editor_with_project(project.clone(), buffer, window, cx)
11508 });
11509
11510 cx.executor().start_waiting();
11511 let fake_server = fake_servers.next().await.unwrap();
11512
11513 (project, editor, cx, fake_server)
11514}
11515
11516#[gpui::test]
11517async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11518 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11519
11520 editor.update_in(cx, |editor, window, cx| {
11521 editor.set_text("one\ntwo\nthree\n", window, cx)
11522 });
11523 assert!(cx.read(|cx| editor.is_dirty(cx)));
11524
11525 let save = editor
11526 .update_in(cx, |editor, window, cx| {
11527 editor.save(
11528 SaveOptions {
11529 format: true,
11530 autosave: false,
11531 },
11532 project.clone(),
11533 window,
11534 cx,
11535 )
11536 })
11537 .unwrap();
11538 fake_server
11539 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11540 assert_eq!(
11541 params.text_document.uri,
11542 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11543 );
11544 assert_eq!(params.options.tab_size, 4);
11545 Ok(Some(vec![lsp::TextEdit::new(
11546 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11547 ", ".to_string(),
11548 )]))
11549 })
11550 .next()
11551 .await;
11552 cx.executor().start_waiting();
11553 save.await;
11554 assert_eq!(
11555 editor.update(cx, |editor, cx| editor.text(cx)),
11556 "one, two\nthree\n"
11557 );
11558 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11559}
11560
11561#[gpui::test]
11562async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11563 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11564
11565 editor.update_in(cx, |editor, window, cx| {
11566 editor.set_text("one\ntwo\nthree\n", window, cx)
11567 });
11568 assert!(cx.read(|cx| editor.is_dirty(cx)));
11569
11570 // Test that save still works when formatting hangs
11571 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11572 move |params, _| async move {
11573 assert_eq!(
11574 params.text_document.uri,
11575 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11576 );
11577 futures::future::pending::<()>().await;
11578 unreachable!()
11579 },
11580 );
11581 let save = editor
11582 .update_in(cx, |editor, window, cx| {
11583 editor.save(
11584 SaveOptions {
11585 format: true,
11586 autosave: false,
11587 },
11588 project.clone(),
11589 window,
11590 cx,
11591 )
11592 })
11593 .unwrap();
11594 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11595 cx.executor().start_waiting();
11596 save.await;
11597 assert_eq!(
11598 editor.update(cx, |editor, cx| editor.text(cx)),
11599 "one\ntwo\nthree\n"
11600 );
11601 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11602}
11603
11604#[gpui::test]
11605async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11606 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11607
11608 // Buffer starts clean, no formatting should be requested
11609 let save = editor
11610 .update_in(cx, |editor, window, cx| {
11611 editor.save(
11612 SaveOptions {
11613 format: false,
11614 autosave: false,
11615 },
11616 project.clone(),
11617 window,
11618 cx,
11619 )
11620 })
11621 .unwrap();
11622 let _pending_format_request = fake_server
11623 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11624 panic!("Should not be invoked");
11625 })
11626 .next();
11627 cx.executor().start_waiting();
11628 save.await;
11629 cx.run_until_parked();
11630}
11631
11632#[gpui::test]
11633async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11634 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11635
11636 // Set Rust language override and assert overridden tabsize is sent to language server
11637 update_test_language_settings(cx, |settings| {
11638 settings.languages.0.insert(
11639 "Rust".into(),
11640 LanguageSettingsContent {
11641 tab_size: NonZeroU32::new(8),
11642 ..Default::default()
11643 },
11644 );
11645 });
11646
11647 editor.update_in(cx, |editor, window, cx| {
11648 editor.set_text("something_new\n", window, cx)
11649 });
11650 assert!(cx.read(|cx| editor.is_dirty(cx)));
11651 let save = editor
11652 .update_in(cx, |editor, window, cx| {
11653 editor.save(
11654 SaveOptions {
11655 format: true,
11656 autosave: false,
11657 },
11658 project.clone(),
11659 window,
11660 cx,
11661 )
11662 })
11663 .unwrap();
11664 fake_server
11665 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11666 assert_eq!(
11667 params.text_document.uri,
11668 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11669 );
11670 assert_eq!(params.options.tab_size, 8);
11671 Ok(Some(Vec::new()))
11672 })
11673 .next()
11674 .await;
11675 save.await;
11676}
11677
11678#[gpui::test]
11679async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11680 init_test(cx, |settings| {
11681 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11682 Formatter::LanguageServer { name: None },
11683 )))
11684 });
11685
11686 let fs = FakeFs::new(cx.executor());
11687 fs.insert_file(path!("/file.rs"), Default::default()).await;
11688
11689 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11690
11691 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11692 language_registry.add(Arc::new(Language::new(
11693 LanguageConfig {
11694 name: "Rust".into(),
11695 matcher: LanguageMatcher {
11696 path_suffixes: vec!["rs".to_string()],
11697 ..Default::default()
11698 },
11699 ..LanguageConfig::default()
11700 },
11701 Some(tree_sitter_rust::LANGUAGE.into()),
11702 )));
11703 update_test_language_settings(cx, |settings| {
11704 // Enable Prettier formatting for the same buffer, and ensure
11705 // LSP is called instead of Prettier.
11706 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11707 });
11708 let mut fake_servers = language_registry.register_fake_lsp(
11709 "Rust",
11710 FakeLspAdapter {
11711 capabilities: lsp::ServerCapabilities {
11712 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11713 ..Default::default()
11714 },
11715 ..Default::default()
11716 },
11717 );
11718
11719 let buffer = project
11720 .update(cx, |project, cx| {
11721 project.open_local_buffer(path!("/file.rs"), cx)
11722 })
11723 .await
11724 .unwrap();
11725
11726 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11727 let (editor, cx) = cx.add_window_view(|window, cx| {
11728 build_editor_with_project(project.clone(), buffer, window, cx)
11729 });
11730 editor.update_in(cx, |editor, window, cx| {
11731 editor.set_text("one\ntwo\nthree\n", window, cx)
11732 });
11733
11734 cx.executor().start_waiting();
11735 let fake_server = fake_servers.next().await.unwrap();
11736
11737 let format = editor
11738 .update_in(cx, |editor, window, cx| {
11739 editor.perform_format(
11740 project.clone(),
11741 FormatTrigger::Manual,
11742 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11743 window,
11744 cx,
11745 )
11746 })
11747 .unwrap();
11748 fake_server
11749 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11750 assert_eq!(
11751 params.text_document.uri,
11752 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11753 );
11754 assert_eq!(params.options.tab_size, 4);
11755 Ok(Some(vec![lsp::TextEdit::new(
11756 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11757 ", ".to_string(),
11758 )]))
11759 })
11760 .next()
11761 .await;
11762 cx.executor().start_waiting();
11763 format.await;
11764 assert_eq!(
11765 editor.update(cx, |editor, cx| editor.text(cx)),
11766 "one, two\nthree\n"
11767 );
11768
11769 editor.update_in(cx, |editor, window, cx| {
11770 editor.set_text("one\ntwo\nthree\n", window, cx)
11771 });
11772 // Ensure we don't lock if formatting hangs.
11773 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11774 move |params, _| async move {
11775 assert_eq!(
11776 params.text_document.uri,
11777 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11778 );
11779 futures::future::pending::<()>().await;
11780 unreachable!()
11781 },
11782 );
11783 let format = editor
11784 .update_in(cx, |editor, window, cx| {
11785 editor.perform_format(
11786 project,
11787 FormatTrigger::Manual,
11788 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11789 window,
11790 cx,
11791 )
11792 })
11793 .unwrap();
11794 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11795 cx.executor().start_waiting();
11796 format.await;
11797 assert_eq!(
11798 editor.update(cx, |editor, cx| editor.text(cx)),
11799 "one\ntwo\nthree\n"
11800 );
11801}
11802
11803#[gpui::test]
11804async fn test_multiple_formatters(cx: &mut TestAppContext) {
11805 init_test(cx, |settings| {
11806 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11807 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11808 Formatter::LanguageServer { name: None },
11809 Formatter::CodeActions(
11810 [
11811 ("code-action-1".into(), true),
11812 ("code-action-2".into(), true),
11813 ]
11814 .into_iter()
11815 .collect(),
11816 ),
11817 ])))
11818 });
11819
11820 let fs = FakeFs::new(cx.executor());
11821 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11822 .await;
11823
11824 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11825 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11826 language_registry.add(rust_lang());
11827
11828 let mut fake_servers = language_registry.register_fake_lsp(
11829 "Rust",
11830 FakeLspAdapter {
11831 capabilities: lsp::ServerCapabilities {
11832 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11833 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11834 commands: vec!["the-command-for-code-action-1".into()],
11835 ..Default::default()
11836 }),
11837 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11838 ..Default::default()
11839 },
11840 ..Default::default()
11841 },
11842 );
11843
11844 let buffer = project
11845 .update(cx, |project, cx| {
11846 project.open_local_buffer(path!("/file.rs"), cx)
11847 })
11848 .await
11849 .unwrap();
11850
11851 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11852 let (editor, cx) = cx.add_window_view(|window, cx| {
11853 build_editor_with_project(project.clone(), buffer, window, cx)
11854 });
11855
11856 cx.executor().start_waiting();
11857
11858 let fake_server = fake_servers.next().await.unwrap();
11859 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11860 move |_params, _| async move {
11861 Ok(Some(vec![lsp::TextEdit::new(
11862 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11863 "applied-formatting\n".to_string(),
11864 )]))
11865 },
11866 );
11867 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11868 move |params, _| async move {
11869 assert_eq!(
11870 params.context.only,
11871 Some(vec!["code-action-1".into(), "code-action-2".into()])
11872 );
11873 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11874 Ok(Some(vec![
11875 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11876 kind: Some("code-action-1".into()),
11877 edit: Some(lsp::WorkspaceEdit::new(
11878 [(
11879 uri.clone(),
11880 vec![lsp::TextEdit::new(
11881 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11882 "applied-code-action-1-edit\n".to_string(),
11883 )],
11884 )]
11885 .into_iter()
11886 .collect(),
11887 )),
11888 command: Some(lsp::Command {
11889 command: "the-command-for-code-action-1".into(),
11890 ..Default::default()
11891 }),
11892 ..Default::default()
11893 }),
11894 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11895 kind: Some("code-action-2".into()),
11896 edit: Some(lsp::WorkspaceEdit::new(
11897 [(
11898 uri,
11899 vec![lsp::TextEdit::new(
11900 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11901 "applied-code-action-2-edit\n".to_string(),
11902 )],
11903 )]
11904 .into_iter()
11905 .collect(),
11906 )),
11907 ..Default::default()
11908 }),
11909 ]))
11910 },
11911 );
11912
11913 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11914 move |params, _| async move { Ok(params) }
11915 });
11916
11917 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11918 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11919 let fake = fake_server.clone();
11920 let lock = command_lock.clone();
11921 move |params, _| {
11922 assert_eq!(params.command, "the-command-for-code-action-1");
11923 let fake = fake.clone();
11924 let lock = lock.clone();
11925 async move {
11926 lock.lock().await;
11927 fake.server
11928 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11929 label: None,
11930 edit: lsp::WorkspaceEdit {
11931 changes: Some(
11932 [(
11933 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11934 vec![lsp::TextEdit {
11935 range: lsp::Range::new(
11936 lsp::Position::new(0, 0),
11937 lsp::Position::new(0, 0),
11938 ),
11939 new_text: "applied-code-action-1-command\n".into(),
11940 }],
11941 )]
11942 .into_iter()
11943 .collect(),
11944 ),
11945 ..Default::default()
11946 },
11947 })
11948 .await
11949 .into_response()
11950 .unwrap();
11951 Ok(Some(json!(null)))
11952 }
11953 }
11954 });
11955
11956 cx.executor().start_waiting();
11957 editor
11958 .update_in(cx, |editor, window, cx| {
11959 editor.perform_format(
11960 project.clone(),
11961 FormatTrigger::Manual,
11962 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11963 window,
11964 cx,
11965 )
11966 })
11967 .unwrap()
11968 .await;
11969 editor.update(cx, |editor, cx| {
11970 assert_eq!(
11971 editor.text(cx),
11972 r#"
11973 applied-code-action-2-edit
11974 applied-code-action-1-command
11975 applied-code-action-1-edit
11976 applied-formatting
11977 one
11978 two
11979 three
11980 "#
11981 .unindent()
11982 );
11983 });
11984
11985 editor.update_in(cx, |editor, window, cx| {
11986 editor.undo(&Default::default(), window, cx);
11987 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11988 });
11989
11990 // Perform a manual edit while waiting for an LSP command
11991 // that's being run as part of a formatting code action.
11992 let lock_guard = command_lock.lock().await;
11993 let format = editor
11994 .update_in(cx, |editor, window, cx| {
11995 editor.perform_format(
11996 project.clone(),
11997 FormatTrigger::Manual,
11998 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11999 window,
12000 cx,
12001 )
12002 })
12003 .unwrap();
12004 cx.run_until_parked();
12005 editor.update(cx, |editor, cx| {
12006 assert_eq!(
12007 editor.text(cx),
12008 r#"
12009 applied-code-action-1-edit
12010 applied-formatting
12011 one
12012 two
12013 three
12014 "#
12015 .unindent()
12016 );
12017
12018 editor.buffer.update(cx, |buffer, cx| {
12019 let ix = buffer.len(cx);
12020 buffer.edit([(ix..ix, "edited\n")], None, cx);
12021 });
12022 });
12023
12024 // Allow the LSP command to proceed. Because the buffer was edited,
12025 // the second code action will not be run.
12026 drop(lock_guard);
12027 format.await;
12028 editor.update_in(cx, |editor, window, cx| {
12029 assert_eq!(
12030 editor.text(cx),
12031 r#"
12032 applied-code-action-1-command
12033 applied-code-action-1-edit
12034 applied-formatting
12035 one
12036 two
12037 three
12038 edited
12039 "#
12040 .unindent()
12041 );
12042
12043 // The manual edit is undone first, because it is the last thing the user did
12044 // (even though the command completed afterwards).
12045 editor.undo(&Default::default(), window, cx);
12046 assert_eq!(
12047 editor.text(cx),
12048 r#"
12049 applied-code-action-1-command
12050 applied-code-action-1-edit
12051 applied-formatting
12052 one
12053 two
12054 three
12055 "#
12056 .unindent()
12057 );
12058
12059 // All the formatting (including the command, which completed after the manual edit)
12060 // is undone together.
12061 editor.undo(&Default::default(), window, cx);
12062 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12063 });
12064}
12065
12066#[gpui::test]
12067async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12068 init_test(cx, |settings| {
12069 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12070 Formatter::LanguageServer { name: None },
12071 ])))
12072 });
12073
12074 let fs = FakeFs::new(cx.executor());
12075 fs.insert_file(path!("/file.ts"), Default::default()).await;
12076
12077 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12078
12079 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12080 language_registry.add(Arc::new(Language::new(
12081 LanguageConfig {
12082 name: "TypeScript".into(),
12083 matcher: LanguageMatcher {
12084 path_suffixes: vec!["ts".to_string()],
12085 ..Default::default()
12086 },
12087 ..LanguageConfig::default()
12088 },
12089 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12090 )));
12091 update_test_language_settings(cx, |settings| {
12092 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12093 });
12094 let mut fake_servers = language_registry.register_fake_lsp(
12095 "TypeScript",
12096 FakeLspAdapter {
12097 capabilities: lsp::ServerCapabilities {
12098 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12099 ..Default::default()
12100 },
12101 ..Default::default()
12102 },
12103 );
12104
12105 let buffer = project
12106 .update(cx, |project, cx| {
12107 project.open_local_buffer(path!("/file.ts"), cx)
12108 })
12109 .await
12110 .unwrap();
12111
12112 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12113 let (editor, cx) = cx.add_window_view(|window, cx| {
12114 build_editor_with_project(project.clone(), buffer, window, cx)
12115 });
12116 editor.update_in(cx, |editor, window, cx| {
12117 editor.set_text(
12118 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12119 window,
12120 cx,
12121 )
12122 });
12123
12124 cx.executor().start_waiting();
12125 let fake_server = fake_servers.next().await.unwrap();
12126
12127 let format = editor
12128 .update_in(cx, |editor, window, cx| {
12129 editor.perform_code_action_kind(
12130 project.clone(),
12131 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12132 window,
12133 cx,
12134 )
12135 })
12136 .unwrap();
12137 fake_server
12138 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12139 assert_eq!(
12140 params.text_document.uri,
12141 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12142 );
12143 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12144 lsp::CodeAction {
12145 title: "Organize Imports".to_string(),
12146 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12147 edit: Some(lsp::WorkspaceEdit {
12148 changes: Some(
12149 [(
12150 params.text_document.uri.clone(),
12151 vec![lsp::TextEdit::new(
12152 lsp::Range::new(
12153 lsp::Position::new(1, 0),
12154 lsp::Position::new(2, 0),
12155 ),
12156 "".to_string(),
12157 )],
12158 )]
12159 .into_iter()
12160 .collect(),
12161 ),
12162 ..Default::default()
12163 }),
12164 ..Default::default()
12165 },
12166 )]))
12167 })
12168 .next()
12169 .await;
12170 cx.executor().start_waiting();
12171 format.await;
12172 assert_eq!(
12173 editor.update(cx, |editor, cx| editor.text(cx)),
12174 "import { a } from 'module';\n\nconst x = a;\n"
12175 );
12176
12177 editor.update_in(cx, |editor, window, cx| {
12178 editor.set_text(
12179 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12180 window,
12181 cx,
12182 )
12183 });
12184 // Ensure we don't lock if code action hangs.
12185 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12186 move |params, _| async move {
12187 assert_eq!(
12188 params.text_document.uri,
12189 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12190 );
12191 futures::future::pending::<()>().await;
12192 unreachable!()
12193 },
12194 );
12195 let format = editor
12196 .update_in(cx, |editor, window, cx| {
12197 editor.perform_code_action_kind(
12198 project,
12199 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12200 window,
12201 cx,
12202 )
12203 })
12204 .unwrap();
12205 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12206 cx.executor().start_waiting();
12207 format.await;
12208 assert_eq!(
12209 editor.update(cx, |editor, cx| editor.text(cx)),
12210 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12211 );
12212}
12213
12214#[gpui::test]
12215async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12216 init_test(cx, |_| {});
12217
12218 let mut cx = EditorLspTestContext::new_rust(
12219 lsp::ServerCapabilities {
12220 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12221 ..Default::default()
12222 },
12223 cx,
12224 )
12225 .await;
12226
12227 cx.set_state(indoc! {"
12228 one.twoˇ
12229 "});
12230
12231 // The format request takes a long time. When it completes, it inserts
12232 // a newline and an indent before the `.`
12233 cx.lsp
12234 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12235 let executor = cx.background_executor().clone();
12236 async move {
12237 executor.timer(Duration::from_millis(100)).await;
12238 Ok(Some(vec![lsp::TextEdit {
12239 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12240 new_text: "\n ".into(),
12241 }]))
12242 }
12243 });
12244
12245 // Submit a format request.
12246 let format_1 = cx
12247 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12248 .unwrap();
12249 cx.executor().run_until_parked();
12250
12251 // Submit a second format request.
12252 let format_2 = cx
12253 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12254 .unwrap();
12255 cx.executor().run_until_parked();
12256
12257 // Wait for both format requests to complete
12258 cx.executor().advance_clock(Duration::from_millis(200));
12259 cx.executor().start_waiting();
12260 format_1.await.unwrap();
12261 cx.executor().start_waiting();
12262 format_2.await.unwrap();
12263
12264 // The formatting edits only happens once.
12265 cx.assert_editor_state(indoc! {"
12266 one
12267 .twoˇ
12268 "});
12269}
12270
12271#[gpui::test]
12272async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12273 init_test(cx, |settings| {
12274 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12275 });
12276
12277 let mut cx = EditorLspTestContext::new_rust(
12278 lsp::ServerCapabilities {
12279 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12280 ..Default::default()
12281 },
12282 cx,
12283 )
12284 .await;
12285
12286 // Set up a buffer white some trailing whitespace and no trailing newline.
12287 cx.set_state(
12288 &[
12289 "one ", //
12290 "twoˇ", //
12291 "three ", //
12292 "four", //
12293 ]
12294 .join("\n"),
12295 );
12296
12297 // Submit a format request.
12298 let format = cx
12299 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12300 .unwrap();
12301
12302 // Record which buffer changes have been sent to the language server
12303 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12304 cx.lsp
12305 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12306 let buffer_changes = buffer_changes.clone();
12307 move |params, _| {
12308 buffer_changes.lock().extend(
12309 params
12310 .content_changes
12311 .into_iter()
12312 .map(|e| (e.range.unwrap(), e.text)),
12313 );
12314 }
12315 });
12316
12317 // Handle formatting requests to the language server.
12318 cx.lsp
12319 .set_request_handler::<lsp::request::Formatting, _, _>({
12320 let buffer_changes = buffer_changes.clone();
12321 move |_, _| {
12322 // When formatting is requested, trailing whitespace has already been stripped,
12323 // and the trailing newline has already been added.
12324 assert_eq!(
12325 &buffer_changes.lock()[1..],
12326 &[
12327 (
12328 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12329 "".into()
12330 ),
12331 (
12332 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12333 "".into()
12334 ),
12335 (
12336 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12337 "\n".into()
12338 ),
12339 ]
12340 );
12341
12342 // Insert blank lines between each line of the buffer.
12343 async move {
12344 Ok(Some(vec![
12345 lsp::TextEdit {
12346 range: lsp::Range::new(
12347 lsp::Position::new(1, 0),
12348 lsp::Position::new(1, 0),
12349 ),
12350 new_text: "\n".into(),
12351 },
12352 lsp::TextEdit {
12353 range: lsp::Range::new(
12354 lsp::Position::new(2, 0),
12355 lsp::Position::new(2, 0),
12356 ),
12357 new_text: "\n".into(),
12358 },
12359 ]))
12360 }
12361 }
12362 });
12363
12364 // After formatting the buffer, the trailing whitespace is stripped,
12365 // a newline is appended, and the edits provided by the language server
12366 // have been applied.
12367 format.await.unwrap();
12368 cx.assert_editor_state(
12369 &[
12370 "one", //
12371 "", //
12372 "twoˇ", //
12373 "", //
12374 "three", //
12375 "four", //
12376 "", //
12377 ]
12378 .join("\n"),
12379 );
12380
12381 // Undoing the formatting undoes the trailing whitespace removal, the
12382 // trailing newline, and the LSP edits.
12383 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12384 cx.assert_editor_state(
12385 &[
12386 "one ", //
12387 "twoˇ", //
12388 "three ", //
12389 "four", //
12390 ]
12391 .join("\n"),
12392 );
12393}
12394
12395#[gpui::test]
12396async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12397 cx: &mut TestAppContext,
12398) {
12399 init_test(cx, |_| {});
12400
12401 cx.update(|cx| {
12402 cx.update_global::<SettingsStore, _>(|settings, cx| {
12403 settings.update_user_settings(cx, |settings| {
12404 settings.editor.auto_signature_help = Some(true);
12405 });
12406 });
12407 });
12408
12409 let mut cx = EditorLspTestContext::new_rust(
12410 lsp::ServerCapabilities {
12411 signature_help_provider: Some(lsp::SignatureHelpOptions {
12412 ..Default::default()
12413 }),
12414 ..Default::default()
12415 },
12416 cx,
12417 )
12418 .await;
12419
12420 let language = Language::new(
12421 LanguageConfig {
12422 name: "Rust".into(),
12423 brackets: BracketPairConfig {
12424 pairs: vec![
12425 BracketPair {
12426 start: "{".to_string(),
12427 end: "}".to_string(),
12428 close: true,
12429 surround: true,
12430 newline: true,
12431 },
12432 BracketPair {
12433 start: "(".to_string(),
12434 end: ")".to_string(),
12435 close: true,
12436 surround: true,
12437 newline: true,
12438 },
12439 BracketPair {
12440 start: "/*".to_string(),
12441 end: " */".to_string(),
12442 close: true,
12443 surround: true,
12444 newline: true,
12445 },
12446 BracketPair {
12447 start: "[".to_string(),
12448 end: "]".to_string(),
12449 close: false,
12450 surround: false,
12451 newline: true,
12452 },
12453 BracketPair {
12454 start: "\"".to_string(),
12455 end: "\"".to_string(),
12456 close: true,
12457 surround: true,
12458 newline: false,
12459 },
12460 BracketPair {
12461 start: "<".to_string(),
12462 end: ">".to_string(),
12463 close: false,
12464 surround: true,
12465 newline: true,
12466 },
12467 ],
12468 ..Default::default()
12469 },
12470 autoclose_before: "})]".to_string(),
12471 ..Default::default()
12472 },
12473 Some(tree_sitter_rust::LANGUAGE.into()),
12474 );
12475 let language = Arc::new(language);
12476
12477 cx.language_registry().add(language.clone());
12478 cx.update_buffer(|buffer, cx| {
12479 buffer.set_language(Some(language), cx);
12480 });
12481
12482 cx.set_state(
12483 &r#"
12484 fn main() {
12485 sampleˇ
12486 }
12487 "#
12488 .unindent(),
12489 );
12490
12491 cx.update_editor(|editor, window, cx| {
12492 editor.handle_input("(", window, cx);
12493 });
12494 cx.assert_editor_state(
12495 &"
12496 fn main() {
12497 sample(ˇ)
12498 }
12499 "
12500 .unindent(),
12501 );
12502
12503 let mocked_response = lsp::SignatureHelp {
12504 signatures: vec![lsp::SignatureInformation {
12505 label: "fn sample(param1: u8, param2: u8)".to_string(),
12506 documentation: None,
12507 parameters: Some(vec![
12508 lsp::ParameterInformation {
12509 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12510 documentation: None,
12511 },
12512 lsp::ParameterInformation {
12513 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12514 documentation: None,
12515 },
12516 ]),
12517 active_parameter: None,
12518 }],
12519 active_signature: Some(0),
12520 active_parameter: Some(0),
12521 };
12522 handle_signature_help_request(&mut cx, mocked_response).await;
12523
12524 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12525 .await;
12526
12527 cx.editor(|editor, _, _| {
12528 let signature_help_state = editor.signature_help_state.popover().cloned();
12529 let signature = signature_help_state.unwrap();
12530 assert_eq!(
12531 signature.signatures[signature.current_signature].label,
12532 "fn sample(param1: u8, param2: u8)"
12533 );
12534 });
12535}
12536
12537#[gpui::test]
12538async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12539 init_test(cx, |_| {});
12540
12541 cx.update(|cx| {
12542 cx.update_global::<SettingsStore, _>(|settings, cx| {
12543 settings.update_user_settings(cx, |settings| {
12544 settings.editor.auto_signature_help = Some(false);
12545 settings.editor.show_signature_help_after_edits = Some(false);
12546 });
12547 });
12548 });
12549
12550 let mut cx = EditorLspTestContext::new_rust(
12551 lsp::ServerCapabilities {
12552 signature_help_provider: Some(lsp::SignatureHelpOptions {
12553 ..Default::default()
12554 }),
12555 ..Default::default()
12556 },
12557 cx,
12558 )
12559 .await;
12560
12561 let language = Language::new(
12562 LanguageConfig {
12563 name: "Rust".into(),
12564 brackets: BracketPairConfig {
12565 pairs: vec![
12566 BracketPair {
12567 start: "{".to_string(),
12568 end: "}".to_string(),
12569 close: true,
12570 surround: true,
12571 newline: true,
12572 },
12573 BracketPair {
12574 start: "(".to_string(),
12575 end: ")".to_string(),
12576 close: true,
12577 surround: true,
12578 newline: true,
12579 },
12580 BracketPair {
12581 start: "/*".to_string(),
12582 end: " */".to_string(),
12583 close: true,
12584 surround: true,
12585 newline: true,
12586 },
12587 BracketPair {
12588 start: "[".to_string(),
12589 end: "]".to_string(),
12590 close: false,
12591 surround: false,
12592 newline: true,
12593 },
12594 BracketPair {
12595 start: "\"".to_string(),
12596 end: "\"".to_string(),
12597 close: true,
12598 surround: true,
12599 newline: false,
12600 },
12601 BracketPair {
12602 start: "<".to_string(),
12603 end: ">".to_string(),
12604 close: false,
12605 surround: true,
12606 newline: true,
12607 },
12608 ],
12609 ..Default::default()
12610 },
12611 autoclose_before: "})]".to_string(),
12612 ..Default::default()
12613 },
12614 Some(tree_sitter_rust::LANGUAGE.into()),
12615 );
12616 let language = Arc::new(language);
12617
12618 cx.language_registry().add(language.clone());
12619 cx.update_buffer(|buffer, cx| {
12620 buffer.set_language(Some(language), cx);
12621 });
12622
12623 // Ensure that signature_help is not called when no signature help is enabled.
12624 cx.set_state(
12625 &r#"
12626 fn main() {
12627 sampleˇ
12628 }
12629 "#
12630 .unindent(),
12631 );
12632 cx.update_editor(|editor, window, cx| {
12633 editor.handle_input("(", window, cx);
12634 });
12635 cx.assert_editor_state(
12636 &"
12637 fn main() {
12638 sample(ˇ)
12639 }
12640 "
12641 .unindent(),
12642 );
12643 cx.editor(|editor, _, _| {
12644 assert!(editor.signature_help_state.task().is_none());
12645 });
12646
12647 let mocked_response = lsp::SignatureHelp {
12648 signatures: vec![lsp::SignatureInformation {
12649 label: "fn sample(param1: u8, param2: u8)".to_string(),
12650 documentation: None,
12651 parameters: Some(vec![
12652 lsp::ParameterInformation {
12653 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12654 documentation: None,
12655 },
12656 lsp::ParameterInformation {
12657 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12658 documentation: None,
12659 },
12660 ]),
12661 active_parameter: None,
12662 }],
12663 active_signature: Some(0),
12664 active_parameter: Some(0),
12665 };
12666
12667 // Ensure that signature_help is called when enabled afte edits
12668 cx.update(|_, cx| {
12669 cx.update_global::<SettingsStore, _>(|settings, cx| {
12670 settings.update_user_settings(cx, |settings| {
12671 settings.editor.auto_signature_help = Some(false);
12672 settings.editor.show_signature_help_after_edits = Some(true);
12673 });
12674 });
12675 });
12676 cx.set_state(
12677 &r#"
12678 fn main() {
12679 sampleˇ
12680 }
12681 "#
12682 .unindent(),
12683 );
12684 cx.update_editor(|editor, window, cx| {
12685 editor.handle_input("(", window, cx);
12686 });
12687 cx.assert_editor_state(
12688 &"
12689 fn main() {
12690 sample(ˇ)
12691 }
12692 "
12693 .unindent(),
12694 );
12695 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12696 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12697 .await;
12698 cx.update_editor(|editor, _, _| {
12699 let signature_help_state = editor.signature_help_state.popover().cloned();
12700 assert!(signature_help_state.is_some());
12701 let signature = signature_help_state.unwrap();
12702 assert_eq!(
12703 signature.signatures[signature.current_signature].label,
12704 "fn sample(param1: u8, param2: u8)"
12705 );
12706 editor.signature_help_state = SignatureHelpState::default();
12707 });
12708
12709 // Ensure that signature_help is called when auto signature help override is enabled
12710 cx.update(|_, cx| {
12711 cx.update_global::<SettingsStore, _>(|settings, cx| {
12712 settings.update_user_settings(cx, |settings| {
12713 settings.editor.auto_signature_help = Some(true);
12714 settings.editor.show_signature_help_after_edits = Some(false);
12715 });
12716 });
12717 });
12718 cx.set_state(
12719 &r#"
12720 fn main() {
12721 sampleˇ
12722 }
12723 "#
12724 .unindent(),
12725 );
12726 cx.update_editor(|editor, window, cx| {
12727 editor.handle_input("(", window, cx);
12728 });
12729 cx.assert_editor_state(
12730 &"
12731 fn main() {
12732 sample(ˇ)
12733 }
12734 "
12735 .unindent(),
12736 );
12737 handle_signature_help_request(&mut cx, mocked_response).await;
12738 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12739 .await;
12740 cx.editor(|editor, _, _| {
12741 let signature_help_state = editor.signature_help_state.popover().cloned();
12742 assert!(signature_help_state.is_some());
12743 let signature = signature_help_state.unwrap();
12744 assert_eq!(
12745 signature.signatures[signature.current_signature].label,
12746 "fn sample(param1: u8, param2: u8)"
12747 );
12748 });
12749}
12750
12751#[gpui::test]
12752async fn test_signature_help(cx: &mut TestAppContext) {
12753 init_test(cx, |_| {});
12754 cx.update(|cx| {
12755 cx.update_global::<SettingsStore, _>(|settings, cx| {
12756 settings.update_user_settings(cx, |settings| {
12757 settings.editor.auto_signature_help = Some(true);
12758 });
12759 });
12760 });
12761
12762 let mut cx = EditorLspTestContext::new_rust(
12763 lsp::ServerCapabilities {
12764 signature_help_provider: Some(lsp::SignatureHelpOptions {
12765 ..Default::default()
12766 }),
12767 ..Default::default()
12768 },
12769 cx,
12770 )
12771 .await;
12772
12773 // A test that directly calls `show_signature_help`
12774 cx.update_editor(|editor, window, cx| {
12775 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12776 });
12777
12778 let mocked_response = lsp::SignatureHelp {
12779 signatures: vec![lsp::SignatureInformation {
12780 label: "fn sample(param1: u8, param2: u8)".to_string(),
12781 documentation: None,
12782 parameters: Some(vec![
12783 lsp::ParameterInformation {
12784 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12785 documentation: None,
12786 },
12787 lsp::ParameterInformation {
12788 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12789 documentation: None,
12790 },
12791 ]),
12792 active_parameter: None,
12793 }],
12794 active_signature: Some(0),
12795 active_parameter: Some(0),
12796 };
12797 handle_signature_help_request(&mut cx, mocked_response).await;
12798
12799 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12800 .await;
12801
12802 cx.editor(|editor, _, _| {
12803 let signature_help_state = editor.signature_help_state.popover().cloned();
12804 assert!(signature_help_state.is_some());
12805 let signature = signature_help_state.unwrap();
12806 assert_eq!(
12807 signature.signatures[signature.current_signature].label,
12808 "fn sample(param1: u8, param2: u8)"
12809 );
12810 });
12811
12812 // When exiting outside from inside the brackets, `signature_help` is closed.
12813 cx.set_state(indoc! {"
12814 fn main() {
12815 sample(ˇ);
12816 }
12817
12818 fn sample(param1: u8, param2: u8) {}
12819 "});
12820
12821 cx.update_editor(|editor, window, cx| {
12822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12823 s.select_ranges([0..0])
12824 });
12825 });
12826
12827 let mocked_response = lsp::SignatureHelp {
12828 signatures: Vec::new(),
12829 active_signature: None,
12830 active_parameter: None,
12831 };
12832 handle_signature_help_request(&mut cx, mocked_response).await;
12833
12834 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12835 .await;
12836
12837 cx.editor(|editor, _, _| {
12838 assert!(!editor.signature_help_state.is_shown());
12839 });
12840
12841 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12842 cx.set_state(indoc! {"
12843 fn main() {
12844 sample(ˇ);
12845 }
12846
12847 fn sample(param1: u8, param2: u8) {}
12848 "});
12849
12850 let mocked_response = lsp::SignatureHelp {
12851 signatures: vec![lsp::SignatureInformation {
12852 label: "fn sample(param1: u8, param2: u8)".to_string(),
12853 documentation: None,
12854 parameters: Some(vec![
12855 lsp::ParameterInformation {
12856 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12857 documentation: None,
12858 },
12859 lsp::ParameterInformation {
12860 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12861 documentation: None,
12862 },
12863 ]),
12864 active_parameter: None,
12865 }],
12866 active_signature: Some(0),
12867 active_parameter: Some(0),
12868 };
12869 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12870 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12871 .await;
12872 cx.editor(|editor, _, _| {
12873 assert!(editor.signature_help_state.is_shown());
12874 });
12875
12876 // Restore the popover with more parameter input
12877 cx.set_state(indoc! {"
12878 fn main() {
12879 sample(param1, param2ˇ);
12880 }
12881
12882 fn sample(param1: u8, param2: u8) {}
12883 "});
12884
12885 let mocked_response = lsp::SignatureHelp {
12886 signatures: vec![lsp::SignatureInformation {
12887 label: "fn sample(param1: u8, param2: u8)".to_string(),
12888 documentation: None,
12889 parameters: Some(vec![
12890 lsp::ParameterInformation {
12891 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12892 documentation: None,
12893 },
12894 lsp::ParameterInformation {
12895 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12896 documentation: None,
12897 },
12898 ]),
12899 active_parameter: None,
12900 }],
12901 active_signature: Some(0),
12902 active_parameter: Some(1),
12903 };
12904 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12905 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12906 .await;
12907
12908 // When selecting a range, the popover is gone.
12909 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12910 cx.update_editor(|editor, window, cx| {
12911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12912 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12913 })
12914 });
12915 cx.assert_editor_state(indoc! {"
12916 fn main() {
12917 sample(param1, «ˇparam2»);
12918 }
12919
12920 fn sample(param1: u8, param2: u8) {}
12921 "});
12922 cx.editor(|editor, _, _| {
12923 assert!(!editor.signature_help_state.is_shown());
12924 });
12925
12926 // When unselecting again, the popover is back if within the brackets.
12927 cx.update_editor(|editor, window, cx| {
12928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12929 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12930 })
12931 });
12932 cx.assert_editor_state(indoc! {"
12933 fn main() {
12934 sample(param1, ˇparam2);
12935 }
12936
12937 fn sample(param1: u8, param2: u8) {}
12938 "});
12939 handle_signature_help_request(&mut cx, mocked_response).await;
12940 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12941 .await;
12942 cx.editor(|editor, _, _| {
12943 assert!(editor.signature_help_state.is_shown());
12944 });
12945
12946 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12947 cx.update_editor(|editor, window, cx| {
12948 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12949 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12950 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12951 })
12952 });
12953 cx.assert_editor_state(indoc! {"
12954 fn main() {
12955 sample(param1, ˇparam2);
12956 }
12957
12958 fn sample(param1: u8, param2: u8) {}
12959 "});
12960
12961 let mocked_response = lsp::SignatureHelp {
12962 signatures: vec![lsp::SignatureInformation {
12963 label: "fn sample(param1: u8, param2: u8)".to_string(),
12964 documentation: None,
12965 parameters: Some(vec![
12966 lsp::ParameterInformation {
12967 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12968 documentation: None,
12969 },
12970 lsp::ParameterInformation {
12971 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12972 documentation: None,
12973 },
12974 ]),
12975 active_parameter: None,
12976 }],
12977 active_signature: Some(0),
12978 active_parameter: Some(1),
12979 };
12980 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12981 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12982 .await;
12983 cx.update_editor(|editor, _, cx| {
12984 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12985 });
12986 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12987 .await;
12988 cx.update_editor(|editor, window, cx| {
12989 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12990 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12991 })
12992 });
12993 cx.assert_editor_state(indoc! {"
12994 fn main() {
12995 sample(param1, «ˇparam2»);
12996 }
12997
12998 fn sample(param1: u8, param2: u8) {}
12999 "});
13000 cx.update_editor(|editor, window, cx| {
13001 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13002 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13003 })
13004 });
13005 cx.assert_editor_state(indoc! {"
13006 fn main() {
13007 sample(param1, ˇparam2);
13008 }
13009
13010 fn sample(param1: u8, param2: u8) {}
13011 "});
13012 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13013 .await;
13014}
13015
13016#[gpui::test]
13017async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13018 init_test(cx, |_| {});
13019
13020 let mut cx = EditorLspTestContext::new_rust(
13021 lsp::ServerCapabilities {
13022 signature_help_provider: Some(lsp::SignatureHelpOptions {
13023 ..Default::default()
13024 }),
13025 ..Default::default()
13026 },
13027 cx,
13028 )
13029 .await;
13030
13031 cx.set_state(indoc! {"
13032 fn main() {
13033 overloadedˇ
13034 }
13035 "});
13036
13037 cx.update_editor(|editor, window, cx| {
13038 editor.handle_input("(", window, cx);
13039 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13040 });
13041
13042 // Mock response with 3 signatures
13043 let mocked_response = lsp::SignatureHelp {
13044 signatures: vec![
13045 lsp::SignatureInformation {
13046 label: "fn overloaded(x: i32)".to_string(),
13047 documentation: None,
13048 parameters: Some(vec![lsp::ParameterInformation {
13049 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13050 documentation: None,
13051 }]),
13052 active_parameter: None,
13053 },
13054 lsp::SignatureInformation {
13055 label: "fn overloaded(x: i32, y: i32)".to_string(),
13056 documentation: None,
13057 parameters: Some(vec![
13058 lsp::ParameterInformation {
13059 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13060 documentation: None,
13061 },
13062 lsp::ParameterInformation {
13063 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13064 documentation: None,
13065 },
13066 ]),
13067 active_parameter: None,
13068 },
13069 lsp::SignatureInformation {
13070 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13071 documentation: None,
13072 parameters: Some(vec![
13073 lsp::ParameterInformation {
13074 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13075 documentation: None,
13076 },
13077 lsp::ParameterInformation {
13078 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13079 documentation: None,
13080 },
13081 lsp::ParameterInformation {
13082 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13083 documentation: None,
13084 },
13085 ]),
13086 active_parameter: None,
13087 },
13088 ],
13089 active_signature: Some(1),
13090 active_parameter: Some(0),
13091 };
13092 handle_signature_help_request(&mut cx, mocked_response).await;
13093
13094 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13095 .await;
13096
13097 // Verify we have multiple signatures and the right one is selected
13098 cx.editor(|editor, _, _| {
13099 let popover = editor.signature_help_state.popover().cloned().unwrap();
13100 assert_eq!(popover.signatures.len(), 3);
13101 // active_signature was 1, so that should be the current
13102 assert_eq!(popover.current_signature, 1);
13103 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13104 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13105 assert_eq!(
13106 popover.signatures[2].label,
13107 "fn overloaded(x: i32, y: i32, z: i32)"
13108 );
13109 });
13110
13111 // Test navigation functionality
13112 cx.update_editor(|editor, window, cx| {
13113 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13114 });
13115
13116 cx.editor(|editor, _, _| {
13117 let popover = editor.signature_help_state.popover().cloned().unwrap();
13118 assert_eq!(popover.current_signature, 2);
13119 });
13120
13121 // Test wrap around
13122 cx.update_editor(|editor, window, cx| {
13123 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13124 });
13125
13126 cx.editor(|editor, _, _| {
13127 let popover = editor.signature_help_state.popover().cloned().unwrap();
13128 assert_eq!(popover.current_signature, 0);
13129 });
13130
13131 // Test previous navigation
13132 cx.update_editor(|editor, window, cx| {
13133 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13134 });
13135
13136 cx.editor(|editor, _, _| {
13137 let popover = editor.signature_help_state.popover().cloned().unwrap();
13138 assert_eq!(popover.current_signature, 2);
13139 });
13140}
13141
13142#[gpui::test]
13143async fn test_completion_mode(cx: &mut TestAppContext) {
13144 init_test(cx, |_| {});
13145 let mut cx = EditorLspTestContext::new_rust(
13146 lsp::ServerCapabilities {
13147 completion_provider: Some(lsp::CompletionOptions {
13148 resolve_provider: Some(true),
13149 ..Default::default()
13150 }),
13151 ..Default::default()
13152 },
13153 cx,
13154 )
13155 .await;
13156
13157 struct Run {
13158 run_description: &'static str,
13159 initial_state: String,
13160 buffer_marked_text: String,
13161 completion_label: &'static str,
13162 completion_text: &'static str,
13163 expected_with_insert_mode: String,
13164 expected_with_replace_mode: String,
13165 expected_with_replace_subsequence_mode: String,
13166 expected_with_replace_suffix_mode: String,
13167 }
13168
13169 let runs = [
13170 Run {
13171 run_description: "Start of word matches completion text",
13172 initial_state: "before ediˇ after".into(),
13173 buffer_marked_text: "before <edi|> after".into(),
13174 completion_label: "editor",
13175 completion_text: "editor",
13176 expected_with_insert_mode: "before editorˇ after".into(),
13177 expected_with_replace_mode: "before editorˇ after".into(),
13178 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13179 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13180 },
13181 Run {
13182 run_description: "Accept same text at the middle of the word",
13183 initial_state: "before ediˇtor after".into(),
13184 buffer_marked_text: "before <edi|tor> after".into(),
13185 completion_label: "editor",
13186 completion_text: "editor",
13187 expected_with_insert_mode: "before editorˇtor after".into(),
13188 expected_with_replace_mode: "before editorˇ after".into(),
13189 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13190 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13191 },
13192 Run {
13193 run_description: "End of word matches completion text -- cursor at end",
13194 initial_state: "before torˇ after".into(),
13195 buffer_marked_text: "before <tor|> after".into(),
13196 completion_label: "editor",
13197 completion_text: "editor",
13198 expected_with_insert_mode: "before editorˇ after".into(),
13199 expected_with_replace_mode: "before editorˇ after".into(),
13200 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13201 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13202 },
13203 Run {
13204 run_description: "End of word matches completion text -- cursor at start",
13205 initial_state: "before ˇtor after".into(),
13206 buffer_marked_text: "before <|tor> after".into(),
13207 completion_label: "editor",
13208 completion_text: "editor",
13209 expected_with_insert_mode: "before editorˇtor after".into(),
13210 expected_with_replace_mode: "before editorˇ after".into(),
13211 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13212 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13213 },
13214 Run {
13215 run_description: "Prepend text containing whitespace",
13216 initial_state: "pˇfield: bool".into(),
13217 buffer_marked_text: "<p|field>: bool".into(),
13218 completion_label: "pub ",
13219 completion_text: "pub ",
13220 expected_with_insert_mode: "pub ˇfield: bool".into(),
13221 expected_with_replace_mode: "pub ˇ: bool".into(),
13222 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13223 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13224 },
13225 Run {
13226 run_description: "Add element to start of list",
13227 initial_state: "[element_ˇelement_2]".into(),
13228 buffer_marked_text: "[<element_|element_2>]".into(),
13229 completion_label: "element_1",
13230 completion_text: "element_1",
13231 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13232 expected_with_replace_mode: "[element_1ˇ]".into(),
13233 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13234 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13235 },
13236 Run {
13237 run_description: "Add element to start of list -- first and second elements are equal",
13238 initial_state: "[elˇelement]".into(),
13239 buffer_marked_text: "[<el|element>]".into(),
13240 completion_label: "element",
13241 completion_text: "element",
13242 expected_with_insert_mode: "[elementˇelement]".into(),
13243 expected_with_replace_mode: "[elementˇ]".into(),
13244 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13245 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13246 },
13247 Run {
13248 run_description: "Ends with matching suffix",
13249 initial_state: "SubˇError".into(),
13250 buffer_marked_text: "<Sub|Error>".into(),
13251 completion_label: "SubscriptionError",
13252 completion_text: "SubscriptionError",
13253 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13254 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13255 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13256 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13257 },
13258 Run {
13259 run_description: "Suffix is a subsequence -- contiguous",
13260 initial_state: "SubˇErr".into(),
13261 buffer_marked_text: "<Sub|Err>".into(),
13262 completion_label: "SubscriptionError",
13263 completion_text: "SubscriptionError",
13264 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13265 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13266 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13267 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13268 },
13269 Run {
13270 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13271 initial_state: "Suˇscrirr".into(),
13272 buffer_marked_text: "<Su|scrirr>".into(),
13273 completion_label: "SubscriptionError",
13274 completion_text: "SubscriptionError",
13275 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13276 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13277 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13278 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13279 },
13280 Run {
13281 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13282 initial_state: "foo(indˇix)".into(),
13283 buffer_marked_text: "foo(<ind|ix>)".into(),
13284 completion_label: "node_index",
13285 completion_text: "node_index",
13286 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13287 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13288 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13289 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13290 },
13291 Run {
13292 run_description: "Replace range ends before cursor - should extend to cursor",
13293 initial_state: "before editˇo after".into(),
13294 buffer_marked_text: "before <{ed}>it|o after".into(),
13295 completion_label: "editor",
13296 completion_text: "editor",
13297 expected_with_insert_mode: "before editorˇo after".into(),
13298 expected_with_replace_mode: "before editorˇo after".into(),
13299 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13300 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13301 },
13302 Run {
13303 run_description: "Uses label for suffix matching",
13304 initial_state: "before ediˇtor after".into(),
13305 buffer_marked_text: "before <edi|tor> after".into(),
13306 completion_label: "editor",
13307 completion_text: "editor()",
13308 expected_with_insert_mode: "before editor()ˇtor after".into(),
13309 expected_with_replace_mode: "before editor()ˇ after".into(),
13310 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13311 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13312 },
13313 Run {
13314 run_description: "Case insensitive subsequence and suffix matching",
13315 initial_state: "before EDiˇtoR after".into(),
13316 buffer_marked_text: "before <EDi|toR> after".into(),
13317 completion_label: "editor",
13318 completion_text: "editor",
13319 expected_with_insert_mode: "before editorˇtoR after".into(),
13320 expected_with_replace_mode: "before editorˇ after".into(),
13321 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13322 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13323 },
13324 ];
13325
13326 for run in runs {
13327 let run_variations = [
13328 (LspInsertMode::Insert, run.expected_with_insert_mode),
13329 (LspInsertMode::Replace, run.expected_with_replace_mode),
13330 (
13331 LspInsertMode::ReplaceSubsequence,
13332 run.expected_with_replace_subsequence_mode,
13333 ),
13334 (
13335 LspInsertMode::ReplaceSuffix,
13336 run.expected_with_replace_suffix_mode,
13337 ),
13338 ];
13339
13340 for (lsp_insert_mode, expected_text) in run_variations {
13341 eprintln!(
13342 "run = {:?}, mode = {lsp_insert_mode:.?}",
13343 run.run_description,
13344 );
13345
13346 update_test_language_settings(&mut cx, |settings| {
13347 settings.defaults.completions = Some(CompletionSettingsContent {
13348 lsp_insert_mode: Some(lsp_insert_mode),
13349 words: Some(WordsCompletionMode::Disabled),
13350 words_min_length: Some(0),
13351 ..Default::default()
13352 });
13353 });
13354
13355 cx.set_state(&run.initial_state);
13356 cx.update_editor(|editor, window, cx| {
13357 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13358 });
13359
13360 let counter = Arc::new(AtomicUsize::new(0));
13361 handle_completion_request_with_insert_and_replace(
13362 &mut cx,
13363 &run.buffer_marked_text,
13364 vec![(run.completion_label, run.completion_text)],
13365 counter.clone(),
13366 )
13367 .await;
13368 cx.condition(|editor, _| editor.context_menu_visible())
13369 .await;
13370 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13371
13372 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13373 editor
13374 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13375 .unwrap()
13376 });
13377 cx.assert_editor_state(&expected_text);
13378 handle_resolve_completion_request(&mut cx, None).await;
13379 apply_additional_edits.await.unwrap();
13380 }
13381 }
13382}
13383
13384#[gpui::test]
13385async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13386 init_test(cx, |_| {});
13387 let mut cx = EditorLspTestContext::new_rust(
13388 lsp::ServerCapabilities {
13389 completion_provider: Some(lsp::CompletionOptions {
13390 resolve_provider: Some(true),
13391 ..Default::default()
13392 }),
13393 ..Default::default()
13394 },
13395 cx,
13396 )
13397 .await;
13398
13399 let initial_state = "SubˇError";
13400 let buffer_marked_text = "<Sub|Error>";
13401 let completion_text = "SubscriptionError";
13402 let expected_with_insert_mode = "SubscriptionErrorˇError";
13403 let expected_with_replace_mode = "SubscriptionErrorˇ";
13404
13405 update_test_language_settings(&mut cx, |settings| {
13406 settings.defaults.completions = Some(CompletionSettingsContent {
13407 words: Some(WordsCompletionMode::Disabled),
13408 words_min_length: Some(0),
13409 // set the opposite here to ensure that the action is overriding the default behavior
13410 lsp_insert_mode: Some(LspInsertMode::Insert),
13411 ..Default::default()
13412 });
13413 });
13414
13415 cx.set_state(initial_state);
13416 cx.update_editor(|editor, window, cx| {
13417 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13418 });
13419
13420 let counter = Arc::new(AtomicUsize::new(0));
13421 handle_completion_request_with_insert_and_replace(
13422 &mut cx,
13423 buffer_marked_text,
13424 vec![(completion_text, completion_text)],
13425 counter.clone(),
13426 )
13427 .await;
13428 cx.condition(|editor, _| editor.context_menu_visible())
13429 .await;
13430 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13431
13432 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13433 editor
13434 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13435 .unwrap()
13436 });
13437 cx.assert_editor_state(expected_with_replace_mode);
13438 handle_resolve_completion_request(&mut cx, None).await;
13439 apply_additional_edits.await.unwrap();
13440
13441 update_test_language_settings(&mut cx, |settings| {
13442 settings.defaults.completions = Some(CompletionSettingsContent {
13443 words: Some(WordsCompletionMode::Disabled),
13444 words_min_length: Some(0),
13445 // set the opposite here to ensure that the action is overriding the default behavior
13446 lsp_insert_mode: Some(LspInsertMode::Replace),
13447 ..Default::default()
13448 });
13449 });
13450
13451 cx.set_state(initial_state);
13452 cx.update_editor(|editor, window, cx| {
13453 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13454 });
13455 handle_completion_request_with_insert_and_replace(
13456 &mut cx,
13457 buffer_marked_text,
13458 vec![(completion_text, completion_text)],
13459 counter.clone(),
13460 )
13461 .await;
13462 cx.condition(|editor, _| editor.context_menu_visible())
13463 .await;
13464 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13465
13466 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13467 editor
13468 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13469 .unwrap()
13470 });
13471 cx.assert_editor_state(expected_with_insert_mode);
13472 handle_resolve_completion_request(&mut cx, None).await;
13473 apply_additional_edits.await.unwrap();
13474}
13475
13476#[gpui::test]
13477async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13478 init_test(cx, |_| {});
13479 let mut cx = EditorLspTestContext::new_rust(
13480 lsp::ServerCapabilities {
13481 completion_provider: Some(lsp::CompletionOptions {
13482 resolve_provider: Some(true),
13483 ..Default::default()
13484 }),
13485 ..Default::default()
13486 },
13487 cx,
13488 )
13489 .await;
13490
13491 // scenario: surrounding text matches completion text
13492 let completion_text = "to_offset";
13493 let initial_state = indoc! {"
13494 1. buf.to_offˇsuffix
13495 2. buf.to_offˇsuf
13496 3. buf.to_offˇfix
13497 4. buf.to_offˇ
13498 5. into_offˇensive
13499 6. ˇsuffix
13500 7. let ˇ //
13501 8. aaˇzz
13502 9. buf.to_off«zzzzzˇ»suffix
13503 10. buf.«ˇzzzzz»suffix
13504 11. to_off«ˇzzzzz»
13505
13506 buf.to_offˇsuffix // newest cursor
13507 "};
13508 let completion_marked_buffer = indoc! {"
13509 1. buf.to_offsuffix
13510 2. buf.to_offsuf
13511 3. buf.to_offfix
13512 4. buf.to_off
13513 5. into_offensive
13514 6. suffix
13515 7. let //
13516 8. aazz
13517 9. buf.to_offzzzzzsuffix
13518 10. buf.zzzzzsuffix
13519 11. to_offzzzzz
13520
13521 buf.<to_off|suffix> // newest cursor
13522 "};
13523 let expected = indoc! {"
13524 1. buf.to_offsetˇ
13525 2. buf.to_offsetˇsuf
13526 3. buf.to_offsetˇfix
13527 4. buf.to_offsetˇ
13528 5. into_offsetˇensive
13529 6. to_offsetˇsuffix
13530 7. let to_offsetˇ //
13531 8. aato_offsetˇzz
13532 9. buf.to_offsetˇ
13533 10. buf.to_offsetˇsuffix
13534 11. to_offsetˇ
13535
13536 buf.to_offsetˇ // newest cursor
13537 "};
13538 cx.set_state(initial_state);
13539 cx.update_editor(|editor, window, cx| {
13540 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13541 });
13542 handle_completion_request_with_insert_and_replace(
13543 &mut cx,
13544 completion_marked_buffer,
13545 vec![(completion_text, completion_text)],
13546 Arc::new(AtomicUsize::new(0)),
13547 )
13548 .await;
13549 cx.condition(|editor, _| editor.context_menu_visible())
13550 .await;
13551 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13552 editor
13553 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13554 .unwrap()
13555 });
13556 cx.assert_editor_state(expected);
13557 handle_resolve_completion_request(&mut cx, None).await;
13558 apply_additional_edits.await.unwrap();
13559
13560 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13561 let completion_text = "foo_and_bar";
13562 let initial_state = indoc! {"
13563 1. ooanbˇ
13564 2. zooanbˇ
13565 3. ooanbˇz
13566 4. zooanbˇz
13567 5. ooanˇ
13568 6. oanbˇ
13569
13570 ooanbˇ
13571 "};
13572 let completion_marked_buffer = indoc! {"
13573 1. ooanb
13574 2. zooanb
13575 3. ooanbz
13576 4. zooanbz
13577 5. ooan
13578 6. oanb
13579
13580 <ooanb|>
13581 "};
13582 let expected = indoc! {"
13583 1. foo_and_barˇ
13584 2. zfoo_and_barˇ
13585 3. foo_and_barˇz
13586 4. zfoo_and_barˇz
13587 5. ooanfoo_and_barˇ
13588 6. oanbfoo_and_barˇ
13589
13590 foo_and_barˇ
13591 "};
13592 cx.set_state(initial_state);
13593 cx.update_editor(|editor, window, cx| {
13594 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13595 });
13596 handle_completion_request_with_insert_and_replace(
13597 &mut cx,
13598 completion_marked_buffer,
13599 vec![(completion_text, completion_text)],
13600 Arc::new(AtomicUsize::new(0)),
13601 )
13602 .await;
13603 cx.condition(|editor, _| editor.context_menu_visible())
13604 .await;
13605 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13606 editor
13607 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13608 .unwrap()
13609 });
13610 cx.assert_editor_state(expected);
13611 handle_resolve_completion_request(&mut cx, None).await;
13612 apply_additional_edits.await.unwrap();
13613
13614 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13615 // (expects the same as if it was inserted at the end)
13616 let completion_text = "foo_and_bar";
13617 let initial_state = indoc! {"
13618 1. ooˇanb
13619 2. zooˇanb
13620 3. ooˇanbz
13621 4. zooˇanbz
13622
13623 ooˇanb
13624 "};
13625 let completion_marked_buffer = indoc! {"
13626 1. ooanb
13627 2. zooanb
13628 3. ooanbz
13629 4. zooanbz
13630
13631 <oo|anb>
13632 "};
13633 let expected = indoc! {"
13634 1. foo_and_barˇ
13635 2. zfoo_and_barˇ
13636 3. foo_and_barˇz
13637 4. zfoo_and_barˇz
13638
13639 foo_and_barˇ
13640 "};
13641 cx.set_state(initial_state);
13642 cx.update_editor(|editor, window, cx| {
13643 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13644 });
13645 handle_completion_request_with_insert_and_replace(
13646 &mut cx,
13647 completion_marked_buffer,
13648 vec![(completion_text, completion_text)],
13649 Arc::new(AtomicUsize::new(0)),
13650 )
13651 .await;
13652 cx.condition(|editor, _| editor.context_menu_visible())
13653 .await;
13654 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13655 editor
13656 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13657 .unwrap()
13658 });
13659 cx.assert_editor_state(expected);
13660 handle_resolve_completion_request(&mut cx, None).await;
13661 apply_additional_edits.await.unwrap();
13662}
13663
13664// This used to crash
13665#[gpui::test]
13666async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13667 init_test(cx, |_| {});
13668
13669 let buffer_text = indoc! {"
13670 fn main() {
13671 10.satu;
13672
13673 //
13674 // separate cursors so they open in different excerpts (manually reproducible)
13675 //
13676
13677 10.satu20;
13678 }
13679 "};
13680 let multibuffer_text_with_selections = indoc! {"
13681 fn main() {
13682 10.satuˇ;
13683
13684 //
13685
13686 //
13687
13688 10.satuˇ20;
13689 }
13690 "};
13691 let expected_multibuffer = indoc! {"
13692 fn main() {
13693 10.saturating_sub()ˇ;
13694
13695 //
13696
13697 //
13698
13699 10.saturating_sub()ˇ;
13700 }
13701 "};
13702
13703 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13704 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13705
13706 let fs = FakeFs::new(cx.executor());
13707 fs.insert_tree(
13708 path!("/a"),
13709 json!({
13710 "main.rs": buffer_text,
13711 }),
13712 )
13713 .await;
13714
13715 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13716 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13717 language_registry.add(rust_lang());
13718 let mut fake_servers = language_registry.register_fake_lsp(
13719 "Rust",
13720 FakeLspAdapter {
13721 capabilities: lsp::ServerCapabilities {
13722 completion_provider: Some(lsp::CompletionOptions {
13723 resolve_provider: None,
13724 ..lsp::CompletionOptions::default()
13725 }),
13726 ..lsp::ServerCapabilities::default()
13727 },
13728 ..FakeLspAdapter::default()
13729 },
13730 );
13731 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13732 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13733 let buffer = project
13734 .update(cx, |project, cx| {
13735 project.open_local_buffer(path!("/a/main.rs"), cx)
13736 })
13737 .await
13738 .unwrap();
13739
13740 let multi_buffer = cx.new(|cx| {
13741 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13742 multi_buffer.push_excerpts(
13743 buffer.clone(),
13744 [ExcerptRange::new(0..first_excerpt_end)],
13745 cx,
13746 );
13747 multi_buffer.push_excerpts(
13748 buffer.clone(),
13749 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13750 cx,
13751 );
13752 multi_buffer
13753 });
13754
13755 let editor = workspace
13756 .update(cx, |_, window, cx| {
13757 cx.new(|cx| {
13758 Editor::new(
13759 EditorMode::Full {
13760 scale_ui_elements_with_buffer_font_size: false,
13761 show_active_line_background: false,
13762 sized_by_content: false,
13763 },
13764 multi_buffer.clone(),
13765 Some(project.clone()),
13766 window,
13767 cx,
13768 )
13769 })
13770 })
13771 .unwrap();
13772
13773 let pane = workspace
13774 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13775 .unwrap();
13776 pane.update_in(cx, |pane, window, cx| {
13777 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13778 });
13779
13780 let fake_server = fake_servers.next().await.unwrap();
13781
13782 editor.update_in(cx, |editor, window, cx| {
13783 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13784 s.select_ranges([
13785 Point::new(1, 11)..Point::new(1, 11),
13786 Point::new(7, 11)..Point::new(7, 11),
13787 ])
13788 });
13789
13790 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13791 });
13792
13793 editor.update_in(cx, |editor, window, cx| {
13794 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13795 });
13796
13797 fake_server
13798 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13799 let completion_item = lsp::CompletionItem {
13800 label: "saturating_sub()".into(),
13801 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13802 lsp::InsertReplaceEdit {
13803 new_text: "saturating_sub()".to_owned(),
13804 insert: lsp::Range::new(
13805 lsp::Position::new(7, 7),
13806 lsp::Position::new(7, 11),
13807 ),
13808 replace: lsp::Range::new(
13809 lsp::Position::new(7, 7),
13810 lsp::Position::new(7, 13),
13811 ),
13812 },
13813 )),
13814 ..lsp::CompletionItem::default()
13815 };
13816
13817 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13818 })
13819 .next()
13820 .await
13821 .unwrap();
13822
13823 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13824 .await;
13825
13826 editor
13827 .update_in(cx, |editor, window, cx| {
13828 editor
13829 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13830 .unwrap()
13831 })
13832 .await
13833 .unwrap();
13834
13835 editor.update(cx, |editor, cx| {
13836 assert_text_with_selections(editor, expected_multibuffer, cx);
13837 })
13838}
13839
13840#[gpui::test]
13841async fn test_completion(cx: &mut TestAppContext) {
13842 init_test(cx, |_| {});
13843
13844 let mut cx = EditorLspTestContext::new_rust(
13845 lsp::ServerCapabilities {
13846 completion_provider: Some(lsp::CompletionOptions {
13847 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13848 resolve_provider: Some(true),
13849 ..Default::default()
13850 }),
13851 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13852 ..Default::default()
13853 },
13854 cx,
13855 )
13856 .await;
13857 let counter = Arc::new(AtomicUsize::new(0));
13858
13859 cx.set_state(indoc! {"
13860 oneˇ
13861 two
13862 three
13863 "});
13864 cx.simulate_keystroke(".");
13865 handle_completion_request(
13866 indoc! {"
13867 one.|<>
13868 two
13869 three
13870 "},
13871 vec!["first_completion", "second_completion"],
13872 true,
13873 counter.clone(),
13874 &mut cx,
13875 )
13876 .await;
13877 cx.condition(|editor, _| editor.context_menu_visible())
13878 .await;
13879 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13880
13881 let _handler = handle_signature_help_request(
13882 &mut cx,
13883 lsp::SignatureHelp {
13884 signatures: vec![lsp::SignatureInformation {
13885 label: "test signature".to_string(),
13886 documentation: None,
13887 parameters: Some(vec![lsp::ParameterInformation {
13888 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13889 documentation: None,
13890 }]),
13891 active_parameter: None,
13892 }],
13893 active_signature: None,
13894 active_parameter: None,
13895 },
13896 );
13897 cx.update_editor(|editor, window, cx| {
13898 assert!(
13899 !editor.signature_help_state.is_shown(),
13900 "No signature help was called for"
13901 );
13902 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13903 });
13904 cx.run_until_parked();
13905 cx.update_editor(|editor, _, _| {
13906 assert!(
13907 !editor.signature_help_state.is_shown(),
13908 "No signature help should be shown when completions menu is open"
13909 );
13910 });
13911
13912 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13913 editor.context_menu_next(&Default::default(), window, cx);
13914 editor
13915 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13916 .unwrap()
13917 });
13918 cx.assert_editor_state(indoc! {"
13919 one.second_completionˇ
13920 two
13921 three
13922 "});
13923
13924 handle_resolve_completion_request(
13925 &mut cx,
13926 Some(vec![
13927 (
13928 //This overlaps with the primary completion edit which is
13929 //misbehavior from the LSP spec, test that we filter it out
13930 indoc! {"
13931 one.second_ˇcompletion
13932 two
13933 threeˇ
13934 "},
13935 "overlapping additional edit",
13936 ),
13937 (
13938 indoc! {"
13939 one.second_completion
13940 two
13941 threeˇ
13942 "},
13943 "\nadditional edit",
13944 ),
13945 ]),
13946 )
13947 .await;
13948 apply_additional_edits.await.unwrap();
13949 cx.assert_editor_state(indoc! {"
13950 one.second_completionˇ
13951 two
13952 three
13953 additional edit
13954 "});
13955
13956 cx.set_state(indoc! {"
13957 one.second_completion
13958 twoˇ
13959 threeˇ
13960 additional edit
13961 "});
13962 cx.simulate_keystroke(" ");
13963 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13964 cx.simulate_keystroke("s");
13965 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13966
13967 cx.assert_editor_state(indoc! {"
13968 one.second_completion
13969 two sˇ
13970 three sˇ
13971 additional edit
13972 "});
13973 handle_completion_request(
13974 indoc! {"
13975 one.second_completion
13976 two s
13977 three <s|>
13978 additional edit
13979 "},
13980 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13981 true,
13982 counter.clone(),
13983 &mut cx,
13984 )
13985 .await;
13986 cx.condition(|editor, _| editor.context_menu_visible())
13987 .await;
13988 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13989
13990 cx.simulate_keystroke("i");
13991
13992 handle_completion_request(
13993 indoc! {"
13994 one.second_completion
13995 two si
13996 three <si|>
13997 additional edit
13998 "},
13999 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14000 true,
14001 counter.clone(),
14002 &mut cx,
14003 )
14004 .await;
14005 cx.condition(|editor, _| editor.context_menu_visible())
14006 .await;
14007 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14008
14009 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14010 editor
14011 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14012 .unwrap()
14013 });
14014 cx.assert_editor_state(indoc! {"
14015 one.second_completion
14016 two sixth_completionˇ
14017 three sixth_completionˇ
14018 additional edit
14019 "});
14020
14021 apply_additional_edits.await.unwrap();
14022
14023 update_test_language_settings(&mut cx, |settings| {
14024 settings.defaults.show_completions_on_input = Some(false);
14025 });
14026 cx.set_state("editorˇ");
14027 cx.simulate_keystroke(".");
14028 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14029 cx.simulate_keystrokes("c l o");
14030 cx.assert_editor_state("editor.cloˇ");
14031 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14032 cx.update_editor(|editor, window, cx| {
14033 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14034 });
14035 handle_completion_request(
14036 "editor.<clo|>",
14037 vec!["close", "clobber"],
14038 true,
14039 counter.clone(),
14040 &mut cx,
14041 )
14042 .await;
14043 cx.condition(|editor, _| editor.context_menu_visible())
14044 .await;
14045 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14046
14047 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14048 editor
14049 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14050 .unwrap()
14051 });
14052 cx.assert_editor_state("editor.clobberˇ");
14053 handle_resolve_completion_request(&mut cx, None).await;
14054 apply_additional_edits.await.unwrap();
14055}
14056
14057#[gpui::test]
14058async fn test_completion_reuse(cx: &mut TestAppContext) {
14059 init_test(cx, |_| {});
14060
14061 let mut cx = EditorLspTestContext::new_rust(
14062 lsp::ServerCapabilities {
14063 completion_provider: Some(lsp::CompletionOptions {
14064 trigger_characters: Some(vec![".".to_string()]),
14065 ..Default::default()
14066 }),
14067 ..Default::default()
14068 },
14069 cx,
14070 )
14071 .await;
14072
14073 let counter = Arc::new(AtomicUsize::new(0));
14074 cx.set_state("objˇ");
14075 cx.simulate_keystroke(".");
14076
14077 // Initial completion request returns complete results
14078 let is_incomplete = false;
14079 handle_completion_request(
14080 "obj.|<>",
14081 vec!["a", "ab", "abc"],
14082 is_incomplete,
14083 counter.clone(),
14084 &mut cx,
14085 )
14086 .await;
14087 cx.run_until_parked();
14088 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14089 cx.assert_editor_state("obj.ˇ");
14090 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14091
14092 // Type "a" - filters existing completions
14093 cx.simulate_keystroke("a");
14094 cx.run_until_parked();
14095 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14096 cx.assert_editor_state("obj.aˇ");
14097 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14098
14099 // Type "b" - filters existing completions
14100 cx.simulate_keystroke("b");
14101 cx.run_until_parked();
14102 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14103 cx.assert_editor_state("obj.abˇ");
14104 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14105
14106 // Type "c" - filters existing completions
14107 cx.simulate_keystroke("c");
14108 cx.run_until_parked();
14109 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14110 cx.assert_editor_state("obj.abcˇ");
14111 check_displayed_completions(vec!["abc"], &mut cx);
14112
14113 // Backspace to delete "c" - filters existing completions
14114 cx.update_editor(|editor, window, cx| {
14115 editor.backspace(&Backspace, window, cx);
14116 });
14117 cx.run_until_parked();
14118 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14119 cx.assert_editor_state("obj.abˇ");
14120 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14121
14122 // Moving cursor to the left dismisses menu.
14123 cx.update_editor(|editor, window, cx| {
14124 editor.move_left(&MoveLeft, window, cx);
14125 });
14126 cx.run_until_parked();
14127 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14128 cx.assert_editor_state("obj.aˇb");
14129 cx.update_editor(|editor, _, _| {
14130 assert_eq!(editor.context_menu_visible(), false);
14131 });
14132
14133 // Type "b" - new request
14134 cx.simulate_keystroke("b");
14135 let is_incomplete = false;
14136 handle_completion_request(
14137 "obj.<ab|>a",
14138 vec!["ab", "abc"],
14139 is_incomplete,
14140 counter.clone(),
14141 &mut cx,
14142 )
14143 .await;
14144 cx.run_until_parked();
14145 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14146 cx.assert_editor_state("obj.abˇb");
14147 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14148
14149 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14150 cx.update_editor(|editor, window, cx| {
14151 editor.backspace(&Backspace, window, cx);
14152 });
14153 let is_incomplete = false;
14154 handle_completion_request(
14155 "obj.<a|>b",
14156 vec!["a", "ab", "abc"],
14157 is_incomplete,
14158 counter.clone(),
14159 &mut cx,
14160 )
14161 .await;
14162 cx.run_until_parked();
14163 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14164 cx.assert_editor_state("obj.aˇb");
14165 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14166
14167 // Backspace to delete "a" - dismisses menu.
14168 cx.update_editor(|editor, window, cx| {
14169 editor.backspace(&Backspace, window, cx);
14170 });
14171 cx.run_until_parked();
14172 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14173 cx.assert_editor_state("obj.ˇb");
14174 cx.update_editor(|editor, _, _| {
14175 assert_eq!(editor.context_menu_visible(), false);
14176 });
14177}
14178
14179#[gpui::test]
14180async fn test_word_completion(cx: &mut TestAppContext) {
14181 let lsp_fetch_timeout_ms = 10;
14182 init_test(cx, |language_settings| {
14183 language_settings.defaults.completions = Some(CompletionSettingsContent {
14184 words_min_length: Some(0),
14185 lsp_fetch_timeout_ms: Some(10),
14186 lsp_insert_mode: Some(LspInsertMode::Insert),
14187 ..Default::default()
14188 });
14189 });
14190
14191 let mut cx = EditorLspTestContext::new_rust(
14192 lsp::ServerCapabilities {
14193 completion_provider: Some(lsp::CompletionOptions {
14194 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14195 ..lsp::CompletionOptions::default()
14196 }),
14197 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14198 ..lsp::ServerCapabilities::default()
14199 },
14200 cx,
14201 )
14202 .await;
14203
14204 let throttle_completions = Arc::new(AtomicBool::new(false));
14205
14206 let lsp_throttle_completions = throttle_completions.clone();
14207 let _completion_requests_handler =
14208 cx.lsp
14209 .server
14210 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14211 let lsp_throttle_completions = lsp_throttle_completions.clone();
14212 let cx = cx.clone();
14213 async move {
14214 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14215 cx.background_executor()
14216 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14217 .await;
14218 }
14219 Ok(Some(lsp::CompletionResponse::Array(vec![
14220 lsp::CompletionItem {
14221 label: "first".into(),
14222 ..lsp::CompletionItem::default()
14223 },
14224 lsp::CompletionItem {
14225 label: "last".into(),
14226 ..lsp::CompletionItem::default()
14227 },
14228 ])))
14229 }
14230 });
14231
14232 cx.set_state(indoc! {"
14233 oneˇ
14234 two
14235 three
14236 "});
14237 cx.simulate_keystroke(".");
14238 cx.executor().run_until_parked();
14239 cx.condition(|editor, _| editor.context_menu_visible())
14240 .await;
14241 cx.update_editor(|editor, window, cx| {
14242 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14243 {
14244 assert_eq!(
14245 completion_menu_entries(menu),
14246 &["first", "last"],
14247 "When LSP server is fast to reply, no fallback word completions are used"
14248 );
14249 } else {
14250 panic!("expected completion menu to be open");
14251 }
14252 editor.cancel(&Cancel, window, cx);
14253 });
14254 cx.executor().run_until_parked();
14255 cx.condition(|editor, _| !editor.context_menu_visible())
14256 .await;
14257
14258 throttle_completions.store(true, atomic::Ordering::Release);
14259 cx.simulate_keystroke(".");
14260 cx.executor()
14261 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14262 cx.executor().run_until_parked();
14263 cx.condition(|editor, _| editor.context_menu_visible())
14264 .await;
14265 cx.update_editor(|editor, _, _| {
14266 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14267 {
14268 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14269 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14270 } else {
14271 panic!("expected completion menu to be open");
14272 }
14273 });
14274}
14275
14276#[gpui::test]
14277async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14278 init_test(cx, |language_settings| {
14279 language_settings.defaults.completions = Some(CompletionSettingsContent {
14280 words: Some(WordsCompletionMode::Enabled),
14281 words_min_length: Some(0),
14282 lsp_insert_mode: Some(LspInsertMode::Insert),
14283 ..Default::default()
14284 });
14285 });
14286
14287 let mut cx = EditorLspTestContext::new_rust(
14288 lsp::ServerCapabilities {
14289 completion_provider: Some(lsp::CompletionOptions {
14290 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14291 ..lsp::CompletionOptions::default()
14292 }),
14293 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14294 ..lsp::ServerCapabilities::default()
14295 },
14296 cx,
14297 )
14298 .await;
14299
14300 let _completion_requests_handler =
14301 cx.lsp
14302 .server
14303 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14304 Ok(Some(lsp::CompletionResponse::Array(vec![
14305 lsp::CompletionItem {
14306 label: "first".into(),
14307 ..lsp::CompletionItem::default()
14308 },
14309 lsp::CompletionItem {
14310 label: "last".into(),
14311 ..lsp::CompletionItem::default()
14312 },
14313 ])))
14314 });
14315
14316 cx.set_state(indoc! {"ˇ
14317 first
14318 last
14319 second
14320 "});
14321 cx.simulate_keystroke(".");
14322 cx.executor().run_until_parked();
14323 cx.condition(|editor, _| editor.context_menu_visible())
14324 .await;
14325 cx.update_editor(|editor, _, _| {
14326 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14327 {
14328 assert_eq!(
14329 completion_menu_entries(menu),
14330 &["first", "last", "second"],
14331 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14332 );
14333 } else {
14334 panic!("expected completion menu to be open");
14335 }
14336 });
14337}
14338
14339#[gpui::test]
14340async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14341 init_test(cx, |language_settings| {
14342 language_settings.defaults.completions = Some(CompletionSettingsContent {
14343 words: Some(WordsCompletionMode::Disabled),
14344 words_min_length: Some(0),
14345 lsp_insert_mode: Some(LspInsertMode::Insert),
14346 ..Default::default()
14347 });
14348 });
14349
14350 let mut cx = EditorLspTestContext::new_rust(
14351 lsp::ServerCapabilities {
14352 completion_provider: Some(lsp::CompletionOptions {
14353 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14354 ..lsp::CompletionOptions::default()
14355 }),
14356 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14357 ..lsp::ServerCapabilities::default()
14358 },
14359 cx,
14360 )
14361 .await;
14362
14363 let _completion_requests_handler =
14364 cx.lsp
14365 .server
14366 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14367 panic!("LSP completions should not be queried when dealing with word completions")
14368 });
14369
14370 cx.set_state(indoc! {"ˇ
14371 first
14372 last
14373 second
14374 "});
14375 cx.update_editor(|editor, window, cx| {
14376 editor.show_word_completions(&ShowWordCompletions, window, cx);
14377 });
14378 cx.executor().run_until_parked();
14379 cx.condition(|editor, _| editor.context_menu_visible())
14380 .await;
14381 cx.update_editor(|editor, _, _| {
14382 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14383 {
14384 assert_eq!(
14385 completion_menu_entries(menu),
14386 &["first", "last", "second"],
14387 "`ShowWordCompletions` action should show word completions"
14388 );
14389 } else {
14390 panic!("expected completion menu to be open");
14391 }
14392 });
14393
14394 cx.simulate_keystroke("l");
14395 cx.executor().run_until_parked();
14396 cx.condition(|editor, _| editor.context_menu_visible())
14397 .await;
14398 cx.update_editor(|editor, _, _| {
14399 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14400 {
14401 assert_eq!(
14402 completion_menu_entries(menu),
14403 &["last"],
14404 "After showing word completions, further editing should filter them and not query the LSP"
14405 );
14406 } else {
14407 panic!("expected completion menu to be open");
14408 }
14409 });
14410}
14411
14412#[gpui::test]
14413async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14414 init_test(cx, |language_settings| {
14415 language_settings.defaults.completions = Some(CompletionSettingsContent {
14416 words_min_length: Some(0),
14417 lsp: Some(false),
14418 lsp_insert_mode: Some(LspInsertMode::Insert),
14419 ..Default::default()
14420 });
14421 });
14422
14423 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14424
14425 cx.set_state(indoc! {"ˇ
14426 0_usize
14427 let
14428 33
14429 4.5f32
14430 "});
14431 cx.update_editor(|editor, window, cx| {
14432 editor.show_completions(&ShowCompletions::default(), window, cx);
14433 });
14434 cx.executor().run_until_parked();
14435 cx.condition(|editor, _| editor.context_menu_visible())
14436 .await;
14437 cx.update_editor(|editor, window, cx| {
14438 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14439 {
14440 assert_eq!(
14441 completion_menu_entries(menu),
14442 &["let"],
14443 "With no digits in the completion query, no digits should be in the word completions"
14444 );
14445 } else {
14446 panic!("expected completion menu to be open");
14447 }
14448 editor.cancel(&Cancel, window, cx);
14449 });
14450
14451 cx.set_state(indoc! {"3ˇ
14452 0_usize
14453 let
14454 3
14455 33.35f32
14456 "});
14457 cx.update_editor(|editor, window, cx| {
14458 editor.show_completions(&ShowCompletions::default(), window, cx);
14459 });
14460 cx.executor().run_until_parked();
14461 cx.condition(|editor, _| editor.context_menu_visible())
14462 .await;
14463 cx.update_editor(|editor, _, _| {
14464 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14465 {
14466 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14467 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14468 } else {
14469 panic!("expected completion menu to be open");
14470 }
14471 });
14472}
14473
14474#[gpui::test]
14475async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14476 init_test(cx, |language_settings| {
14477 language_settings.defaults.completions = Some(CompletionSettingsContent {
14478 words: Some(WordsCompletionMode::Enabled),
14479 words_min_length: Some(3),
14480 lsp_insert_mode: Some(LspInsertMode::Insert),
14481 ..Default::default()
14482 });
14483 });
14484
14485 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14486 cx.set_state(indoc! {"ˇ
14487 wow
14488 wowen
14489 wowser
14490 "});
14491 cx.simulate_keystroke("w");
14492 cx.executor().run_until_parked();
14493 cx.update_editor(|editor, _, _| {
14494 if editor.context_menu.borrow_mut().is_some() {
14495 panic!(
14496 "expected completion menu to be hidden, as words completion threshold is not met"
14497 );
14498 }
14499 });
14500
14501 cx.update_editor(|editor, window, cx| {
14502 editor.show_word_completions(&ShowWordCompletions, window, cx);
14503 });
14504 cx.executor().run_until_parked();
14505 cx.update_editor(|editor, window, cx| {
14506 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14507 {
14508 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14509 } else {
14510 panic!("expected completion menu to be open after the word completions are called with an action");
14511 }
14512
14513 editor.cancel(&Cancel, window, cx);
14514 });
14515 cx.update_editor(|editor, _, _| {
14516 if editor.context_menu.borrow_mut().is_some() {
14517 panic!("expected completion menu to be hidden after canceling");
14518 }
14519 });
14520
14521 cx.simulate_keystroke("o");
14522 cx.executor().run_until_parked();
14523 cx.update_editor(|editor, _, _| {
14524 if editor.context_menu.borrow_mut().is_some() {
14525 panic!(
14526 "expected completion menu to be hidden, as words completion threshold is not met still"
14527 );
14528 }
14529 });
14530
14531 cx.simulate_keystroke("w");
14532 cx.executor().run_until_parked();
14533 cx.update_editor(|editor, _, _| {
14534 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14535 {
14536 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14537 } else {
14538 panic!("expected completion menu to be open after the word completions threshold is met");
14539 }
14540 });
14541}
14542
14543#[gpui::test]
14544async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14545 init_test(cx, |language_settings| {
14546 language_settings.defaults.completions = Some(CompletionSettingsContent {
14547 words: Some(WordsCompletionMode::Enabled),
14548 words_min_length: Some(0),
14549 lsp_insert_mode: Some(LspInsertMode::Insert),
14550 ..Default::default()
14551 });
14552 });
14553
14554 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14555 cx.update_editor(|editor, _, _| {
14556 editor.disable_word_completions();
14557 });
14558 cx.set_state(indoc! {"ˇ
14559 wow
14560 wowen
14561 wowser
14562 "});
14563 cx.simulate_keystroke("w");
14564 cx.executor().run_until_parked();
14565 cx.update_editor(|editor, _, _| {
14566 if editor.context_menu.borrow_mut().is_some() {
14567 panic!(
14568 "expected completion menu to be hidden, as words completion are disabled for this editor"
14569 );
14570 }
14571 });
14572
14573 cx.update_editor(|editor, window, cx| {
14574 editor.show_word_completions(&ShowWordCompletions, window, cx);
14575 });
14576 cx.executor().run_until_parked();
14577 cx.update_editor(|editor, _, _| {
14578 if editor.context_menu.borrow_mut().is_some() {
14579 panic!(
14580 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14581 );
14582 }
14583 });
14584}
14585
14586fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14587 let position = || lsp::Position {
14588 line: params.text_document_position.position.line,
14589 character: params.text_document_position.position.character,
14590 };
14591 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14592 range: lsp::Range {
14593 start: position(),
14594 end: position(),
14595 },
14596 new_text: text.to_string(),
14597 }))
14598}
14599
14600#[gpui::test]
14601async fn test_multiline_completion(cx: &mut TestAppContext) {
14602 init_test(cx, |_| {});
14603
14604 let fs = FakeFs::new(cx.executor());
14605 fs.insert_tree(
14606 path!("/a"),
14607 json!({
14608 "main.ts": "a",
14609 }),
14610 )
14611 .await;
14612
14613 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14614 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14615 let typescript_language = Arc::new(Language::new(
14616 LanguageConfig {
14617 name: "TypeScript".into(),
14618 matcher: LanguageMatcher {
14619 path_suffixes: vec!["ts".to_string()],
14620 ..LanguageMatcher::default()
14621 },
14622 line_comments: vec!["// ".into()],
14623 ..LanguageConfig::default()
14624 },
14625 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14626 ));
14627 language_registry.add(typescript_language.clone());
14628 let mut fake_servers = language_registry.register_fake_lsp(
14629 "TypeScript",
14630 FakeLspAdapter {
14631 capabilities: lsp::ServerCapabilities {
14632 completion_provider: Some(lsp::CompletionOptions {
14633 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14634 ..lsp::CompletionOptions::default()
14635 }),
14636 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14637 ..lsp::ServerCapabilities::default()
14638 },
14639 // Emulate vtsls label generation
14640 label_for_completion: Some(Box::new(|item, _| {
14641 let text = if let Some(description) = item
14642 .label_details
14643 .as_ref()
14644 .and_then(|label_details| label_details.description.as_ref())
14645 {
14646 format!("{} {}", item.label, description)
14647 } else if let Some(detail) = &item.detail {
14648 format!("{} {}", item.label, detail)
14649 } else {
14650 item.label.clone()
14651 };
14652 let len = text.len();
14653 Some(language::CodeLabel {
14654 text,
14655 runs: Vec::new(),
14656 filter_range: 0..len,
14657 })
14658 })),
14659 ..FakeLspAdapter::default()
14660 },
14661 );
14662 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14663 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14664 let worktree_id = workspace
14665 .update(cx, |workspace, _window, cx| {
14666 workspace.project().update(cx, |project, cx| {
14667 project.worktrees(cx).next().unwrap().read(cx).id()
14668 })
14669 })
14670 .unwrap();
14671 let _buffer = project
14672 .update(cx, |project, cx| {
14673 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14674 })
14675 .await
14676 .unwrap();
14677 let editor = workspace
14678 .update(cx, |workspace, window, cx| {
14679 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14680 })
14681 .unwrap()
14682 .await
14683 .unwrap()
14684 .downcast::<Editor>()
14685 .unwrap();
14686 let fake_server = fake_servers.next().await.unwrap();
14687
14688 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14689 let multiline_label_2 = "a\nb\nc\n";
14690 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14691 let multiline_description = "d\ne\nf\n";
14692 let multiline_detail_2 = "g\nh\ni\n";
14693
14694 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14695 move |params, _| async move {
14696 Ok(Some(lsp::CompletionResponse::Array(vec![
14697 lsp::CompletionItem {
14698 label: multiline_label.to_string(),
14699 text_edit: gen_text_edit(¶ms, "new_text_1"),
14700 ..lsp::CompletionItem::default()
14701 },
14702 lsp::CompletionItem {
14703 label: "single line label 1".to_string(),
14704 detail: Some(multiline_detail.to_string()),
14705 text_edit: gen_text_edit(¶ms, "new_text_2"),
14706 ..lsp::CompletionItem::default()
14707 },
14708 lsp::CompletionItem {
14709 label: "single line label 2".to_string(),
14710 label_details: Some(lsp::CompletionItemLabelDetails {
14711 description: Some(multiline_description.to_string()),
14712 detail: None,
14713 }),
14714 text_edit: gen_text_edit(¶ms, "new_text_2"),
14715 ..lsp::CompletionItem::default()
14716 },
14717 lsp::CompletionItem {
14718 label: multiline_label_2.to_string(),
14719 detail: Some(multiline_detail_2.to_string()),
14720 text_edit: gen_text_edit(¶ms, "new_text_3"),
14721 ..lsp::CompletionItem::default()
14722 },
14723 lsp::CompletionItem {
14724 label: "Label with many spaces and \t but without newlines".to_string(),
14725 detail: Some(
14726 "Details with many spaces and \t but without newlines".to_string(),
14727 ),
14728 text_edit: gen_text_edit(¶ms, "new_text_4"),
14729 ..lsp::CompletionItem::default()
14730 },
14731 ])))
14732 },
14733 );
14734
14735 editor.update_in(cx, |editor, window, cx| {
14736 cx.focus_self(window);
14737 editor.move_to_end(&MoveToEnd, window, cx);
14738 editor.handle_input(".", window, cx);
14739 });
14740 cx.run_until_parked();
14741 completion_handle.next().await.unwrap();
14742
14743 editor.update(cx, |editor, _| {
14744 assert!(editor.context_menu_visible());
14745 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14746 {
14747 let completion_labels = menu
14748 .completions
14749 .borrow()
14750 .iter()
14751 .map(|c| c.label.text.clone())
14752 .collect::<Vec<_>>();
14753 assert_eq!(
14754 completion_labels,
14755 &[
14756 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14757 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14758 "single line label 2 d e f ",
14759 "a b c g h i ",
14760 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14761 ],
14762 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14763 );
14764
14765 for completion in menu
14766 .completions
14767 .borrow()
14768 .iter() {
14769 assert_eq!(
14770 completion.label.filter_range,
14771 0..completion.label.text.len(),
14772 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14773 );
14774 }
14775 } else {
14776 panic!("expected completion menu to be open");
14777 }
14778 });
14779}
14780
14781#[gpui::test]
14782async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14783 init_test(cx, |_| {});
14784 let mut cx = EditorLspTestContext::new_rust(
14785 lsp::ServerCapabilities {
14786 completion_provider: Some(lsp::CompletionOptions {
14787 trigger_characters: Some(vec![".".to_string()]),
14788 ..Default::default()
14789 }),
14790 ..Default::default()
14791 },
14792 cx,
14793 )
14794 .await;
14795 cx.lsp
14796 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14797 Ok(Some(lsp::CompletionResponse::Array(vec![
14798 lsp::CompletionItem {
14799 label: "first".into(),
14800 ..Default::default()
14801 },
14802 lsp::CompletionItem {
14803 label: "last".into(),
14804 ..Default::default()
14805 },
14806 ])))
14807 });
14808 cx.set_state("variableˇ");
14809 cx.simulate_keystroke(".");
14810 cx.executor().run_until_parked();
14811
14812 cx.update_editor(|editor, _, _| {
14813 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14814 {
14815 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14816 } else {
14817 panic!("expected completion menu to be open");
14818 }
14819 });
14820
14821 cx.update_editor(|editor, window, cx| {
14822 editor.move_page_down(&MovePageDown::default(), window, cx);
14823 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14824 {
14825 assert!(
14826 menu.selected_item == 1,
14827 "expected PageDown to select the last item from the context menu"
14828 );
14829 } else {
14830 panic!("expected completion menu to stay open after PageDown");
14831 }
14832 });
14833
14834 cx.update_editor(|editor, window, cx| {
14835 editor.move_page_up(&MovePageUp::default(), window, cx);
14836 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14837 {
14838 assert!(
14839 menu.selected_item == 0,
14840 "expected PageUp to select the first item from the context menu"
14841 );
14842 } else {
14843 panic!("expected completion menu to stay open after PageUp");
14844 }
14845 });
14846}
14847
14848#[gpui::test]
14849async fn test_as_is_completions(cx: &mut TestAppContext) {
14850 init_test(cx, |_| {});
14851 let mut cx = EditorLspTestContext::new_rust(
14852 lsp::ServerCapabilities {
14853 completion_provider: Some(lsp::CompletionOptions {
14854 ..Default::default()
14855 }),
14856 ..Default::default()
14857 },
14858 cx,
14859 )
14860 .await;
14861 cx.lsp
14862 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14863 Ok(Some(lsp::CompletionResponse::Array(vec![
14864 lsp::CompletionItem {
14865 label: "unsafe".into(),
14866 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14867 range: lsp::Range {
14868 start: lsp::Position {
14869 line: 1,
14870 character: 2,
14871 },
14872 end: lsp::Position {
14873 line: 1,
14874 character: 3,
14875 },
14876 },
14877 new_text: "unsafe".to_string(),
14878 })),
14879 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14880 ..Default::default()
14881 },
14882 ])))
14883 });
14884 cx.set_state("fn a() {}\n nˇ");
14885 cx.executor().run_until_parked();
14886 cx.update_editor(|editor, window, cx| {
14887 editor.show_completions(
14888 &ShowCompletions {
14889 trigger: Some("\n".into()),
14890 },
14891 window,
14892 cx,
14893 );
14894 });
14895 cx.executor().run_until_parked();
14896
14897 cx.update_editor(|editor, window, cx| {
14898 editor.confirm_completion(&Default::default(), window, cx)
14899 });
14900 cx.executor().run_until_parked();
14901 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14902}
14903
14904#[gpui::test]
14905async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14906 init_test(cx, |_| {});
14907 let language =
14908 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14909 let mut cx = EditorLspTestContext::new(
14910 language,
14911 lsp::ServerCapabilities {
14912 completion_provider: Some(lsp::CompletionOptions {
14913 ..lsp::CompletionOptions::default()
14914 }),
14915 ..lsp::ServerCapabilities::default()
14916 },
14917 cx,
14918 )
14919 .await;
14920
14921 cx.set_state(
14922 "#ifndef BAR_H
14923#define BAR_H
14924
14925#include <stdbool.h>
14926
14927int fn_branch(bool do_branch1, bool do_branch2);
14928
14929#endif // BAR_H
14930ˇ",
14931 );
14932 cx.executor().run_until_parked();
14933 cx.update_editor(|editor, window, cx| {
14934 editor.handle_input("#", window, cx);
14935 });
14936 cx.executor().run_until_parked();
14937 cx.update_editor(|editor, window, cx| {
14938 editor.handle_input("i", window, cx);
14939 });
14940 cx.executor().run_until_parked();
14941 cx.update_editor(|editor, window, cx| {
14942 editor.handle_input("n", window, cx);
14943 });
14944 cx.executor().run_until_parked();
14945 cx.assert_editor_state(
14946 "#ifndef BAR_H
14947#define BAR_H
14948
14949#include <stdbool.h>
14950
14951int fn_branch(bool do_branch1, bool do_branch2);
14952
14953#endif // BAR_H
14954#inˇ",
14955 );
14956
14957 cx.lsp
14958 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14959 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14960 is_incomplete: false,
14961 item_defaults: None,
14962 items: vec![lsp::CompletionItem {
14963 kind: Some(lsp::CompletionItemKind::SNIPPET),
14964 label_details: Some(lsp::CompletionItemLabelDetails {
14965 detail: Some("header".to_string()),
14966 description: None,
14967 }),
14968 label: " include".to_string(),
14969 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14970 range: lsp::Range {
14971 start: lsp::Position {
14972 line: 8,
14973 character: 1,
14974 },
14975 end: lsp::Position {
14976 line: 8,
14977 character: 1,
14978 },
14979 },
14980 new_text: "include \"$0\"".to_string(),
14981 })),
14982 sort_text: Some("40b67681include".to_string()),
14983 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14984 filter_text: Some("include".to_string()),
14985 insert_text: Some("include \"$0\"".to_string()),
14986 ..lsp::CompletionItem::default()
14987 }],
14988 })))
14989 });
14990 cx.update_editor(|editor, window, cx| {
14991 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14992 });
14993 cx.executor().run_until_parked();
14994 cx.update_editor(|editor, window, cx| {
14995 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14996 });
14997 cx.executor().run_until_parked();
14998 cx.assert_editor_state(
14999 "#ifndef BAR_H
15000#define BAR_H
15001
15002#include <stdbool.h>
15003
15004int fn_branch(bool do_branch1, bool do_branch2);
15005
15006#endif // BAR_H
15007#include \"ˇ\"",
15008 );
15009
15010 cx.lsp
15011 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15012 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15013 is_incomplete: true,
15014 item_defaults: None,
15015 items: vec![lsp::CompletionItem {
15016 kind: Some(lsp::CompletionItemKind::FILE),
15017 label: "AGL/".to_string(),
15018 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15019 range: lsp::Range {
15020 start: lsp::Position {
15021 line: 8,
15022 character: 10,
15023 },
15024 end: lsp::Position {
15025 line: 8,
15026 character: 11,
15027 },
15028 },
15029 new_text: "AGL/".to_string(),
15030 })),
15031 sort_text: Some("40b67681AGL/".to_string()),
15032 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15033 filter_text: Some("AGL/".to_string()),
15034 insert_text: Some("AGL/".to_string()),
15035 ..lsp::CompletionItem::default()
15036 }],
15037 })))
15038 });
15039 cx.update_editor(|editor, window, cx| {
15040 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15041 });
15042 cx.executor().run_until_parked();
15043 cx.update_editor(|editor, window, cx| {
15044 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15045 });
15046 cx.executor().run_until_parked();
15047 cx.assert_editor_state(
15048 r##"#ifndef BAR_H
15049#define BAR_H
15050
15051#include <stdbool.h>
15052
15053int fn_branch(bool do_branch1, bool do_branch2);
15054
15055#endif // BAR_H
15056#include "AGL/ˇ"##,
15057 );
15058
15059 cx.update_editor(|editor, window, cx| {
15060 editor.handle_input("\"", window, cx);
15061 });
15062 cx.executor().run_until_parked();
15063 cx.assert_editor_state(
15064 r##"#ifndef BAR_H
15065#define BAR_H
15066
15067#include <stdbool.h>
15068
15069int fn_branch(bool do_branch1, bool do_branch2);
15070
15071#endif // BAR_H
15072#include "AGL/"ˇ"##,
15073 );
15074}
15075
15076#[gpui::test]
15077async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15078 init_test(cx, |_| {});
15079
15080 let mut cx = EditorLspTestContext::new_rust(
15081 lsp::ServerCapabilities {
15082 completion_provider: Some(lsp::CompletionOptions {
15083 trigger_characters: Some(vec![".".to_string()]),
15084 resolve_provider: Some(true),
15085 ..Default::default()
15086 }),
15087 ..Default::default()
15088 },
15089 cx,
15090 )
15091 .await;
15092
15093 cx.set_state("fn main() { let a = 2ˇ; }");
15094 cx.simulate_keystroke(".");
15095 let completion_item = lsp::CompletionItem {
15096 label: "Some".into(),
15097 kind: Some(lsp::CompletionItemKind::SNIPPET),
15098 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15099 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15100 kind: lsp::MarkupKind::Markdown,
15101 value: "```rust\nSome(2)\n```".to_string(),
15102 })),
15103 deprecated: Some(false),
15104 sort_text: Some("Some".to_string()),
15105 filter_text: Some("Some".to_string()),
15106 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15107 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15108 range: lsp::Range {
15109 start: lsp::Position {
15110 line: 0,
15111 character: 22,
15112 },
15113 end: lsp::Position {
15114 line: 0,
15115 character: 22,
15116 },
15117 },
15118 new_text: "Some(2)".to_string(),
15119 })),
15120 additional_text_edits: Some(vec![lsp::TextEdit {
15121 range: lsp::Range {
15122 start: lsp::Position {
15123 line: 0,
15124 character: 20,
15125 },
15126 end: lsp::Position {
15127 line: 0,
15128 character: 22,
15129 },
15130 },
15131 new_text: "".to_string(),
15132 }]),
15133 ..Default::default()
15134 };
15135
15136 let closure_completion_item = completion_item.clone();
15137 let counter = Arc::new(AtomicUsize::new(0));
15138 let counter_clone = counter.clone();
15139 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15140 let task_completion_item = closure_completion_item.clone();
15141 counter_clone.fetch_add(1, atomic::Ordering::Release);
15142 async move {
15143 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15144 is_incomplete: true,
15145 item_defaults: None,
15146 items: vec![task_completion_item],
15147 })))
15148 }
15149 });
15150
15151 cx.condition(|editor, _| editor.context_menu_visible())
15152 .await;
15153 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15154 assert!(request.next().await.is_some());
15155 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15156
15157 cx.simulate_keystrokes("S o m");
15158 cx.condition(|editor, _| editor.context_menu_visible())
15159 .await;
15160 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15161 assert!(request.next().await.is_some());
15162 assert!(request.next().await.is_some());
15163 assert!(request.next().await.is_some());
15164 request.close();
15165 assert!(request.next().await.is_none());
15166 assert_eq!(
15167 counter.load(atomic::Ordering::Acquire),
15168 4,
15169 "With the completions menu open, only one LSP request should happen per input"
15170 );
15171}
15172
15173#[gpui::test]
15174async fn test_toggle_comment(cx: &mut TestAppContext) {
15175 init_test(cx, |_| {});
15176 let mut cx = EditorTestContext::new(cx).await;
15177 let language = Arc::new(Language::new(
15178 LanguageConfig {
15179 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15180 ..Default::default()
15181 },
15182 Some(tree_sitter_rust::LANGUAGE.into()),
15183 ));
15184 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15185
15186 // If multiple selections intersect a line, the line is only toggled once.
15187 cx.set_state(indoc! {"
15188 fn a() {
15189 «//b();
15190 ˇ»// «c();
15191 //ˇ» d();
15192 }
15193 "});
15194
15195 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15196
15197 cx.assert_editor_state(indoc! {"
15198 fn a() {
15199 «b();
15200 c();
15201 ˇ» d();
15202 }
15203 "});
15204
15205 // The comment prefix is inserted at the same column for every line in a
15206 // selection.
15207 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15208
15209 cx.assert_editor_state(indoc! {"
15210 fn a() {
15211 // «b();
15212 // c();
15213 ˇ»// d();
15214 }
15215 "});
15216
15217 // If a selection ends at the beginning of a line, that line is not toggled.
15218 cx.set_selections_state(indoc! {"
15219 fn a() {
15220 // b();
15221 «// c();
15222 ˇ» // d();
15223 }
15224 "});
15225
15226 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15227
15228 cx.assert_editor_state(indoc! {"
15229 fn a() {
15230 // b();
15231 «c();
15232 ˇ» // d();
15233 }
15234 "});
15235
15236 // If a selection span a single line and is empty, the line is toggled.
15237 cx.set_state(indoc! {"
15238 fn a() {
15239 a();
15240 b();
15241 ˇ
15242 }
15243 "});
15244
15245 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15246
15247 cx.assert_editor_state(indoc! {"
15248 fn a() {
15249 a();
15250 b();
15251 //•ˇ
15252 }
15253 "});
15254
15255 // If a selection span multiple lines, empty lines are not toggled.
15256 cx.set_state(indoc! {"
15257 fn a() {
15258 «a();
15259
15260 c();ˇ»
15261 }
15262 "});
15263
15264 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15265
15266 cx.assert_editor_state(indoc! {"
15267 fn a() {
15268 // «a();
15269
15270 // c();ˇ»
15271 }
15272 "});
15273
15274 // If a selection includes multiple comment prefixes, all lines are uncommented.
15275 cx.set_state(indoc! {"
15276 fn a() {
15277 «// a();
15278 /// b();
15279 //! c();ˇ»
15280 }
15281 "});
15282
15283 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15284
15285 cx.assert_editor_state(indoc! {"
15286 fn a() {
15287 «a();
15288 b();
15289 c();ˇ»
15290 }
15291 "});
15292}
15293
15294#[gpui::test]
15295async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15296 init_test(cx, |_| {});
15297 let mut cx = EditorTestContext::new(cx).await;
15298 let language = Arc::new(Language::new(
15299 LanguageConfig {
15300 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15301 ..Default::default()
15302 },
15303 Some(tree_sitter_rust::LANGUAGE.into()),
15304 ));
15305 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15306
15307 let toggle_comments = &ToggleComments {
15308 advance_downwards: false,
15309 ignore_indent: true,
15310 };
15311
15312 // If multiple selections intersect a line, the line is only toggled once.
15313 cx.set_state(indoc! {"
15314 fn a() {
15315 // «b();
15316 // c();
15317 // ˇ» d();
15318 }
15319 "});
15320
15321 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15322
15323 cx.assert_editor_state(indoc! {"
15324 fn a() {
15325 «b();
15326 c();
15327 ˇ» d();
15328 }
15329 "});
15330
15331 // The comment prefix is inserted at the beginning of each line
15332 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15333
15334 cx.assert_editor_state(indoc! {"
15335 fn a() {
15336 // «b();
15337 // c();
15338 // ˇ» d();
15339 }
15340 "});
15341
15342 // If a selection ends at the beginning of a line, that line is not toggled.
15343 cx.set_selections_state(indoc! {"
15344 fn a() {
15345 // b();
15346 // «c();
15347 ˇ»// d();
15348 }
15349 "});
15350
15351 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15352
15353 cx.assert_editor_state(indoc! {"
15354 fn a() {
15355 // b();
15356 «c();
15357 ˇ»// d();
15358 }
15359 "});
15360
15361 // If a selection span a single line and is empty, the line is toggled.
15362 cx.set_state(indoc! {"
15363 fn a() {
15364 a();
15365 b();
15366 ˇ
15367 }
15368 "});
15369
15370 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15371
15372 cx.assert_editor_state(indoc! {"
15373 fn a() {
15374 a();
15375 b();
15376 //ˇ
15377 }
15378 "});
15379
15380 // If a selection span multiple lines, empty lines are not toggled.
15381 cx.set_state(indoc! {"
15382 fn a() {
15383 «a();
15384
15385 c();ˇ»
15386 }
15387 "});
15388
15389 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15390
15391 cx.assert_editor_state(indoc! {"
15392 fn a() {
15393 // «a();
15394
15395 // c();ˇ»
15396 }
15397 "});
15398
15399 // If a selection includes multiple comment prefixes, all lines are uncommented.
15400 cx.set_state(indoc! {"
15401 fn a() {
15402 // «a();
15403 /// b();
15404 //! c();ˇ»
15405 }
15406 "});
15407
15408 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15409
15410 cx.assert_editor_state(indoc! {"
15411 fn a() {
15412 «a();
15413 b();
15414 c();ˇ»
15415 }
15416 "});
15417}
15418
15419#[gpui::test]
15420async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15421 init_test(cx, |_| {});
15422
15423 let language = Arc::new(Language::new(
15424 LanguageConfig {
15425 line_comments: vec!["// ".into()],
15426 ..Default::default()
15427 },
15428 Some(tree_sitter_rust::LANGUAGE.into()),
15429 ));
15430
15431 let mut cx = EditorTestContext::new(cx).await;
15432
15433 cx.language_registry().add(language.clone());
15434 cx.update_buffer(|buffer, cx| {
15435 buffer.set_language(Some(language), cx);
15436 });
15437
15438 let toggle_comments = &ToggleComments {
15439 advance_downwards: true,
15440 ignore_indent: false,
15441 };
15442
15443 // Single cursor on one line -> advance
15444 // Cursor moves horizontally 3 characters as well on non-blank line
15445 cx.set_state(indoc!(
15446 "fn a() {
15447 ˇdog();
15448 cat();
15449 }"
15450 ));
15451 cx.update_editor(|editor, window, cx| {
15452 editor.toggle_comments(toggle_comments, window, cx);
15453 });
15454 cx.assert_editor_state(indoc!(
15455 "fn a() {
15456 // dog();
15457 catˇ();
15458 }"
15459 ));
15460
15461 // Single selection on one line -> don't advance
15462 cx.set_state(indoc!(
15463 "fn a() {
15464 «dog()ˇ»;
15465 cat();
15466 }"
15467 ));
15468 cx.update_editor(|editor, window, cx| {
15469 editor.toggle_comments(toggle_comments, window, cx);
15470 });
15471 cx.assert_editor_state(indoc!(
15472 "fn a() {
15473 // «dog()ˇ»;
15474 cat();
15475 }"
15476 ));
15477
15478 // Multiple cursors on one line -> advance
15479 cx.set_state(indoc!(
15480 "fn a() {
15481 ˇdˇog();
15482 cat();
15483 }"
15484 ));
15485 cx.update_editor(|editor, window, cx| {
15486 editor.toggle_comments(toggle_comments, window, cx);
15487 });
15488 cx.assert_editor_state(indoc!(
15489 "fn a() {
15490 // dog();
15491 catˇ(ˇ);
15492 }"
15493 ));
15494
15495 // Multiple cursors on one line, with selection -> don't advance
15496 cx.set_state(indoc!(
15497 "fn a() {
15498 ˇdˇog«()ˇ»;
15499 cat();
15500 }"
15501 ));
15502 cx.update_editor(|editor, window, cx| {
15503 editor.toggle_comments(toggle_comments, window, cx);
15504 });
15505 cx.assert_editor_state(indoc!(
15506 "fn a() {
15507 // ˇdˇog«()ˇ»;
15508 cat();
15509 }"
15510 ));
15511
15512 // Single cursor on one line -> advance
15513 // Cursor moves to column 0 on blank line
15514 cx.set_state(indoc!(
15515 "fn a() {
15516 ˇdog();
15517
15518 cat();
15519 }"
15520 ));
15521 cx.update_editor(|editor, window, cx| {
15522 editor.toggle_comments(toggle_comments, window, cx);
15523 });
15524 cx.assert_editor_state(indoc!(
15525 "fn a() {
15526 // dog();
15527 ˇ
15528 cat();
15529 }"
15530 ));
15531
15532 // Single cursor on one line -> advance
15533 // Cursor starts and ends at column 0
15534 cx.set_state(indoc!(
15535 "fn a() {
15536 ˇ dog();
15537 cat();
15538 }"
15539 ));
15540 cx.update_editor(|editor, window, cx| {
15541 editor.toggle_comments(toggle_comments, window, cx);
15542 });
15543 cx.assert_editor_state(indoc!(
15544 "fn a() {
15545 // dog();
15546 ˇ cat();
15547 }"
15548 ));
15549}
15550
15551#[gpui::test]
15552async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15553 init_test(cx, |_| {});
15554
15555 let mut cx = EditorTestContext::new(cx).await;
15556
15557 let html_language = Arc::new(
15558 Language::new(
15559 LanguageConfig {
15560 name: "HTML".into(),
15561 block_comment: Some(BlockCommentConfig {
15562 start: "<!-- ".into(),
15563 prefix: "".into(),
15564 end: " -->".into(),
15565 tab_size: 0,
15566 }),
15567 ..Default::default()
15568 },
15569 Some(tree_sitter_html::LANGUAGE.into()),
15570 )
15571 .with_injection_query(
15572 r#"
15573 (script_element
15574 (raw_text) @injection.content
15575 (#set! injection.language "javascript"))
15576 "#,
15577 )
15578 .unwrap(),
15579 );
15580
15581 let javascript_language = Arc::new(Language::new(
15582 LanguageConfig {
15583 name: "JavaScript".into(),
15584 line_comments: vec!["// ".into()],
15585 ..Default::default()
15586 },
15587 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15588 ));
15589
15590 cx.language_registry().add(html_language.clone());
15591 cx.language_registry().add(javascript_language);
15592 cx.update_buffer(|buffer, cx| {
15593 buffer.set_language(Some(html_language), cx);
15594 });
15595
15596 // Toggle comments for empty selections
15597 cx.set_state(
15598 &r#"
15599 <p>A</p>ˇ
15600 <p>B</p>ˇ
15601 <p>C</p>ˇ
15602 "#
15603 .unindent(),
15604 );
15605 cx.update_editor(|editor, window, cx| {
15606 editor.toggle_comments(&ToggleComments::default(), window, cx)
15607 });
15608 cx.assert_editor_state(
15609 &r#"
15610 <!-- <p>A</p>ˇ -->
15611 <!-- <p>B</p>ˇ -->
15612 <!-- <p>C</p>ˇ -->
15613 "#
15614 .unindent(),
15615 );
15616 cx.update_editor(|editor, window, cx| {
15617 editor.toggle_comments(&ToggleComments::default(), window, cx)
15618 });
15619 cx.assert_editor_state(
15620 &r#"
15621 <p>A</p>ˇ
15622 <p>B</p>ˇ
15623 <p>C</p>ˇ
15624 "#
15625 .unindent(),
15626 );
15627
15628 // Toggle comments for mixture of empty and non-empty selections, where
15629 // multiple selections occupy a given line.
15630 cx.set_state(
15631 &r#"
15632 <p>A«</p>
15633 <p>ˇ»B</p>ˇ
15634 <p>C«</p>
15635 <p>ˇ»D</p>ˇ
15636 "#
15637 .unindent(),
15638 );
15639
15640 cx.update_editor(|editor, window, cx| {
15641 editor.toggle_comments(&ToggleComments::default(), window, cx)
15642 });
15643 cx.assert_editor_state(
15644 &r#"
15645 <!-- <p>A«</p>
15646 <p>ˇ»B</p>ˇ -->
15647 <!-- <p>C«</p>
15648 <p>ˇ»D</p>ˇ -->
15649 "#
15650 .unindent(),
15651 );
15652 cx.update_editor(|editor, window, cx| {
15653 editor.toggle_comments(&ToggleComments::default(), window, cx)
15654 });
15655 cx.assert_editor_state(
15656 &r#"
15657 <p>A«</p>
15658 <p>ˇ»B</p>ˇ
15659 <p>C«</p>
15660 <p>ˇ»D</p>ˇ
15661 "#
15662 .unindent(),
15663 );
15664
15665 // Toggle comments when different languages are active for different
15666 // selections.
15667 cx.set_state(
15668 &r#"
15669 ˇ<script>
15670 ˇvar x = new Y();
15671 ˇ</script>
15672 "#
15673 .unindent(),
15674 );
15675 cx.executor().run_until_parked();
15676 cx.update_editor(|editor, window, cx| {
15677 editor.toggle_comments(&ToggleComments::default(), window, cx)
15678 });
15679 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15680 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15681 cx.assert_editor_state(
15682 &r#"
15683 <!-- ˇ<script> -->
15684 // ˇvar x = new Y();
15685 <!-- ˇ</script> -->
15686 "#
15687 .unindent(),
15688 );
15689}
15690
15691#[gpui::test]
15692fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15693 init_test(cx, |_| {});
15694
15695 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15696 let multibuffer = cx.new(|cx| {
15697 let mut multibuffer = MultiBuffer::new(ReadWrite);
15698 multibuffer.push_excerpts(
15699 buffer.clone(),
15700 [
15701 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15702 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15703 ],
15704 cx,
15705 );
15706 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15707 multibuffer
15708 });
15709
15710 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15711 editor.update_in(cx, |editor, window, cx| {
15712 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15713 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15714 s.select_ranges([
15715 Point::new(0, 0)..Point::new(0, 0),
15716 Point::new(1, 0)..Point::new(1, 0),
15717 ])
15718 });
15719
15720 editor.handle_input("X", window, cx);
15721 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15722 assert_eq!(
15723 editor.selections.ranges(cx),
15724 [
15725 Point::new(0, 1)..Point::new(0, 1),
15726 Point::new(1, 1)..Point::new(1, 1),
15727 ]
15728 );
15729
15730 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15732 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15733 });
15734 editor.backspace(&Default::default(), window, cx);
15735 assert_eq!(editor.text(cx), "Xa\nbbb");
15736 assert_eq!(
15737 editor.selections.ranges(cx),
15738 [Point::new(1, 0)..Point::new(1, 0)]
15739 );
15740
15741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15742 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15743 });
15744 editor.backspace(&Default::default(), window, cx);
15745 assert_eq!(editor.text(cx), "X\nbb");
15746 assert_eq!(
15747 editor.selections.ranges(cx),
15748 [Point::new(0, 1)..Point::new(0, 1)]
15749 );
15750 });
15751}
15752
15753#[gpui::test]
15754fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15755 init_test(cx, |_| {});
15756
15757 let markers = vec![('[', ']').into(), ('(', ')').into()];
15758 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15759 indoc! {"
15760 [aaaa
15761 (bbbb]
15762 cccc)",
15763 },
15764 markers.clone(),
15765 );
15766 let excerpt_ranges = markers.into_iter().map(|marker| {
15767 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15768 ExcerptRange::new(context)
15769 });
15770 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15771 let multibuffer = cx.new(|cx| {
15772 let mut multibuffer = MultiBuffer::new(ReadWrite);
15773 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15774 multibuffer
15775 });
15776
15777 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15778 editor.update_in(cx, |editor, window, cx| {
15779 let (expected_text, selection_ranges) = marked_text_ranges(
15780 indoc! {"
15781 aaaa
15782 bˇbbb
15783 bˇbbˇb
15784 cccc"
15785 },
15786 true,
15787 );
15788 assert_eq!(editor.text(cx), expected_text);
15789 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15790 s.select_ranges(selection_ranges)
15791 });
15792
15793 editor.handle_input("X", window, cx);
15794
15795 let (expected_text, expected_selections) = marked_text_ranges(
15796 indoc! {"
15797 aaaa
15798 bXˇbbXb
15799 bXˇbbXˇb
15800 cccc"
15801 },
15802 false,
15803 );
15804 assert_eq!(editor.text(cx), expected_text);
15805 assert_eq!(editor.selections.ranges(cx), expected_selections);
15806
15807 editor.newline(&Newline, window, cx);
15808 let (expected_text, expected_selections) = marked_text_ranges(
15809 indoc! {"
15810 aaaa
15811 bX
15812 ˇbbX
15813 b
15814 bX
15815 ˇbbX
15816 ˇb
15817 cccc"
15818 },
15819 false,
15820 );
15821 assert_eq!(editor.text(cx), expected_text);
15822 assert_eq!(editor.selections.ranges(cx), expected_selections);
15823 });
15824}
15825
15826#[gpui::test]
15827fn test_refresh_selections(cx: &mut TestAppContext) {
15828 init_test(cx, |_| {});
15829
15830 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15831 let mut excerpt1_id = None;
15832 let multibuffer = cx.new(|cx| {
15833 let mut multibuffer = MultiBuffer::new(ReadWrite);
15834 excerpt1_id = multibuffer
15835 .push_excerpts(
15836 buffer.clone(),
15837 [
15838 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15839 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15840 ],
15841 cx,
15842 )
15843 .into_iter()
15844 .next();
15845 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15846 multibuffer
15847 });
15848
15849 let editor = cx.add_window(|window, cx| {
15850 let mut editor = build_editor(multibuffer.clone(), window, cx);
15851 let snapshot = editor.snapshot(window, cx);
15852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15853 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15854 });
15855 editor.begin_selection(
15856 Point::new(2, 1).to_display_point(&snapshot),
15857 true,
15858 1,
15859 window,
15860 cx,
15861 );
15862 assert_eq!(
15863 editor.selections.ranges(cx),
15864 [
15865 Point::new(1, 3)..Point::new(1, 3),
15866 Point::new(2, 1)..Point::new(2, 1),
15867 ]
15868 );
15869 editor
15870 });
15871
15872 // Refreshing selections is a no-op when excerpts haven't changed.
15873 _ = editor.update(cx, |editor, window, cx| {
15874 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15875 assert_eq!(
15876 editor.selections.ranges(cx),
15877 [
15878 Point::new(1, 3)..Point::new(1, 3),
15879 Point::new(2, 1)..Point::new(2, 1),
15880 ]
15881 );
15882 });
15883
15884 multibuffer.update(cx, |multibuffer, cx| {
15885 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15886 });
15887 _ = editor.update(cx, |editor, window, cx| {
15888 // Removing an excerpt causes the first selection to become degenerate.
15889 assert_eq!(
15890 editor.selections.ranges(cx),
15891 [
15892 Point::new(0, 0)..Point::new(0, 0),
15893 Point::new(0, 1)..Point::new(0, 1)
15894 ]
15895 );
15896
15897 // Refreshing selections will relocate the first selection to the original buffer
15898 // location.
15899 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15900 assert_eq!(
15901 editor.selections.ranges(cx),
15902 [
15903 Point::new(0, 1)..Point::new(0, 1),
15904 Point::new(0, 3)..Point::new(0, 3)
15905 ]
15906 );
15907 assert!(editor.selections.pending_anchor().is_some());
15908 });
15909}
15910
15911#[gpui::test]
15912fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15913 init_test(cx, |_| {});
15914
15915 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15916 let mut excerpt1_id = None;
15917 let multibuffer = cx.new(|cx| {
15918 let mut multibuffer = MultiBuffer::new(ReadWrite);
15919 excerpt1_id = multibuffer
15920 .push_excerpts(
15921 buffer.clone(),
15922 [
15923 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15924 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15925 ],
15926 cx,
15927 )
15928 .into_iter()
15929 .next();
15930 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15931 multibuffer
15932 });
15933
15934 let editor = cx.add_window(|window, cx| {
15935 let mut editor = build_editor(multibuffer.clone(), window, cx);
15936 let snapshot = editor.snapshot(window, cx);
15937 editor.begin_selection(
15938 Point::new(1, 3).to_display_point(&snapshot),
15939 false,
15940 1,
15941 window,
15942 cx,
15943 );
15944 assert_eq!(
15945 editor.selections.ranges(cx),
15946 [Point::new(1, 3)..Point::new(1, 3)]
15947 );
15948 editor
15949 });
15950
15951 multibuffer.update(cx, |multibuffer, cx| {
15952 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15953 });
15954 _ = editor.update(cx, |editor, window, cx| {
15955 assert_eq!(
15956 editor.selections.ranges(cx),
15957 [Point::new(0, 0)..Point::new(0, 0)]
15958 );
15959
15960 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15962 assert_eq!(
15963 editor.selections.ranges(cx),
15964 [Point::new(0, 3)..Point::new(0, 3)]
15965 );
15966 assert!(editor.selections.pending_anchor().is_some());
15967 });
15968}
15969
15970#[gpui::test]
15971async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15972 init_test(cx, |_| {});
15973
15974 let language = Arc::new(
15975 Language::new(
15976 LanguageConfig {
15977 brackets: BracketPairConfig {
15978 pairs: vec![
15979 BracketPair {
15980 start: "{".to_string(),
15981 end: "}".to_string(),
15982 close: true,
15983 surround: true,
15984 newline: true,
15985 },
15986 BracketPair {
15987 start: "/* ".to_string(),
15988 end: " */".to_string(),
15989 close: true,
15990 surround: true,
15991 newline: true,
15992 },
15993 ],
15994 ..Default::default()
15995 },
15996 ..Default::default()
15997 },
15998 Some(tree_sitter_rust::LANGUAGE.into()),
15999 )
16000 .with_indents_query("")
16001 .unwrap(),
16002 );
16003
16004 let text = concat!(
16005 "{ }\n", //
16006 " x\n", //
16007 " /* */\n", //
16008 "x\n", //
16009 "{{} }\n", //
16010 );
16011
16012 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16013 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16014 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16015 editor
16016 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16017 .await;
16018
16019 editor.update_in(cx, |editor, window, cx| {
16020 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16021 s.select_display_ranges([
16022 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16023 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16024 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16025 ])
16026 });
16027 editor.newline(&Newline, window, cx);
16028
16029 assert_eq!(
16030 editor.buffer().read(cx).read(cx).text(),
16031 concat!(
16032 "{ \n", // Suppress rustfmt
16033 "\n", //
16034 "}\n", //
16035 " x\n", //
16036 " /* \n", //
16037 " \n", //
16038 " */\n", //
16039 "x\n", //
16040 "{{} \n", //
16041 "}\n", //
16042 )
16043 );
16044 });
16045}
16046
16047#[gpui::test]
16048fn test_highlighted_ranges(cx: &mut TestAppContext) {
16049 init_test(cx, |_| {});
16050
16051 let editor = cx.add_window(|window, cx| {
16052 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16053 build_editor(buffer, window, cx)
16054 });
16055
16056 _ = editor.update(cx, |editor, window, cx| {
16057 struct Type1;
16058 struct Type2;
16059
16060 let buffer = editor.buffer.read(cx).snapshot(cx);
16061
16062 let anchor_range =
16063 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16064
16065 editor.highlight_background::<Type1>(
16066 &[
16067 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16068 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16069 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16070 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16071 ],
16072 |_| Hsla::red(),
16073 cx,
16074 );
16075 editor.highlight_background::<Type2>(
16076 &[
16077 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16078 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16079 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16080 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16081 ],
16082 |_| Hsla::green(),
16083 cx,
16084 );
16085
16086 let snapshot = editor.snapshot(window, cx);
16087 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16088 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16089 &snapshot,
16090 cx.theme(),
16091 );
16092 assert_eq!(
16093 highlighted_ranges,
16094 &[
16095 (
16096 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16097 Hsla::green(),
16098 ),
16099 (
16100 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16101 Hsla::red(),
16102 ),
16103 (
16104 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16105 Hsla::green(),
16106 ),
16107 (
16108 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16109 Hsla::red(),
16110 ),
16111 ]
16112 );
16113 assert_eq!(
16114 editor.sorted_background_highlights_in_range(
16115 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16116 &snapshot,
16117 cx.theme(),
16118 ),
16119 &[(
16120 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16121 Hsla::red(),
16122 )]
16123 );
16124 });
16125}
16126
16127#[gpui::test]
16128async fn test_following(cx: &mut TestAppContext) {
16129 init_test(cx, |_| {});
16130
16131 let fs = FakeFs::new(cx.executor());
16132 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16133
16134 let buffer = project.update(cx, |project, cx| {
16135 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16136 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16137 });
16138 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16139 let follower = cx.update(|cx| {
16140 cx.open_window(
16141 WindowOptions {
16142 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16143 gpui::Point::new(px(0.), px(0.)),
16144 gpui::Point::new(px(10.), px(80.)),
16145 ))),
16146 ..Default::default()
16147 },
16148 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16149 )
16150 .unwrap()
16151 });
16152
16153 let is_still_following = Rc::new(RefCell::new(true));
16154 let follower_edit_event_count = Rc::new(RefCell::new(0));
16155 let pending_update = Rc::new(RefCell::new(None));
16156 let leader_entity = leader.root(cx).unwrap();
16157 let follower_entity = follower.root(cx).unwrap();
16158 _ = follower.update(cx, {
16159 let update = pending_update.clone();
16160 let is_still_following = is_still_following.clone();
16161 let follower_edit_event_count = follower_edit_event_count.clone();
16162 |_, window, cx| {
16163 cx.subscribe_in(
16164 &leader_entity,
16165 window,
16166 move |_, leader, event, window, cx| {
16167 leader.read(cx).add_event_to_update_proto(
16168 event,
16169 &mut update.borrow_mut(),
16170 window,
16171 cx,
16172 );
16173 },
16174 )
16175 .detach();
16176
16177 cx.subscribe_in(
16178 &follower_entity,
16179 window,
16180 move |_, _, event: &EditorEvent, _window, _cx| {
16181 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16182 *is_still_following.borrow_mut() = false;
16183 }
16184
16185 if let EditorEvent::BufferEdited = event {
16186 *follower_edit_event_count.borrow_mut() += 1;
16187 }
16188 },
16189 )
16190 .detach();
16191 }
16192 });
16193
16194 // Update the selections only
16195 _ = leader.update(cx, |leader, window, cx| {
16196 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16197 s.select_ranges([1..1])
16198 });
16199 });
16200 follower
16201 .update(cx, |follower, window, cx| {
16202 follower.apply_update_proto(
16203 &project,
16204 pending_update.borrow_mut().take().unwrap(),
16205 window,
16206 cx,
16207 )
16208 })
16209 .unwrap()
16210 .await
16211 .unwrap();
16212 _ = follower.update(cx, |follower, _, cx| {
16213 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16214 });
16215 assert!(*is_still_following.borrow());
16216 assert_eq!(*follower_edit_event_count.borrow(), 0);
16217
16218 // Update the scroll position only
16219 _ = leader.update(cx, |leader, window, cx| {
16220 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16221 });
16222 follower
16223 .update(cx, |follower, window, cx| {
16224 follower.apply_update_proto(
16225 &project,
16226 pending_update.borrow_mut().take().unwrap(),
16227 window,
16228 cx,
16229 )
16230 })
16231 .unwrap()
16232 .await
16233 .unwrap();
16234 assert_eq!(
16235 follower
16236 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16237 .unwrap(),
16238 gpui::Point::new(1.5, 3.5)
16239 );
16240 assert!(*is_still_following.borrow());
16241 assert_eq!(*follower_edit_event_count.borrow(), 0);
16242
16243 // Update the selections and scroll position. The follower's scroll position is updated
16244 // via autoscroll, not via the leader's exact scroll position.
16245 _ = leader.update(cx, |leader, window, cx| {
16246 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16247 s.select_ranges([0..0])
16248 });
16249 leader.request_autoscroll(Autoscroll::newest(), cx);
16250 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16251 });
16252 follower
16253 .update(cx, |follower, window, cx| {
16254 follower.apply_update_proto(
16255 &project,
16256 pending_update.borrow_mut().take().unwrap(),
16257 window,
16258 cx,
16259 )
16260 })
16261 .unwrap()
16262 .await
16263 .unwrap();
16264 _ = follower.update(cx, |follower, _, cx| {
16265 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16266 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16267 });
16268 assert!(*is_still_following.borrow());
16269
16270 // Creating a pending selection that precedes another selection
16271 _ = leader.update(cx, |leader, window, cx| {
16272 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16273 s.select_ranges([1..1])
16274 });
16275 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16276 });
16277 follower
16278 .update(cx, |follower, window, cx| {
16279 follower.apply_update_proto(
16280 &project,
16281 pending_update.borrow_mut().take().unwrap(),
16282 window,
16283 cx,
16284 )
16285 })
16286 .unwrap()
16287 .await
16288 .unwrap();
16289 _ = follower.update(cx, |follower, _, cx| {
16290 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16291 });
16292 assert!(*is_still_following.borrow());
16293
16294 // Extend the pending selection so that it surrounds another selection
16295 _ = leader.update(cx, |leader, window, cx| {
16296 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16297 });
16298 follower
16299 .update(cx, |follower, window, cx| {
16300 follower.apply_update_proto(
16301 &project,
16302 pending_update.borrow_mut().take().unwrap(),
16303 window,
16304 cx,
16305 )
16306 })
16307 .unwrap()
16308 .await
16309 .unwrap();
16310 _ = follower.update(cx, |follower, _, cx| {
16311 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16312 });
16313
16314 // Scrolling locally breaks the follow
16315 _ = follower.update(cx, |follower, window, cx| {
16316 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16317 follower.set_scroll_anchor(
16318 ScrollAnchor {
16319 anchor: top_anchor,
16320 offset: gpui::Point::new(0.0, 0.5),
16321 },
16322 window,
16323 cx,
16324 );
16325 });
16326 assert!(!(*is_still_following.borrow()));
16327}
16328
16329#[gpui::test]
16330async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16331 init_test(cx, |_| {});
16332
16333 let fs = FakeFs::new(cx.executor());
16334 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16335 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16336 let pane = workspace
16337 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16338 .unwrap();
16339
16340 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16341
16342 let leader = pane.update_in(cx, |_, window, cx| {
16343 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16344 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16345 });
16346
16347 // Start following the editor when it has no excerpts.
16348 let mut state_message =
16349 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16350 let workspace_entity = workspace.root(cx).unwrap();
16351 let follower_1 = cx
16352 .update_window(*workspace.deref(), |_, window, cx| {
16353 Editor::from_state_proto(
16354 workspace_entity,
16355 ViewId {
16356 creator: CollaboratorId::PeerId(PeerId::default()),
16357 id: 0,
16358 },
16359 &mut state_message,
16360 window,
16361 cx,
16362 )
16363 })
16364 .unwrap()
16365 .unwrap()
16366 .await
16367 .unwrap();
16368
16369 let update_message = Rc::new(RefCell::new(None));
16370 follower_1.update_in(cx, {
16371 let update = update_message.clone();
16372 |_, window, cx| {
16373 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16374 leader.read(cx).add_event_to_update_proto(
16375 event,
16376 &mut update.borrow_mut(),
16377 window,
16378 cx,
16379 );
16380 })
16381 .detach();
16382 }
16383 });
16384
16385 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16386 (
16387 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16388 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16389 )
16390 });
16391
16392 // Insert some excerpts.
16393 leader.update(cx, |leader, cx| {
16394 leader.buffer.update(cx, |multibuffer, cx| {
16395 multibuffer.set_excerpts_for_path(
16396 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16397 buffer_1.clone(),
16398 vec![
16399 Point::row_range(0..3),
16400 Point::row_range(1..6),
16401 Point::row_range(12..15),
16402 ],
16403 0,
16404 cx,
16405 );
16406 multibuffer.set_excerpts_for_path(
16407 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16408 buffer_2.clone(),
16409 vec![Point::row_range(0..6), Point::row_range(8..12)],
16410 0,
16411 cx,
16412 );
16413 });
16414 });
16415
16416 // Apply the update of adding the excerpts.
16417 follower_1
16418 .update_in(cx, |follower, window, cx| {
16419 follower.apply_update_proto(
16420 &project,
16421 update_message.borrow().clone().unwrap(),
16422 window,
16423 cx,
16424 )
16425 })
16426 .await
16427 .unwrap();
16428 assert_eq!(
16429 follower_1.update(cx, |editor, cx| editor.text(cx)),
16430 leader.update(cx, |editor, cx| editor.text(cx))
16431 );
16432 update_message.borrow_mut().take();
16433
16434 // Start following separately after it already has excerpts.
16435 let mut state_message =
16436 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16437 let workspace_entity = workspace.root(cx).unwrap();
16438 let follower_2 = cx
16439 .update_window(*workspace.deref(), |_, window, cx| {
16440 Editor::from_state_proto(
16441 workspace_entity,
16442 ViewId {
16443 creator: CollaboratorId::PeerId(PeerId::default()),
16444 id: 0,
16445 },
16446 &mut state_message,
16447 window,
16448 cx,
16449 )
16450 })
16451 .unwrap()
16452 .unwrap()
16453 .await
16454 .unwrap();
16455 assert_eq!(
16456 follower_2.update(cx, |editor, cx| editor.text(cx)),
16457 leader.update(cx, |editor, cx| editor.text(cx))
16458 );
16459
16460 // Remove some excerpts.
16461 leader.update(cx, |leader, cx| {
16462 leader.buffer.update(cx, |multibuffer, cx| {
16463 let excerpt_ids = multibuffer.excerpt_ids();
16464 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16465 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16466 });
16467 });
16468
16469 // Apply the update of removing the excerpts.
16470 follower_1
16471 .update_in(cx, |follower, window, cx| {
16472 follower.apply_update_proto(
16473 &project,
16474 update_message.borrow().clone().unwrap(),
16475 window,
16476 cx,
16477 )
16478 })
16479 .await
16480 .unwrap();
16481 follower_2
16482 .update_in(cx, |follower, window, cx| {
16483 follower.apply_update_proto(
16484 &project,
16485 update_message.borrow().clone().unwrap(),
16486 window,
16487 cx,
16488 )
16489 })
16490 .await
16491 .unwrap();
16492 update_message.borrow_mut().take();
16493 assert_eq!(
16494 follower_1.update(cx, |editor, cx| editor.text(cx)),
16495 leader.update(cx, |editor, cx| editor.text(cx))
16496 );
16497}
16498
16499#[gpui::test]
16500async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16501 init_test(cx, |_| {});
16502
16503 let mut cx = EditorTestContext::new(cx).await;
16504 let lsp_store =
16505 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16506
16507 cx.set_state(indoc! {"
16508 ˇfn func(abc def: i32) -> u32 {
16509 }
16510 "});
16511
16512 cx.update(|_, cx| {
16513 lsp_store.update(cx, |lsp_store, cx| {
16514 lsp_store
16515 .update_diagnostics(
16516 LanguageServerId(0),
16517 lsp::PublishDiagnosticsParams {
16518 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16519 version: None,
16520 diagnostics: vec![
16521 lsp::Diagnostic {
16522 range: lsp::Range::new(
16523 lsp::Position::new(0, 11),
16524 lsp::Position::new(0, 12),
16525 ),
16526 severity: Some(lsp::DiagnosticSeverity::ERROR),
16527 ..Default::default()
16528 },
16529 lsp::Diagnostic {
16530 range: lsp::Range::new(
16531 lsp::Position::new(0, 12),
16532 lsp::Position::new(0, 15),
16533 ),
16534 severity: Some(lsp::DiagnosticSeverity::ERROR),
16535 ..Default::default()
16536 },
16537 lsp::Diagnostic {
16538 range: lsp::Range::new(
16539 lsp::Position::new(0, 25),
16540 lsp::Position::new(0, 28),
16541 ),
16542 severity: Some(lsp::DiagnosticSeverity::ERROR),
16543 ..Default::default()
16544 },
16545 ],
16546 },
16547 None,
16548 DiagnosticSourceKind::Pushed,
16549 &[],
16550 cx,
16551 )
16552 .unwrap()
16553 });
16554 });
16555
16556 executor.run_until_parked();
16557
16558 cx.update_editor(|editor, window, cx| {
16559 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16560 });
16561
16562 cx.assert_editor_state(indoc! {"
16563 fn func(abc def: i32) -> ˇu32 {
16564 }
16565 "});
16566
16567 cx.update_editor(|editor, window, cx| {
16568 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16569 });
16570
16571 cx.assert_editor_state(indoc! {"
16572 fn func(abc ˇdef: i32) -> u32 {
16573 }
16574 "});
16575
16576 cx.update_editor(|editor, window, cx| {
16577 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16578 });
16579
16580 cx.assert_editor_state(indoc! {"
16581 fn func(abcˇ def: i32) -> u32 {
16582 }
16583 "});
16584
16585 cx.update_editor(|editor, window, cx| {
16586 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16587 });
16588
16589 cx.assert_editor_state(indoc! {"
16590 fn func(abc def: i32) -> ˇu32 {
16591 }
16592 "});
16593}
16594
16595#[gpui::test]
16596async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16597 init_test(cx, |_| {});
16598
16599 let mut cx = EditorTestContext::new(cx).await;
16600
16601 let diff_base = r#"
16602 use some::mod;
16603
16604 const A: u32 = 42;
16605
16606 fn main() {
16607 println!("hello");
16608
16609 println!("world");
16610 }
16611 "#
16612 .unindent();
16613
16614 // Edits are modified, removed, modified, added
16615 cx.set_state(
16616 &r#"
16617 use some::modified;
16618
16619 ˇ
16620 fn main() {
16621 println!("hello there");
16622
16623 println!("around the");
16624 println!("world");
16625 }
16626 "#
16627 .unindent(),
16628 );
16629
16630 cx.set_head_text(&diff_base);
16631 executor.run_until_parked();
16632
16633 cx.update_editor(|editor, window, cx| {
16634 //Wrap around the bottom of the buffer
16635 for _ in 0..3 {
16636 editor.go_to_next_hunk(&GoToHunk, window, cx);
16637 }
16638 });
16639
16640 cx.assert_editor_state(
16641 &r#"
16642 ˇuse some::modified;
16643
16644
16645 fn main() {
16646 println!("hello there");
16647
16648 println!("around the");
16649 println!("world");
16650 }
16651 "#
16652 .unindent(),
16653 );
16654
16655 cx.update_editor(|editor, window, cx| {
16656 //Wrap around the top of the buffer
16657 for _ in 0..2 {
16658 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16659 }
16660 });
16661
16662 cx.assert_editor_state(
16663 &r#"
16664 use some::modified;
16665
16666
16667 fn main() {
16668 ˇ println!("hello there");
16669
16670 println!("around the");
16671 println!("world");
16672 }
16673 "#
16674 .unindent(),
16675 );
16676
16677 cx.update_editor(|editor, window, cx| {
16678 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16679 });
16680
16681 cx.assert_editor_state(
16682 &r#"
16683 use some::modified;
16684
16685 ˇ
16686 fn main() {
16687 println!("hello there");
16688
16689 println!("around the");
16690 println!("world");
16691 }
16692 "#
16693 .unindent(),
16694 );
16695
16696 cx.update_editor(|editor, window, cx| {
16697 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16698 });
16699
16700 cx.assert_editor_state(
16701 &r#"
16702 ˇuse some::modified;
16703
16704
16705 fn main() {
16706 println!("hello there");
16707
16708 println!("around the");
16709 println!("world");
16710 }
16711 "#
16712 .unindent(),
16713 );
16714
16715 cx.update_editor(|editor, window, cx| {
16716 for _ in 0..2 {
16717 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16718 }
16719 });
16720
16721 cx.assert_editor_state(
16722 &r#"
16723 use some::modified;
16724
16725
16726 fn main() {
16727 ˇ println!("hello there");
16728
16729 println!("around the");
16730 println!("world");
16731 }
16732 "#
16733 .unindent(),
16734 );
16735
16736 cx.update_editor(|editor, window, cx| {
16737 editor.fold(&Fold, window, cx);
16738 });
16739
16740 cx.update_editor(|editor, window, cx| {
16741 editor.go_to_next_hunk(&GoToHunk, window, cx);
16742 });
16743
16744 cx.assert_editor_state(
16745 &r#"
16746 ˇuse some::modified;
16747
16748
16749 fn main() {
16750 println!("hello there");
16751
16752 println!("around the");
16753 println!("world");
16754 }
16755 "#
16756 .unindent(),
16757 );
16758}
16759
16760#[test]
16761fn test_split_words() {
16762 fn split(text: &str) -> Vec<&str> {
16763 split_words(text).collect()
16764 }
16765
16766 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16767 assert_eq!(split("hello_world"), &["hello_", "world"]);
16768 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16769 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16770 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16771 assert_eq!(split("helloworld"), &["helloworld"]);
16772
16773 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16774}
16775
16776#[gpui::test]
16777async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16778 init_test(cx, |_| {});
16779
16780 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16781 let mut assert = |before, after| {
16782 let _state_context = cx.set_state(before);
16783 cx.run_until_parked();
16784 cx.update_editor(|editor, window, cx| {
16785 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16786 });
16787 cx.run_until_parked();
16788 cx.assert_editor_state(after);
16789 };
16790
16791 // Outside bracket jumps to outside of matching bracket
16792 assert("console.logˇ(var);", "console.log(var)ˇ;");
16793 assert("console.log(var)ˇ;", "console.logˇ(var);");
16794
16795 // Inside bracket jumps to inside of matching bracket
16796 assert("console.log(ˇvar);", "console.log(varˇ);");
16797 assert("console.log(varˇ);", "console.log(ˇvar);");
16798
16799 // When outside a bracket and inside, favor jumping to the inside bracket
16800 assert(
16801 "console.log('foo', [1, 2, 3]ˇ);",
16802 "console.log(ˇ'foo', [1, 2, 3]);",
16803 );
16804 assert(
16805 "console.log(ˇ'foo', [1, 2, 3]);",
16806 "console.log('foo', [1, 2, 3]ˇ);",
16807 );
16808
16809 // Bias forward if two options are equally likely
16810 assert(
16811 "let result = curried_fun()ˇ();",
16812 "let result = curried_fun()()ˇ;",
16813 );
16814
16815 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16816 assert(
16817 indoc! {"
16818 function test() {
16819 console.log('test')ˇ
16820 }"},
16821 indoc! {"
16822 function test() {
16823 console.logˇ('test')
16824 }"},
16825 );
16826}
16827
16828#[gpui::test]
16829async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16830 init_test(cx, |_| {});
16831
16832 let fs = FakeFs::new(cx.executor());
16833 fs.insert_tree(
16834 path!("/a"),
16835 json!({
16836 "main.rs": "fn main() { let a = 5; }",
16837 "other.rs": "// Test file",
16838 }),
16839 )
16840 .await;
16841 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16842
16843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16844 language_registry.add(Arc::new(Language::new(
16845 LanguageConfig {
16846 name: "Rust".into(),
16847 matcher: LanguageMatcher {
16848 path_suffixes: vec!["rs".to_string()],
16849 ..Default::default()
16850 },
16851 brackets: BracketPairConfig {
16852 pairs: vec![BracketPair {
16853 start: "{".to_string(),
16854 end: "}".to_string(),
16855 close: true,
16856 surround: true,
16857 newline: true,
16858 }],
16859 disabled_scopes_by_bracket_ix: Vec::new(),
16860 },
16861 ..Default::default()
16862 },
16863 Some(tree_sitter_rust::LANGUAGE.into()),
16864 )));
16865 let mut fake_servers = language_registry.register_fake_lsp(
16866 "Rust",
16867 FakeLspAdapter {
16868 capabilities: lsp::ServerCapabilities {
16869 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16870 first_trigger_character: "{".to_string(),
16871 more_trigger_character: None,
16872 }),
16873 ..Default::default()
16874 },
16875 ..Default::default()
16876 },
16877 );
16878
16879 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16880
16881 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16882
16883 let worktree_id = workspace
16884 .update(cx, |workspace, _, cx| {
16885 workspace.project().update(cx, |project, cx| {
16886 project.worktrees(cx).next().unwrap().read(cx).id()
16887 })
16888 })
16889 .unwrap();
16890
16891 let buffer = project
16892 .update(cx, |project, cx| {
16893 project.open_local_buffer(path!("/a/main.rs"), cx)
16894 })
16895 .await
16896 .unwrap();
16897 let editor_handle = workspace
16898 .update(cx, |workspace, window, cx| {
16899 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16900 })
16901 .unwrap()
16902 .await
16903 .unwrap()
16904 .downcast::<Editor>()
16905 .unwrap();
16906
16907 cx.executor().start_waiting();
16908 let fake_server = fake_servers.next().await.unwrap();
16909
16910 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16911 |params, _| async move {
16912 assert_eq!(
16913 params.text_document_position.text_document.uri,
16914 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16915 );
16916 assert_eq!(
16917 params.text_document_position.position,
16918 lsp::Position::new(0, 21),
16919 );
16920
16921 Ok(Some(vec![lsp::TextEdit {
16922 new_text: "]".to_string(),
16923 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16924 }]))
16925 },
16926 );
16927
16928 editor_handle.update_in(cx, |editor, window, cx| {
16929 window.focus(&editor.focus_handle(cx));
16930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16931 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16932 });
16933 editor.handle_input("{", window, cx);
16934 });
16935
16936 cx.executor().run_until_parked();
16937
16938 buffer.update(cx, |buffer, _| {
16939 assert_eq!(
16940 buffer.text(),
16941 "fn main() { let a = {5}; }",
16942 "No extra braces from on type formatting should appear in the buffer"
16943 )
16944 });
16945}
16946
16947#[gpui::test(iterations = 20, seeds(31))]
16948async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16949 init_test(cx, |_| {});
16950
16951 let mut cx = EditorLspTestContext::new_rust(
16952 lsp::ServerCapabilities {
16953 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16954 first_trigger_character: ".".to_string(),
16955 more_trigger_character: None,
16956 }),
16957 ..Default::default()
16958 },
16959 cx,
16960 )
16961 .await;
16962
16963 cx.update_buffer(|buffer, _| {
16964 // This causes autoindent to be async.
16965 buffer.set_sync_parse_timeout(Duration::ZERO)
16966 });
16967
16968 cx.set_state("fn c() {\n d()ˇ\n}\n");
16969 cx.simulate_keystroke("\n");
16970 cx.run_until_parked();
16971
16972 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16973 let mut request =
16974 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16975 let buffer_cloned = buffer_cloned.clone();
16976 async move {
16977 buffer_cloned.update(&mut cx, |buffer, _| {
16978 assert_eq!(
16979 buffer.text(),
16980 "fn c() {\n d()\n .\n}\n",
16981 "OnTypeFormatting should triggered after autoindent applied"
16982 )
16983 })?;
16984
16985 Ok(Some(vec![]))
16986 }
16987 });
16988
16989 cx.simulate_keystroke(".");
16990 cx.run_until_parked();
16991
16992 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16993 assert!(request.next().await.is_some());
16994 request.close();
16995 assert!(request.next().await.is_none());
16996}
16997
16998#[gpui::test]
16999async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17000 init_test(cx, |_| {});
17001
17002 let fs = FakeFs::new(cx.executor());
17003 fs.insert_tree(
17004 path!("/a"),
17005 json!({
17006 "main.rs": "fn main() { let a = 5; }",
17007 "other.rs": "// Test file",
17008 }),
17009 )
17010 .await;
17011
17012 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17013
17014 let server_restarts = Arc::new(AtomicUsize::new(0));
17015 let closure_restarts = Arc::clone(&server_restarts);
17016 let language_server_name = "test language server";
17017 let language_name: LanguageName = "Rust".into();
17018
17019 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17020 language_registry.add(Arc::new(Language::new(
17021 LanguageConfig {
17022 name: language_name.clone(),
17023 matcher: LanguageMatcher {
17024 path_suffixes: vec!["rs".to_string()],
17025 ..Default::default()
17026 },
17027 ..Default::default()
17028 },
17029 Some(tree_sitter_rust::LANGUAGE.into()),
17030 )));
17031 let mut fake_servers = language_registry.register_fake_lsp(
17032 "Rust",
17033 FakeLspAdapter {
17034 name: language_server_name,
17035 initialization_options: Some(json!({
17036 "testOptionValue": true
17037 })),
17038 initializer: Some(Box::new(move |fake_server| {
17039 let task_restarts = Arc::clone(&closure_restarts);
17040 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17041 task_restarts.fetch_add(1, atomic::Ordering::Release);
17042 futures::future::ready(Ok(()))
17043 });
17044 })),
17045 ..Default::default()
17046 },
17047 );
17048
17049 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17050 let _buffer = project
17051 .update(cx, |project, cx| {
17052 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17053 })
17054 .await
17055 .unwrap();
17056 let _fake_server = fake_servers.next().await.unwrap();
17057 update_test_language_settings(cx, |language_settings| {
17058 language_settings.languages.0.insert(
17059 language_name.clone().0,
17060 LanguageSettingsContent {
17061 tab_size: NonZeroU32::new(8),
17062 ..Default::default()
17063 },
17064 );
17065 });
17066 cx.executor().run_until_parked();
17067 assert_eq!(
17068 server_restarts.load(atomic::Ordering::Acquire),
17069 0,
17070 "Should not restart LSP server on an unrelated change"
17071 );
17072
17073 update_test_project_settings(cx, |project_settings| {
17074 project_settings.lsp.insert(
17075 "Some other server name".into(),
17076 LspSettings {
17077 binary: None,
17078 settings: None,
17079 initialization_options: Some(json!({
17080 "some other init value": false
17081 })),
17082 enable_lsp_tasks: false,
17083 fetch: None,
17084 },
17085 );
17086 });
17087 cx.executor().run_until_parked();
17088 assert_eq!(
17089 server_restarts.load(atomic::Ordering::Acquire),
17090 0,
17091 "Should not restart LSP server on an unrelated LSP settings change"
17092 );
17093
17094 update_test_project_settings(cx, |project_settings| {
17095 project_settings.lsp.insert(
17096 language_server_name.into(),
17097 LspSettings {
17098 binary: None,
17099 settings: None,
17100 initialization_options: Some(json!({
17101 "anotherInitValue": false
17102 })),
17103 enable_lsp_tasks: false,
17104 fetch: None,
17105 },
17106 );
17107 });
17108 cx.executor().run_until_parked();
17109 assert_eq!(
17110 server_restarts.load(atomic::Ordering::Acquire),
17111 1,
17112 "Should restart LSP server on a related LSP settings change"
17113 );
17114
17115 update_test_project_settings(cx, |project_settings| {
17116 project_settings.lsp.insert(
17117 language_server_name.into(),
17118 LspSettings {
17119 binary: None,
17120 settings: None,
17121 initialization_options: Some(json!({
17122 "anotherInitValue": false
17123 })),
17124 enable_lsp_tasks: false,
17125 fetch: None,
17126 },
17127 );
17128 });
17129 cx.executor().run_until_parked();
17130 assert_eq!(
17131 server_restarts.load(atomic::Ordering::Acquire),
17132 1,
17133 "Should not restart LSP server on a related LSP settings change that is the same"
17134 );
17135
17136 update_test_project_settings(cx, |project_settings| {
17137 project_settings.lsp.insert(
17138 language_server_name.into(),
17139 LspSettings {
17140 binary: None,
17141 settings: None,
17142 initialization_options: None,
17143 enable_lsp_tasks: false,
17144 fetch: None,
17145 },
17146 );
17147 });
17148 cx.executor().run_until_parked();
17149 assert_eq!(
17150 server_restarts.load(atomic::Ordering::Acquire),
17151 2,
17152 "Should restart LSP server on another related LSP settings change"
17153 );
17154}
17155
17156#[gpui::test]
17157async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17158 init_test(cx, |_| {});
17159
17160 let mut cx = EditorLspTestContext::new_rust(
17161 lsp::ServerCapabilities {
17162 completion_provider: Some(lsp::CompletionOptions {
17163 trigger_characters: Some(vec![".".to_string()]),
17164 resolve_provider: Some(true),
17165 ..Default::default()
17166 }),
17167 ..Default::default()
17168 },
17169 cx,
17170 )
17171 .await;
17172
17173 cx.set_state("fn main() { let a = 2ˇ; }");
17174 cx.simulate_keystroke(".");
17175 let completion_item = lsp::CompletionItem {
17176 label: "some".into(),
17177 kind: Some(lsp::CompletionItemKind::SNIPPET),
17178 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17179 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17180 kind: lsp::MarkupKind::Markdown,
17181 value: "```rust\nSome(2)\n```".to_string(),
17182 })),
17183 deprecated: Some(false),
17184 sort_text: Some("fffffff2".to_string()),
17185 filter_text: Some("some".to_string()),
17186 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17187 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17188 range: lsp::Range {
17189 start: lsp::Position {
17190 line: 0,
17191 character: 22,
17192 },
17193 end: lsp::Position {
17194 line: 0,
17195 character: 22,
17196 },
17197 },
17198 new_text: "Some(2)".to_string(),
17199 })),
17200 additional_text_edits: Some(vec![lsp::TextEdit {
17201 range: lsp::Range {
17202 start: lsp::Position {
17203 line: 0,
17204 character: 20,
17205 },
17206 end: lsp::Position {
17207 line: 0,
17208 character: 22,
17209 },
17210 },
17211 new_text: "".to_string(),
17212 }]),
17213 ..Default::default()
17214 };
17215
17216 let closure_completion_item = completion_item.clone();
17217 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17218 let task_completion_item = closure_completion_item.clone();
17219 async move {
17220 Ok(Some(lsp::CompletionResponse::Array(vec![
17221 task_completion_item,
17222 ])))
17223 }
17224 });
17225
17226 request.next().await;
17227
17228 cx.condition(|editor, _| editor.context_menu_visible())
17229 .await;
17230 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17231 editor
17232 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17233 .unwrap()
17234 });
17235 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17236
17237 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17238 let task_completion_item = completion_item.clone();
17239 async move { Ok(task_completion_item) }
17240 })
17241 .next()
17242 .await
17243 .unwrap();
17244 apply_additional_edits.await.unwrap();
17245 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17246}
17247
17248#[gpui::test]
17249async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17250 init_test(cx, |_| {});
17251
17252 let mut cx = EditorLspTestContext::new_rust(
17253 lsp::ServerCapabilities {
17254 completion_provider: Some(lsp::CompletionOptions {
17255 trigger_characters: Some(vec![".".to_string()]),
17256 resolve_provider: Some(true),
17257 ..Default::default()
17258 }),
17259 ..Default::default()
17260 },
17261 cx,
17262 )
17263 .await;
17264
17265 cx.set_state("fn main() { let a = 2ˇ; }");
17266 cx.simulate_keystroke(".");
17267
17268 let item1 = lsp::CompletionItem {
17269 label: "method id()".to_string(),
17270 filter_text: Some("id".to_string()),
17271 detail: None,
17272 documentation: None,
17273 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17274 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17275 new_text: ".id".to_string(),
17276 })),
17277 ..lsp::CompletionItem::default()
17278 };
17279
17280 let item2 = lsp::CompletionItem {
17281 label: "other".to_string(),
17282 filter_text: Some("other".to_string()),
17283 detail: None,
17284 documentation: None,
17285 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17286 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17287 new_text: ".other".to_string(),
17288 })),
17289 ..lsp::CompletionItem::default()
17290 };
17291
17292 let item1 = item1.clone();
17293 cx.set_request_handler::<lsp::request::Completion, _, _>({
17294 let item1 = item1.clone();
17295 move |_, _, _| {
17296 let item1 = item1.clone();
17297 let item2 = item2.clone();
17298 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17299 }
17300 })
17301 .next()
17302 .await;
17303
17304 cx.condition(|editor, _| editor.context_menu_visible())
17305 .await;
17306 cx.update_editor(|editor, _, _| {
17307 let context_menu = editor.context_menu.borrow_mut();
17308 let context_menu = context_menu
17309 .as_ref()
17310 .expect("Should have the context menu deployed");
17311 match context_menu {
17312 CodeContextMenu::Completions(completions_menu) => {
17313 let completions = completions_menu.completions.borrow_mut();
17314 assert_eq!(
17315 completions
17316 .iter()
17317 .map(|completion| &completion.label.text)
17318 .collect::<Vec<_>>(),
17319 vec!["method id()", "other"]
17320 )
17321 }
17322 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17323 }
17324 });
17325
17326 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17327 let item1 = item1.clone();
17328 move |_, item_to_resolve, _| {
17329 let item1 = item1.clone();
17330 async move {
17331 if item1 == item_to_resolve {
17332 Ok(lsp::CompletionItem {
17333 label: "method id()".to_string(),
17334 filter_text: Some("id".to_string()),
17335 detail: Some("Now resolved!".to_string()),
17336 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17337 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17338 range: lsp::Range::new(
17339 lsp::Position::new(0, 22),
17340 lsp::Position::new(0, 22),
17341 ),
17342 new_text: ".id".to_string(),
17343 })),
17344 ..lsp::CompletionItem::default()
17345 })
17346 } else {
17347 Ok(item_to_resolve)
17348 }
17349 }
17350 }
17351 })
17352 .next()
17353 .await
17354 .unwrap();
17355 cx.run_until_parked();
17356
17357 cx.update_editor(|editor, window, cx| {
17358 editor.context_menu_next(&Default::default(), window, cx);
17359 });
17360
17361 cx.update_editor(|editor, _, _| {
17362 let context_menu = editor.context_menu.borrow_mut();
17363 let context_menu = context_menu
17364 .as_ref()
17365 .expect("Should have the context menu deployed");
17366 match context_menu {
17367 CodeContextMenu::Completions(completions_menu) => {
17368 let completions = completions_menu.completions.borrow_mut();
17369 assert_eq!(
17370 completions
17371 .iter()
17372 .map(|completion| &completion.label.text)
17373 .collect::<Vec<_>>(),
17374 vec!["method id() Now resolved!", "other"],
17375 "Should update first completion label, but not second as the filter text did not match."
17376 );
17377 }
17378 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17379 }
17380 });
17381}
17382
17383#[gpui::test]
17384async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17385 init_test(cx, |_| {});
17386 let mut cx = EditorLspTestContext::new_rust(
17387 lsp::ServerCapabilities {
17388 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17389 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17390 completion_provider: Some(lsp::CompletionOptions {
17391 resolve_provider: Some(true),
17392 ..Default::default()
17393 }),
17394 ..Default::default()
17395 },
17396 cx,
17397 )
17398 .await;
17399 cx.set_state(indoc! {"
17400 struct TestStruct {
17401 field: i32
17402 }
17403
17404 fn mainˇ() {
17405 let unused_var = 42;
17406 let test_struct = TestStruct { field: 42 };
17407 }
17408 "});
17409 let symbol_range = cx.lsp_range(indoc! {"
17410 struct TestStruct {
17411 field: i32
17412 }
17413
17414 «fn main»() {
17415 let unused_var = 42;
17416 let test_struct = TestStruct { field: 42 };
17417 }
17418 "});
17419 let mut hover_requests =
17420 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17421 Ok(Some(lsp::Hover {
17422 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17423 kind: lsp::MarkupKind::Markdown,
17424 value: "Function documentation".to_string(),
17425 }),
17426 range: Some(symbol_range),
17427 }))
17428 });
17429
17430 // Case 1: Test that code action menu hide hover popover
17431 cx.dispatch_action(Hover);
17432 hover_requests.next().await;
17433 cx.condition(|editor, _| editor.hover_state.visible()).await;
17434 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17435 move |_, _, _| async move {
17436 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17437 lsp::CodeAction {
17438 title: "Remove unused variable".to_string(),
17439 kind: Some(CodeActionKind::QUICKFIX),
17440 edit: Some(lsp::WorkspaceEdit {
17441 changes: Some(
17442 [(
17443 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17444 vec![lsp::TextEdit {
17445 range: lsp::Range::new(
17446 lsp::Position::new(5, 4),
17447 lsp::Position::new(5, 27),
17448 ),
17449 new_text: "".to_string(),
17450 }],
17451 )]
17452 .into_iter()
17453 .collect(),
17454 ),
17455 ..Default::default()
17456 }),
17457 ..Default::default()
17458 },
17459 )]))
17460 },
17461 );
17462 cx.update_editor(|editor, window, cx| {
17463 editor.toggle_code_actions(
17464 &ToggleCodeActions {
17465 deployed_from: None,
17466 quick_launch: false,
17467 },
17468 window,
17469 cx,
17470 );
17471 });
17472 code_action_requests.next().await;
17473 cx.run_until_parked();
17474 cx.condition(|editor, _| editor.context_menu_visible())
17475 .await;
17476 cx.update_editor(|editor, _, _| {
17477 assert!(
17478 !editor.hover_state.visible(),
17479 "Hover popover should be hidden when code action menu is shown"
17480 );
17481 // Hide code actions
17482 editor.context_menu.take();
17483 });
17484
17485 // Case 2: Test that code completions hide hover popover
17486 cx.dispatch_action(Hover);
17487 hover_requests.next().await;
17488 cx.condition(|editor, _| editor.hover_state.visible()).await;
17489 let counter = Arc::new(AtomicUsize::new(0));
17490 let mut completion_requests =
17491 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17492 let counter = counter.clone();
17493 async move {
17494 counter.fetch_add(1, atomic::Ordering::Release);
17495 Ok(Some(lsp::CompletionResponse::Array(vec![
17496 lsp::CompletionItem {
17497 label: "main".into(),
17498 kind: Some(lsp::CompletionItemKind::FUNCTION),
17499 detail: Some("() -> ()".to_string()),
17500 ..Default::default()
17501 },
17502 lsp::CompletionItem {
17503 label: "TestStruct".into(),
17504 kind: Some(lsp::CompletionItemKind::STRUCT),
17505 detail: Some("struct TestStruct".to_string()),
17506 ..Default::default()
17507 },
17508 ])))
17509 }
17510 });
17511 cx.update_editor(|editor, window, cx| {
17512 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17513 });
17514 completion_requests.next().await;
17515 cx.condition(|editor, _| editor.context_menu_visible())
17516 .await;
17517 cx.update_editor(|editor, _, _| {
17518 assert!(
17519 !editor.hover_state.visible(),
17520 "Hover popover should be hidden when completion menu is shown"
17521 );
17522 });
17523}
17524
17525#[gpui::test]
17526async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17527 init_test(cx, |_| {});
17528
17529 let mut cx = EditorLspTestContext::new_rust(
17530 lsp::ServerCapabilities {
17531 completion_provider: Some(lsp::CompletionOptions {
17532 trigger_characters: Some(vec![".".to_string()]),
17533 resolve_provider: Some(true),
17534 ..Default::default()
17535 }),
17536 ..Default::default()
17537 },
17538 cx,
17539 )
17540 .await;
17541
17542 cx.set_state("fn main() { let a = 2ˇ; }");
17543 cx.simulate_keystroke(".");
17544
17545 let unresolved_item_1 = lsp::CompletionItem {
17546 label: "id".to_string(),
17547 filter_text: Some("id".to_string()),
17548 detail: None,
17549 documentation: None,
17550 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17551 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17552 new_text: ".id".to_string(),
17553 })),
17554 ..lsp::CompletionItem::default()
17555 };
17556 let resolved_item_1 = lsp::CompletionItem {
17557 additional_text_edits: Some(vec![lsp::TextEdit {
17558 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17559 new_text: "!!".to_string(),
17560 }]),
17561 ..unresolved_item_1.clone()
17562 };
17563 let unresolved_item_2 = lsp::CompletionItem {
17564 label: "other".to_string(),
17565 filter_text: Some("other".to_string()),
17566 detail: None,
17567 documentation: None,
17568 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17569 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17570 new_text: ".other".to_string(),
17571 })),
17572 ..lsp::CompletionItem::default()
17573 };
17574 let resolved_item_2 = lsp::CompletionItem {
17575 additional_text_edits: Some(vec![lsp::TextEdit {
17576 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17577 new_text: "??".to_string(),
17578 }]),
17579 ..unresolved_item_2.clone()
17580 };
17581
17582 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17583 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17584 cx.lsp
17585 .server
17586 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17587 let unresolved_item_1 = unresolved_item_1.clone();
17588 let resolved_item_1 = resolved_item_1.clone();
17589 let unresolved_item_2 = unresolved_item_2.clone();
17590 let resolved_item_2 = resolved_item_2.clone();
17591 let resolve_requests_1 = resolve_requests_1.clone();
17592 let resolve_requests_2 = resolve_requests_2.clone();
17593 move |unresolved_request, _| {
17594 let unresolved_item_1 = unresolved_item_1.clone();
17595 let resolved_item_1 = resolved_item_1.clone();
17596 let unresolved_item_2 = unresolved_item_2.clone();
17597 let resolved_item_2 = resolved_item_2.clone();
17598 let resolve_requests_1 = resolve_requests_1.clone();
17599 let resolve_requests_2 = resolve_requests_2.clone();
17600 async move {
17601 if unresolved_request == unresolved_item_1 {
17602 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17603 Ok(resolved_item_1.clone())
17604 } else if unresolved_request == unresolved_item_2 {
17605 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17606 Ok(resolved_item_2.clone())
17607 } else {
17608 panic!("Unexpected completion item {unresolved_request:?}")
17609 }
17610 }
17611 }
17612 })
17613 .detach();
17614
17615 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17616 let unresolved_item_1 = unresolved_item_1.clone();
17617 let unresolved_item_2 = unresolved_item_2.clone();
17618 async move {
17619 Ok(Some(lsp::CompletionResponse::Array(vec![
17620 unresolved_item_1,
17621 unresolved_item_2,
17622 ])))
17623 }
17624 })
17625 .next()
17626 .await;
17627
17628 cx.condition(|editor, _| editor.context_menu_visible())
17629 .await;
17630 cx.update_editor(|editor, _, _| {
17631 let context_menu = editor.context_menu.borrow_mut();
17632 let context_menu = context_menu
17633 .as_ref()
17634 .expect("Should have the context menu deployed");
17635 match context_menu {
17636 CodeContextMenu::Completions(completions_menu) => {
17637 let completions = completions_menu.completions.borrow_mut();
17638 assert_eq!(
17639 completions
17640 .iter()
17641 .map(|completion| &completion.label.text)
17642 .collect::<Vec<_>>(),
17643 vec!["id", "other"]
17644 )
17645 }
17646 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17647 }
17648 });
17649 cx.run_until_parked();
17650
17651 cx.update_editor(|editor, window, cx| {
17652 editor.context_menu_next(&ContextMenuNext, window, cx);
17653 });
17654 cx.run_until_parked();
17655 cx.update_editor(|editor, window, cx| {
17656 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17657 });
17658 cx.run_until_parked();
17659 cx.update_editor(|editor, window, cx| {
17660 editor.context_menu_next(&ContextMenuNext, window, cx);
17661 });
17662 cx.run_until_parked();
17663 cx.update_editor(|editor, window, cx| {
17664 editor
17665 .compose_completion(&ComposeCompletion::default(), window, cx)
17666 .expect("No task returned")
17667 })
17668 .await
17669 .expect("Completion failed");
17670 cx.run_until_parked();
17671
17672 cx.update_editor(|editor, _, cx| {
17673 assert_eq!(
17674 resolve_requests_1.load(atomic::Ordering::Acquire),
17675 1,
17676 "Should always resolve once despite multiple selections"
17677 );
17678 assert_eq!(
17679 resolve_requests_2.load(atomic::Ordering::Acquire),
17680 1,
17681 "Should always resolve once after multiple selections and applying the completion"
17682 );
17683 assert_eq!(
17684 editor.text(cx),
17685 "fn main() { let a = ??.other; }",
17686 "Should use resolved data when applying the completion"
17687 );
17688 });
17689}
17690
17691#[gpui::test]
17692async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17693 init_test(cx, |_| {});
17694
17695 let item_0 = lsp::CompletionItem {
17696 label: "abs".into(),
17697 insert_text: Some("abs".into()),
17698 data: Some(json!({ "very": "special"})),
17699 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17700 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17701 lsp::InsertReplaceEdit {
17702 new_text: "abs".to_string(),
17703 insert: lsp::Range::default(),
17704 replace: lsp::Range::default(),
17705 },
17706 )),
17707 ..lsp::CompletionItem::default()
17708 };
17709 let items = iter::once(item_0.clone())
17710 .chain((11..51).map(|i| lsp::CompletionItem {
17711 label: format!("item_{}", i),
17712 insert_text: Some(format!("item_{}", i)),
17713 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17714 ..lsp::CompletionItem::default()
17715 }))
17716 .collect::<Vec<_>>();
17717
17718 let default_commit_characters = vec!["?".to_string()];
17719 let default_data = json!({ "default": "data"});
17720 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17721 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17722 let default_edit_range = lsp::Range {
17723 start: lsp::Position {
17724 line: 0,
17725 character: 5,
17726 },
17727 end: lsp::Position {
17728 line: 0,
17729 character: 5,
17730 },
17731 };
17732
17733 let mut cx = EditorLspTestContext::new_rust(
17734 lsp::ServerCapabilities {
17735 completion_provider: Some(lsp::CompletionOptions {
17736 trigger_characters: Some(vec![".".to_string()]),
17737 resolve_provider: Some(true),
17738 ..Default::default()
17739 }),
17740 ..Default::default()
17741 },
17742 cx,
17743 )
17744 .await;
17745
17746 cx.set_state("fn main() { let a = 2ˇ; }");
17747 cx.simulate_keystroke(".");
17748
17749 let completion_data = default_data.clone();
17750 let completion_characters = default_commit_characters.clone();
17751 let completion_items = items.clone();
17752 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17753 let default_data = completion_data.clone();
17754 let default_commit_characters = completion_characters.clone();
17755 let items = completion_items.clone();
17756 async move {
17757 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17758 items,
17759 item_defaults: Some(lsp::CompletionListItemDefaults {
17760 data: Some(default_data.clone()),
17761 commit_characters: Some(default_commit_characters.clone()),
17762 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17763 default_edit_range,
17764 )),
17765 insert_text_format: Some(default_insert_text_format),
17766 insert_text_mode: Some(default_insert_text_mode),
17767 }),
17768 ..lsp::CompletionList::default()
17769 })))
17770 }
17771 })
17772 .next()
17773 .await;
17774
17775 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17776 cx.lsp
17777 .server
17778 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17779 let closure_resolved_items = resolved_items.clone();
17780 move |item_to_resolve, _| {
17781 let closure_resolved_items = closure_resolved_items.clone();
17782 async move {
17783 closure_resolved_items.lock().push(item_to_resolve.clone());
17784 Ok(item_to_resolve)
17785 }
17786 }
17787 })
17788 .detach();
17789
17790 cx.condition(|editor, _| editor.context_menu_visible())
17791 .await;
17792 cx.run_until_parked();
17793 cx.update_editor(|editor, _, _| {
17794 let menu = editor.context_menu.borrow_mut();
17795 match menu.as_ref().expect("should have the completions menu") {
17796 CodeContextMenu::Completions(completions_menu) => {
17797 assert_eq!(
17798 completions_menu
17799 .entries
17800 .borrow()
17801 .iter()
17802 .map(|mat| mat.string.clone())
17803 .collect::<Vec<String>>(),
17804 items
17805 .iter()
17806 .map(|completion| completion.label.clone())
17807 .collect::<Vec<String>>()
17808 );
17809 }
17810 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17811 }
17812 });
17813 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17814 // with 4 from the end.
17815 assert_eq!(
17816 *resolved_items.lock(),
17817 [&items[0..16], &items[items.len() - 4..items.len()]]
17818 .concat()
17819 .iter()
17820 .cloned()
17821 .map(|mut item| {
17822 if item.data.is_none() {
17823 item.data = Some(default_data.clone());
17824 }
17825 item
17826 })
17827 .collect::<Vec<lsp::CompletionItem>>(),
17828 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17829 );
17830 resolved_items.lock().clear();
17831
17832 cx.update_editor(|editor, window, cx| {
17833 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17834 });
17835 cx.run_until_parked();
17836 // Completions that have already been resolved are skipped.
17837 assert_eq!(
17838 *resolved_items.lock(),
17839 items[items.len() - 17..items.len() - 4]
17840 .iter()
17841 .cloned()
17842 .map(|mut item| {
17843 if item.data.is_none() {
17844 item.data = Some(default_data.clone());
17845 }
17846 item
17847 })
17848 .collect::<Vec<lsp::CompletionItem>>()
17849 );
17850 resolved_items.lock().clear();
17851}
17852
17853#[gpui::test]
17854async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17855 init_test(cx, |_| {});
17856
17857 let mut cx = EditorLspTestContext::new(
17858 Language::new(
17859 LanguageConfig {
17860 matcher: LanguageMatcher {
17861 path_suffixes: vec!["jsx".into()],
17862 ..Default::default()
17863 },
17864 overrides: [(
17865 "element".into(),
17866 LanguageConfigOverride {
17867 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17868 ..Default::default()
17869 },
17870 )]
17871 .into_iter()
17872 .collect(),
17873 ..Default::default()
17874 },
17875 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17876 )
17877 .with_override_query("(jsx_self_closing_element) @element")
17878 .unwrap(),
17879 lsp::ServerCapabilities {
17880 completion_provider: Some(lsp::CompletionOptions {
17881 trigger_characters: Some(vec![":".to_string()]),
17882 ..Default::default()
17883 }),
17884 ..Default::default()
17885 },
17886 cx,
17887 )
17888 .await;
17889
17890 cx.lsp
17891 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17892 Ok(Some(lsp::CompletionResponse::Array(vec![
17893 lsp::CompletionItem {
17894 label: "bg-blue".into(),
17895 ..Default::default()
17896 },
17897 lsp::CompletionItem {
17898 label: "bg-red".into(),
17899 ..Default::default()
17900 },
17901 lsp::CompletionItem {
17902 label: "bg-yellow".into(),
17903 ..Default::default()
17904 },
17905 ])))
17906 });
17907
17908 cx.set_state(r#"<p class="bgˇ" />"#);
17909
17910 // Trigger completion when typing a dash, because the dash is an extra
17911 // word character in the 'element' scope, which contains the cursor.
17912 cx.simulate_keystroke("-");
17913 cx.executor().run_until_parked();
17914 cx.update_editor(|editor, _, _| {
17915 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17916 {
17917 assert_eq!(
17918 completion_menu_entries(menu),
17919 &["bg-blue", "bg-red", "bg-yellow"]
17920 );
17921 } else {
17922 panic!("expected completion menu to be open");
17923 }
17924 });
17925
17926 cx.simulate_keystroke("l");
17927 cx.executor().run_until_parked();
17928 cx.update_editor(|editor, _, _| {
17929 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17930 {
17931 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17932 } else {
17933 panic!("expected completion menu to be open");
17934 }
17935 });
17936
17937 // When filtering completions, consider the character after the '-' to
17938 // be the start of a subword.
17939 cx.set_state(r#"<p class="yelˇ" />"#);
17940 cx.simulate_keystroke("l");
17941 cx.executor().run_until_parked();
17942 cx.update_editor(|editor, _, _| {
17943 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17944 {
17945 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17946 } else {
17947 panic!("expected completion menu to be open");
17948 }
17949 });
17950}
17951
17952fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17953 let entries = menu.entries.borrow();
17954 entries.iter().map(|mat| mat.string.clone()).collect()
17955}
17956
17957#[gpui::test]
17958async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17959 init_test(cx, |settings| {
17960 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17961 Formatter::Prettier,
17962 )))
17963 });
17964
17965 let fs = FakeFs::new(cx.executor());
17966 fs.insert_file(path!("/file.ts"), Default::default()).await;
17967
17968 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17969 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17970
17971 language_registry.add(Arc::new(Language::new(
17972 LanguageConfig {
17973 name: "TypeScript".into(),
17974 matcher: LanguageMatcher {
17975 path_suffixes: vec!["ts".to_string()],
17976 ..Default::default()
17977 },
17978 ..Default::default()
17979 },
17980 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17981 )));
17982 update_test_language_settings(cx, |settings| {
17983 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
17984 });
17985
17986 let test_plugin = "test_plugin";
17987 let _ = language_registry.register_fake_lsp(
17988 "TypeScript",
17989 FakeLspAdapter {
17990 prettier_plugins: vec![test_plugin],
17991 ..Default::default()
17992 },
17993 );
17994
17995 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17996 let buffer = project
17997 .update(cx, |project, cx| {
17998 project.open_local_buffer(path!("/file.ts"), cx)
17999 })
18000 .await
18001 .unwrap();
18002
18003 let buffer_text = "one\ntwo\nthree\n";
18004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18005 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18006 editor.update_in(cx, |editor, window, cx| {
18007 editor.set_text(buffer_text, window, cx)
18008 });
18009
18010 editor
18011 .update_in(cx, |editor, window, cx| {
18012 editor.perform_format(
18013 project.clone(),
18014 FormatTrigger::Manual,
18015 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18016 window,
18017 cx,
18018 )
18019 })
18020 .unwrap()
18021 .await;
18022 assert_eq!(
18023 editor.update(cx, |editor, cx| editor.text(cx)),
18024 buffer_text.to_string() + prettier_format_suffix,
18025 "Test prettier formatting was not applied to the original buffer text",
18026 );
18027
18028 update_test_language_settings(cx, |settings| {
18029 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18030 });
18031 let format = editor.update_in(cx, |editor, window, cx| {
18032 editor.perform_format(
18033 project.clone(),
18034 FormatTrigger::Manual,
18035 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18036 window,
18037 cx,
18038 )
18039 });
18040 format.await.unwrap();
18041 assert_eq!(
18042 editor.update(cx, |editor, cx| editor.text(cx)),
18043 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18044 "Autoformatting (via test prettier) was not applied to the original buffer text",
18045 );
18046}
18047
18048#[gpui::test]
18049async fn test_addition_reverts(cx: &mut TestAppContext) {
18050 init_test(cx, |_| {});
18051 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18052 let base_text = indoc! {r#"
18053 struct Row;
18054 struct Row1;
18055 struct Row2;
18056
18057 struct Row4;
18058 struct Row5;
18059 struct Row6;
18060
18061 struct Row8;
18062 struct Row9;
18063 struct Row10;"#};
18064
18065 // When addition hunks are not adjacent to carets, no hunk revert is performed
18066 assert_hunk_revert(
18067 indoc! {r#"struct Row;
18068 struct Row1;
18069 struct Row1.1;
18070 struct Row1.2;
18071 struct Row2;ˇ
18072
18073 struct Row4;
18074 struct Row5;
18075 struct Row6;
18076
18077 struct Row8;
18078 ˇstruct Row9;
18079 struct Row9.1;
18080 struct Row9.2;
18081 struct Row9.3;
18082 struct Row10;"#},
18083 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18084 indoc! {r#"struct Row;
18085 struct Row1;
18086 struct Row1.1;
18087 struct Row1.2;
18088 struct Row2;ˇ
18089
18090 struct Row4;
18091 struct Row5;
18092 struct Row6;
18093
18094 struct Row8;
18095 ˇstruct Row9;
18096 struct Row9.1;
18097 struct Row9.2;
18098 struct Row9.3;
18099 struct Row10;"#},
18100 base_text,
18101 &mut cx,
18102 );
18103 // Same for selections
18104 assert_hunk_revert(
18105 indoc! {r#"struct Row;
18106 struct Row1;
18107 struct Row2;
18108 struct Row2.1;
18109 struct Row2.2;
18110 «ˇ
18111 struct Row4;
18112 struct» Row5;
18113 «struct Row6;
18114 ˇ»
18115 struct Row9.1;
18116 struct Row9.2;
18117 struct Row9.3;
18118 struct Row8;
18119 struct Row9;
18120 struct Row10;"#},
18121 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18122 indoc! {r#"struct Row;
18123 struct Row1;
18124 struct Row2;
18125 struct Row2.1;
18126 struct Row2.2;
18127 «ˇ
18128 struct Row4;
18129 struct» Row5;
18130 «struct Row6;
18131 ˇ»
18132 struct Row9.1;
18133 struct Row9.2;
18134 struct Row9.3;
18135 struct Row8;
18136 struct Row9;
18137 struct Row10;"#},
18138 base_text,
18139 &mut cx,
18140 );
18141
18142 // When carets and selections intersect the addition hunks, those are reverted.
18143 // Adjacent carets got merged.
18144 assert_hunk_revert(
18145 indoc! {r#"struct Row;
18146 ˇ// something on the top
18147 struct Row1;
18148 struct Row2;
18149 struct Roˇw3.1;
18150 struct Row2.2;
18151 struct Row2.3;ˇ
18152
18153 struct Row4;
18154 struct ˇRow5.1;
18155 struct Row5.2;
18156 struct «Rowˇ»5.3;
18157 struct Row5;
18158 struct Row6;
18159 ˇ
18160 struct Row9.1;
18161 struct «Rowˇ»9.2;
18162 struct «ˇRow»9.3;
18163 struct Row8;
18164 struct Row9;
18165 «ˇ// something on bottom»
18166 struct Row10;"#},
18167 vec![
18168 DiffHunkStatusKind::Added,
18169 DiffHunkStatusKind::Added,
18170 DiffHunkStatusKind::Added,
18171 DiffHunkStatusKind::Added,
18172 DiffHunkStatusKind::Added,
18173 ],
18174 indoc! {r#"struct Row;
18175 ˇstruct Row1;
18176 struct Row2;
18177 ˇ
18178 struct Row4;
18179 ˇstruct Row5;
18180 struct Row6;
18181 ˇ
18182 ˇstruct Row8;
18183 struct Row9;
18184 ˇstruct Row10;"#},
18185 base_text,
18186 &mut cx,
18187 );
18188}
18189
18190#[gpui::test]
18191async fn test_modification_reverts(cx: &mut TestAppContext) {
18192 init_test(cx, |_| {});
18193 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18194 let base_text = indoc! {r#"
18195 struct Row;
18196 struct Row1;
18197 struct Row2;
18198
18199 struct Row4;
18200 struct Row5;
18201 struct Row6;
18202
18203 struct Row8;
18204 struct Row9;
18205 struct Row10;"#};
18206
18207 // Modification hunks behave the same as the addition ones.
18208 assert_hunk_revert(
18209 indoc! {r#"struct Row;
18210 struct Row1;
18211 struct Row33;
18212 ˇ
18213 struct Row4;
18214 struct Row5;
18215 struct Row6;
18216 ˇ
18217 struct Row99;
18218 struct Row9;
18219 struct Row10;"#},
18220 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18221 indoc! {r#"struct Row;
18222 struct Row1;
18223 struct Row33;
18224 ˇ
18225 struct Row4;
18226 struct Row5;
18227 struct Row6;
18228 ˇ
18229 struct Row99;
18230 struct Row9;
18231 struct Row10;"#},
18232 base_text,
18233 &mut cx,
18234 );
18235 assert_hunk_revert(
18236 indoc! {r#"struct Row;
18237 struct Row1;
18238 struct Row33;
18239 «ˇ
18240 struct Row4;
18241 struct» Row5;
18242 «struct Row6;
18243 ˇ»
18244 struct Row99;
18245 struct Row9;
18246 struct Row10;"#},
18247 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18248 indoc! {r#"struct Row;
18249 struct Row1;
18250 struct Row33;
18251 «ˇ
18252 struct Row4;
18253 struct» Row5;
18254 «struct Row6;
18255 ˇ»
18256 struct Row99;
18257 struct Row9;
18258 struct Row10;"#},
18259 base_text,
18260 &mut cx,
18261 );
18262
18263 assert_hunk_revert(
18264 indoc! {r#"ˇstruct Row1.1;
18265 struct Row1;
18266 «ˇstr»uct Row22;
18267
18268 struct ˇRow44;
18269 struct Row5;
18270 struct «Rˇ»ow66;ˇ
18271
18272 «struˇ»ct Row88;
18273 struct Row9;
18274 struct Row1011;ˇ"#},
18275 vec![
18276 DiffHunkStatusKind::Modified,
18277 DiffHunkStatusKind::Modified,
18278 DiffHunkStatusKind::Modified,
18279 DiffHunkStatusKind::Modified,
18280 DiffHunkStatusKind::Modified,
18281 DiffHunkStatusKind::Modified,
18282 ],
18283 indoc! {r#"struct Row;
18284 ˇstruct Row1;
18285 struct Row2;
18286 ˇ
18287 struct Row4;
18288 ˇstruct Row5;
18289 struct Row6;
18290 ˇ
18291 struct Row8;
18292 ˇstruct Row9;
18293 struct Row10;ˇ"#},
18294 base_text,
18295 &mut cx,
18296 );
18297}
18298
18299#[gpui::test]
18300async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18301 init_test(cx, |_| {});
18302 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18303 let base_text = indoc! {r#"
18304 one
18305
18306 two
18307 three
18308 "#};
18309
18310 cx.set_head_text(base_text);
18311 cx.set_state("\nˇ\n");
18312 cx.executor().run_until_parked();
18313 cx.update_editor(|editor, _window, cx| {
18314 editor.expand_selected_diff_hunks(cx);
18315 });
18316 cx.executor().run_until_parked();
18317 cx.update_editor(|editor, window, cx| {
18318 editor.backspace(&Default::default(), window, cx);
18319 });
18320 cx.run_until_parked();
18321 cx.assert_state_with_diff(
18322 indoc! {r#"
18323
18324 - two
18325 - threeˇ
18326 +
18327 "#}
18328 .to_string(),
18329 );
18330}
18331
18332#[gpui::test]
18333async fn test_deletion_reverts(cx: &mut TestAppContext) {
18334 init_test(cx, |_| {});
18335 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18336 let base_text = indoc! {r#"struct Row;
18337struct Row1;
18338struct Row2;
18339
18340struct Row4;
18341struct Row5;
18342struct Row6;
18343
18344struct Row8;
18345struct Row9;
18346struct Row10;"#};
18347
18348 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18349 assert_hunk_revert(
18350 indoc! {r#"struct Row;
18351 struct Row2;
18352
18353 ˇstruct Row4;
18354 struct Row5;
18355 struct Row6;
18356 ˇ
18357 struct Row8;
18358 struct Row10;"#},
18359 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18360 indoc! {r#"struct Row;
18361 struct Row2;
18362
18363 ˇstruct Row4;
18364 struct Row5;
18365 struct Row6;
18366 ˇ
18367 struct Row8;
18368 struct Row10;"#},
18369 base_text,
18370 &mut cx,
18371 );
18372 assert_hunk_revert(
18373 indoc! {r#"struct Row;
18374 struct Row2;
18375
18376 «ˇstruct Row4;
18377 struct» Row5;
18378 «struct Row6;
18379 ˇ»
18380 struct Row8;
18381 struct Row10;"#},
18382 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18383 indoc! {r#"struct Row;
18384 struct Row2;
18385
18386 «ˇstruct Row4;
18387 struct» Row5;
18388 «struct Row6;
18389 ˇ»
18390 struct Row8;
18391 struct Row10;"#},
18392 base_text,
18393 &mut cx,
18394 );
18395
18396 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18397 assert_hunk_revert(
18398 indoc! {r#"struct Row;
18399 ˇstruct Row2;
18400
18401 struct Row4;
18402 struct Row5;
18403 struct Row6;
18404
18405 struct Row8;ˇ
18406 struct Row10;"#},
18407 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18408 indoc! {r#"struct Row;
18409 struct Row1;
18410 ˇstruct Row2;
18411
18412 struct Row4;
18413 struct Row5;
18414 struct Row6;
18415
18416 struct Row8;ˇ
18417 struct Row9;
18418 struct Row10;"#},
18419 base_text,
18420 &mut cx,
18421 );
18422 assert_hunk_revert(
18423 indoc! {r#"struct Row;
18424 struct Row2«ˇ;
18425 struct Row4;
18426 struct» Row5;
18427 «struct Row6;
18428
18429 struct Row8;ˇ»
18430 struct Row10;"#},
18431 vec![
18432 DiffHunkStatusKind::Deleted,
18433 DiffHunkStatusKind::Deleted,
18434 DiffHunkStatusKind::Deleted,
18435 ],
18436 indoc! {r#"struct Row;
18437 struct Row1;
18438 struct Row2«ˇ;
18439
18440 struct Row4;
18441 struct» Row5;
18442 «struct Row6;
18443
18444 struct Row8;ˇ»
18445 struct Row9;
18446 struct Row10;"#},
18447 base_text,
18448 &mut cx,
18449 );
18450}
18451
18452#[gpui::test]
18453async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18454 init_test(cx, |_| {});
18455
18456 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18457 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18458 let base_text_3 =
18459 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18460
18461 let text_1 = edit_first_char_of_every_line(base_text_1);
18462 let text_2 = edit_first_char_of_every_line(base_text_2);
18463 let text_3 = edit_first_char_of_every_line(base_text_3);
18464
18465 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18466 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18467 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18468
18469 let multibuffer = cx.new(|cx| {
18470 let mut multibuffer = MultiBuffer::new(ReadWrite);
18471 multibuffer.push_excerpts(
18472 buffer_1.clone(),
18473 [
18474 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18475 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18476 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18477 ],
18478 cx,
18479 );
18480 multibuffer.push_excerpts(
18481 buffer_2.clone(),
18482 [
18483 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18484 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18485 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18486 ],
18487 cx,
18488 );
18489 multibuffer.push_excerpts(
18490 buffer_3.clone(),
18491 [
18492 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18493 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18494 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18495 ],
18496 cx,
18497 );
18498 multibuffer
18499 });
18500
18501 let fs = FakeFs::new(cx.executor());
18502 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18503 let (editor, cx) = cx
18504 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18505 editor.update_in(cx, |editor, _window, cx| {
18506 for (buffer, diff_base) in [
18507 (buffer_1.clone(), base_text_1),
18508 (buffer_2.clone(), base_text_2),
18509 (buffer_3.clone(), base_text_3),
18510 ] {
18511 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18512 editor
18513 .buffer
18514 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18515 }
18516 });
18517 cx.executor().run_until_parked();
18518
18519 editor.update_in(cx, |editor, window, cx| {
18520 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}");
18521 editor.select_all(&SelectAll, window, cx);
18522 editor.git_restore(&Default::default(), window, cx);
18523 });
18524 cx.executor().run_until_parked();
18525
18526 // When all ranges are selected, all buffer hunks are reverted.
18527 editor.update(cx, |editor, cx| {
18528 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");
18529 });
18530 buffer_1.update(cx, |buffer, _| {
18531 assert_eq!(buffer.text(), base_text_1);
18532 });
18533 buffer_2.update(cx, |buffer, _| {
18534 assert_eq!(buffer.text(), base_text_2);
18535 });
18536 buffer_3.update(cx, |buffer, _| {
18537 assert_eq!(buffer.text(), base_text_3);
18538 });
18539
18540 editor.update_in(cx, |editor, window, cx| {
18541 editor.undo(&Default::default(), window, cx);
18542 });
18543
18544 editor.update_in(cx, |editor, window, cx| {
18545 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18546 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18547 });
18548 editor.git_restore(&Default::default(), window, cx);
18549 });
18550
18551 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18552 // but not affect buffer_2 and its related excerpts.
18553 editor.update(cx, |editor, cx| {
18554 assert_eq!(
18555 editor.text(cx),
18556 "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}"
18557 );
18558 });
18559 buffer_1.update(cx, |buffer, _| {
18560 assert_eq!(buffer.text(), base_text_1);
18561 });
18562 buffer_2.update(cx, |buffer, _| {
18563 assert_eq!(
18564 buffer.text(),
18565 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18566 );
18567 });
18568 buffer_3.update(cx, |buffer, _| {
18569 assert_eq!(
18570 buffer.text(),
18571 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18572 );
18573 });
18574
18575 fn edit_first_char_of_every_line(text: &str) -> String {
18576 text.split('\n')
18577 .map(|line| format!("X{}", &line[1..]))
18578 .collect::<Vec<_>>()
18579 .join("\n")
18580 }
18581}
18582
18583#[gpui::test]
18584async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18585 init_test(cx, |_| {});
18586
18587 let cols = 4;
18588 let rows = 10;
18589 let sample_text_1 = sample_text(rows, cols, 'a');
18590 assert_eq!(
18591 sample_text_1,
18592 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18593 );
18594 let sample_text_2 = sample_text(rows, cols, 'l');
18595 assert_eq!(
18596 sample_text_2,
18597 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18598 );
18599 let sample_text_3 = sample_text(rows, cols, 'v');
18600 assert_eq!(
18601 sample_text_3,
18602 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18603 );
18604
18605 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18606 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18607 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18608
18609 let multi_buffer = cx.new(|cx| {
18610 let mut multibuffer = MultiBuffer::new(ReadWrite);
18611 multibuffer.push_excerpts(
18612 buffer_1.clone(),
18613 [
18614 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18615 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18616 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18617 ],
18618 cx,
18619 );
18620 multibuffer.push_excerpts(
18621 buffer_2.clone(),
18622 [
18623 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18624 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18625 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18626 ],
18627 cx,
18628 );
18629 multibuffer.push_excerpts(
18630 buffer_3.clone(),
18631 [
18632 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18633 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18634 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18635 ],
18636 cx,
18637 );
18638 multibuffer
18639 });
18640
18641 let fs = FakeFs::new(cx.executor());
18642 fs.insert_tree(
18643 "/a",
18644 json!({
18645 "main.rs": sample_text_1,
18646 "other.rs": sample_text_2,
18647 "lib.rs": sample_text_3,
18648 }),
18649 )
18650 .await;
18651 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18652 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18653 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18654 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18655 Editor::new(
18656 EditorMode::full(),
18657 multi_buffer,
18658 Some(project.clone()),
18659 window,
18660 cx,
18661 )
18662 });
18663 let multibuffer_item_id = workspace
18664 .update(cx, |workspace, window, cx| {
18665 assert!(
18666 workspace.active_item(cx).is_none(),
18667 "active item should be None before the first item is added"
18668 );
18669 workspace.add_item_to_active_pane(
18670 Box::new(multi_buffer_editor.clone()),
18671 None,
18672 true,
18673 window,
18674 cx,
18675 );
18676 let active_item = workspace
18677 .active_item(cx)
18678 .expect("should have an active item after adding the multi buffer");
18679 assert!(
18680 !active_item.is_singleton(cx),
18681 "A multi buffer was expected to active after adding"
18682 );
18683 active_item.item_id()
18684 })
18685 .unwrap();
18686 cx.executor().run_until_parked();
18687
18688 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18689 editor.change_selections(
18690 SelectionEffects::scroll(Autoscroll::Next),
18691 window,
18692 cx,
18693 |s| s.select_ranges(Some(1..2)),
18694 );
18695 editor.open_excerpts(&OpenExcerpts, window, cx);
18696 });
18697 cx.executor().run_until_parked();
18698 let first_item_id = workspace
18699 .update(cx, |workspace, window, cx| {
18700 let active_item = workspace
18701 .active_item(cx)
18702 .expect("should have an active item after navigating into the 1st buffer");
18703 let first_item_id = active_item.item_id();
18704 assert_ne!(
18705 first_item_id, multibuffer_item_id,
18706 "Should navigate into the 1st buffer and activate it"
18707 );
18708 assert!(
18709 active_item.is_singleton(cx),
18710 "New active item should be a singleton buffer"
18711 );
18712 assert_eq!(
18713 active_item
18714 .act_as::<Editor>(cx)
18715 .expect("should have navigated into an editor for the 1st buffer")
18716 .read(cx)
18717 .text(cx),
18718 sample_text_1
18719 );
18720
18721 workspace
18722 .go_back(workspace.active_pane().downgrade(), window, cx)
18723 .detach_and_log_err(cx);
18724
18725 first_item_id
18726 })
18727 .unwrap();
18728 cx.executor().run_until_parked();
18729 workspace
18730 .update(cx, |workspace, _, cx| {
18731 let active_item = workspace
18732 .active_item(cx)
18733 .expect("should have an active item after navigating back");
18734 assert_eq!(
18735 active_item.item_id(),
18736 multibuffer_item_id,
18737 "Should navigate back to the multi buffer"
18738 );
18739 assert!(!active_item.is_singleton(cx));
18740 })
18741 .unwrap();
18742
18743 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18744 editor.change_selections(
18745 SelectionEffects::scroll(Autoscroll::Next),
18746 window,
18747 cx,
18748 |s| s.select_ranges(Some(39..40)),
18749 );
18750 editor.open_excerpts(&OpenExcerpts, window, cx);
18751 });
18752 cx.executor().run_until_parked();
18753 let second_item_id = workspace
18754 .update(cx, |workspace, window, cx| {
18755 let active_item = workspace
18756 .active_item(cx)
18757 .expect("should have an active item after navigating into the 2nd buffer");
18758 let second_item_id = active_item.item_id();
18759 assert_ne!(
18760 second_item_id, multibuffer_item_id,
18761 "Should navigate away from the multibuffer"
18762 );
18763 assert_ne!(
18764 second_item_id, first_item_id,
18765 "Should navigate into the 2nd buffer and activate it"
18766 );
18767 assert!(
18768 active_item.is_singleton(cx),
18769 "New active item should be a singleton buffer"
18770 );
18771 assert_eq!(
18772 active_item
18773 .act_as::<Editor>(cx)
18774 .expect("should have navigated into an editor")
18775 .read(cx)
18776 .text(cx),
18777 sample_text_2
18778 );
18779
18780 workspace
18781 .go_back(workspace.active_pane().downgrade(), window, cx)
18782 .detach_and_log_err(cx);
18783
18784 second_item_id
18785 })
18786 .unwrap();
18787 cx.executor().run_until_parked();
18788 workspace
18789 .update(cx, |workspace, _, cx| {
18790 let active_item = workspace
18791 .active_item(cx)
18792 .expect("should have an active item after navigating back from the 2nd buffer");
18793 assert_eq!(
18794 active_item.item_id(),
18795 multibuffer_item_id,
18796 "Should navigate back from the 2nd buffer to the multi buffer"
18797 );
18798 assert!(!active_item.is_singleton(cx));
18799 })
18800 .unwrap();
18801
18802 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18803 editor.change_selections(
18804 SelectionEffects::scroll(Autoscroll::Next),
18805 window,
18806 cx,
18807 |s| s.select_ranges(Some(70..70)),
18808 );
18809 editor.open_excerpts(&OpenExcerpts, window, cx);
18810 });
18811 cx.executor().run_until_parked();
18812 workspace
18813 .update(cx, |workspace, window, cx| {
18814 let active_item = workspace
18815 .active_item(cx)
18816 .expect("should have an active item after navigating into the 3rd buffer");
18817 let third_item_id = active_item.item_id();
18818 assert_ne!(
18819 third_item_id, multibuffer_item_id,
18820 "Should navigate into the 3rd buffer and activate it"
18821 );
18822 assert_ne!(third_item_id, first_item_id);
18823 assert_ne!(third_item_id, second_item_id);
18824 assert!(
18825 active_item.is_singleton(cx),
18826 "New active item should be a singleton buffer"
18827 );
18828 assert_eq!(
18829 active_item
18830 .act_as::<Editor>(cx)
18831 .expect("should have navigated into an editor")
18832 .read(cx)
18833 .text(cx),
18834 sample_text_3
18835 );
18836
18837 workspace
18838 .go_back(workspace.active_pane().downgrade(), window, cx)
18839 .detach_and_log_err(cx);
18840 })
18841 .unwrap();
18842 cx.executor().run_until_parked();
18843 workspace
18844 .update(cx, |workspace, _, cx| {
18845 let active_item = workspace
18846 .active_item(cx)
18847 .expect("should have an active item after navigating back from the 3rd buffer");
18848 assert_eq!(
18849 active_item.item_id(),
18850 multibuffer_item_id,
18851 "Should navigate back from the 3rd buffer to the multi buffer"
18852 );
18853 assert!(!active_item.is_singleton(cx));
18854 })
18855 .unwrap();
18856}
18857
18858#[gpui::test]
18859async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18860 init_test(cx, |_| {});
18861
18862 let mut cx = EditorTestContext::new(cx).await;
18863
18864 let diff_base = r#"
18865 use some::mod;
18866
18867 const A: u32 = 42;
18868
18869 fn main() {
18870 println!("hello");
18871
18872 println!("world");
18873 }
18874 "#
18875 .unindent();
18876
18877 cx.set_state(
18878 &r#"
18879 use some::modified;
18880
18881 ˇ
18882 fn main() {
18883 println!("hello there");
18884
18885 println!("around the");
18886 println!("world");
18887 }
18888 "#
18889 .unindent(),
18890 );
18891
18892 cx.set_head_text(&diff_base);
18893 executor.run_until_parked();
18894
18895 cx.update_editor(|editor, window, cx| {
18896 editor.go_to_next_hunk(&GoToHunk, window, cx);
18897 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18898 });
18899 executor.run_until_parked();
18900 cx.assert_state_with_diff(
18901 r#"
18902 use some::modified;
18903
18904
18905 fn main() {
18906 - println!("hello");
18907 + ˇ println!("hello there");
18908
18909 println!("around the");
18910 println!("world");
18911 }
18912 "#
18913 .unindent(),
18914 );
18915
18916 cx.update_editor(|editor, window, cx| {
18917 for _ in 0..2 {
18918 editor.go_to_next_hunk(&GoToHunk, window, cx);
18919 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18920 }
18921 });
18922 executor.run_until_parked();
18923 cx.assert_state_with_diff(
18924 r#"
18925 - use some::mod;
18926 + ˇuse some::modified;
18927
18928
18929 fn main() {
18930 - println!("hello");
18931 + println!("hello there");
18932
18933 + println!("around the");
18934 println!("world");
18935 }
18936 "#
18937 .unindent(),
18938 );
18939
18940 cx.update_editor(|editor, window, cx| {
18941 editor.go_to_next_hunk(&GoToHunk, window, cx);
18942 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18943 });
18944 executor.run_until_parked();
18945 cx.assert_state_with_diff(
18946 r#"
18947 - use some::mod;
18948 + use some::modified;
18949
18950 - const A: u32 = 42;
18951 ˇ
18952 fn main() {
18953 - println!("hello");
18954 + println!("hello there");
18955
18956 + println!("around the");
18957 println!("world");
18958 }
18959 "#
18960 .unindent(),
18961 );
18962
18963 cx.update_editor(|editor, window, cx| {
18964 editor.cancel(&Cancel, window, cx);
18965 });
18966
18967 cx.assert_state_with_diff(
18968 r#"
18969 use some::modified;
18970
18971 ˇ
18972 fn main() {
18973 println!("hello there");
18974
18975 println!("around the");
18976 println!("world");
18977 }
18978 "#
18979 .unindent(),
18980 );
18981}
18982
18983#[gpui::test]
18984async fn test_diff_base_change_with_expanded_diff_hunks(
18985 executor: BackgroundExecutor,
18986 cx: &mut TestAppContext,
18987) {
18988 init_test(cx, |_| {});
18989
18990 let mut cx = EditorTestContext::new(cx).await;
18991
18992 let diff_base = r#"
18993 use some::mod1;
18994 use some::mod2;
18995
18996 const A: u32 = 42;
18997 const B: u32 = 42;
18998 const C: u32 = 42;
18999
19000 fn main() {
19001 println!("hello");
19002
19003 println!("world");
19004 }
19005 "#
19006 .unindent();
19007
19008 cx.set_state(
19009 &r#"
19010 use some::mod2;
19011
19012 const A: u32 = 42;
19013 const C: u32 = 42;
19014
19015 fn main(ˇ) {
19016 //println!("hello");
19017
19018 println!("world");
19019 //
19020 //
19021 }
19022 "#
19023 .unindent(),
19024 );
19025
19026 cx.set_head_text(&diff_base);
19027 executor.run_until_parked();
19028
19029 cx.update_editor(|editor, window, cx| {
19030 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19031 });
19032 executor.run_until_parked();
19033 cx.assert_state_with_diff(
19034 r#"
19035 - use some::mod1;
19036 use some::mod2;
19037
19038 const A: u32 = 42;
19039 - const B: u32 = 42;
19040 const C: u32 = 42;
19041
19042 fn main(ˇ) {
19043 - println!("hello");
19044 + //println!("hello");
19045
19046 println!("world");
19047 + //
19048 + //
19049 }
19050 "#
19051 .unindent(),
19052 );
19053
19054 cx.set_head_text("new diff base!");
19055 executor.run_until_parked();
19056 cx.assert_state_with_diff(
19057 r#"
19058 - new diff base!
19059 + use some::mod2;
19060 +
19061 + const A: u32 = 42;
19062 + const C: u32 = 42;
19063 +
19064 + fn main(ˇ) {
19065 + //println!("hello");
19066 +
19067 + println!("world");
19068 + //
19069 + //
19070 + }
19071 "#
19072 .unindent(),
19073 );
19074}
19075
19076#[gpui::test]
19077async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19078 init_test(cx, |_| {});
19079
19080 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19081 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19082 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19083 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19084 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19085 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19086
19087 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19088 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19089 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19090
19091 let multi_buffer = cx.new(|cx| {
19092 let mut multibuffer = MultiBuffer::new(ReadWrite);
19093 multibuffer.push_excerpts(
19094 buffer_1.clone(),
19095 [
19096 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19097 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19098 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19099 ],
19100 cx,
19101 );
19102 multibuffer.push_excerpts(
19103 buffer_2.clone(),
19104 [
19105 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19106 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19107 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19108 ],
19109 cx,
19110 );
19111 multibuffer.push_excerpts(
19112 buffer_3.clone(),
19113 [
19114 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19115 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19116 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19117 ],
19118 cx,
19119 );
19120 multibuffer
19121 });
19122
19123 let editor =
19124 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19125 editor
19126 .update(cx, |editor, _window, cx| {
19127 for (buffer, diff_base) in [
19128 (buffer_1.clone(), file_1_old),
19129 (buffer_2.clone(), file_2_old),
19130 (buffer_3.clone(), file_3_old),
19131 ] {
19132 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19133 editor
19134 .buffer
19135 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19136 }
19137 })
19138 .unwrap();
19139
19140 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19141 cx.run_until_parked();
19142
19143 cx.assert_editor_state(
19144 &"
19145 ˇaaa
19146 ccc
19147 ddd
19148
19149 ggg
19150 hhh
19151
19152
19153 lll
19154 mmm
19155 NNN
19156
19157 qqq
19158 rrr
19159
19160 uuu
19161 111
19162 222
19163 333
19164
19165 666
19166 777
19167
19168 000
19169 !!!"
19170 .unindent(),
19171 );
19172
19173 cx.update_editor(|editor, window, cx| {
19174 editor.select_all(&SelectAll, window, cx);
19175 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19176 });
19177 cx.executor().run_until_parked();
19178
19179 cx.assert_state_with_diff(
19180 "
19181 «aaa
19182 - bbb
19183 ccc
19184 ddd
19185
19186 ggg
19187 hhh
19188
19189
19190 lll
19191 mmm
19192 - nnn
19193 + NNN
19194
19195 qqq
19196 rrr
19197
19198 uuu
19199 111
19200 222
19201 333
19202
19203 + 666
19204 777
19205
19206 000
19207 !!!ˇ»"
19208 .unindent(),
19209 );
19210}
19211
19212#[gpui::test]
19213async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19214 init_test(cx, |_| {});
19215
19216 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19217 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19218
19219 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19220 let multi_buffer = cx.new(|cx| {
19221 let mut multibuffer = MultiBuffer::new(ReadWrite);
19222 multibuffer.push_excerpts(
19223 buffer.clone(),
19224 [
19225 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19226 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19227 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19228 ],
19229 cx,
19230 );
19231 multibuffer
19232 });
19233
19234 let editor =
19235 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19236 editor
19237 .update(cx, |editor, _window, cx| {
19238 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19239 editor
19240 .buffer
19241 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19242 })
19243 .unwrap();
19244
19245 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19246 cx.run_until_parked();
19247
19248 cx.update_editor(|editor, window, cx| {
19249 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19250 });
19251 cx.executor().run_until_parked();
19252
19253 // When the start of a hunk coincides with the start of its excerpt,
19254 // the hunk is expanded. When the start of a hunk is earlier than
19255 // the start of its excerpt, the hunk is not expanded.
19256 cx.assert_state_with_diff(
19257 "
19258 ˇaaa
19259 - bbb
19260 + BBB
19261
19262 - ddd
19263 - eee
19264 + DDD
19265 + EEE
19266 fff
19267
19268 iii
19269 "
19270 .unindent(),
19271 );
19272}
19273
19274#[gpui::test]
19275async fn test_edits_around_expanded_insertion_hunks(
19276 executor: BackgroundExecutor,
19277 cx: &mut TestAppContext,
19278) {
19279 init_test(cx, |_| {});
19280
19281 let mut cx = EditorTestContext::new(cx).await;
19282
19283 let diff_base = r#"
19284 use some::mod1;
19285 use some::mod2;
19286
19287 const A: u32 = 42;
19288
19289 fn main() {
19290 println!("hello");
19291
19292 println!("world");
19293 }
19294 "#
19295 .unindent();
19296 executor.run_until_parked();
19297 cx.set_state(
19298 &r#"
19299 use some::mod1;
19300 use some::mod2;
19301
19302 const A: u32 = 42;
19303 const B: u32 = 42;
19304 const C: u32 = 42;
19305 ˇ
19306
19307 fn main() {
19308 println!("hello");
19309
19310 println!("world");
19311 }
19312 "#
19313 .unindent(),
19314 );
19315
19316 cx.set_head_text(&diff_base);
19317 executor.run_until_parked();
19318
19319 cx.update_editor(|editor, window, cx| {
19320 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19321 });
19322 executor.run_until_parked();
19323
19324 cx.assert_state_with_diff(
19325 r#"
19326 use some::mod1;
19327 use some::mod2;
19328
19329 const A: u32 = 42;
19330 + const B: u32 = 42;
19331 + const C: u32 = 42;
19332 + ˇ
19333
19334 fn main() {
19335 println!("hello");
19336
19337 println!("world");
19338 }
19339 "#
19340 .unindent(),
19341 );
19342
19343 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19344 executor.run_until_parked();
19345
19346 cx.assert_state_with_diff(
19347 r#"
19348 use some::mod1;
19349 use some::mod2;
19350
19351 const A: u32 = 42;
19352 + const B: u32 = 42;
19353 + const C: u32 = 42;
19354 + const D: u32 = 42;
19355 + ˇ
19356
19357 fn main() {
19358 println!("hello");
19359
19360 println!("world");
19361 }
19362 "#
19363 .unindent(),
19364 );
19365
19366 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19367 executor.run_until_parked();
19368
19369 cx.assert_state_with_diff(
19370 r#"
19371 use some::mod1;
19372 use some::mod2;
19373
19374 const A: u32 = 42;
19375 + const B: u32 = 42;
19376 + const C: u32 = 42;
19377 + const D: u32 = 42;
19378 + const E: u32 = 42;
19379 + ˇ
19380
19381 fn main() {
19382 println!("hello");
19383
19384 println!("world");
19385 }
19386 "#
19387 .unindent(),
19388 );
19389
19390 cx.update_editor(|editor, window, cx| {
19391 editor.delete_line(&DeleteLine, window, cx);
19392 });
19393 executor.run_until_parked();
19394
19395 cx.assert_state_with_diff(
19396 r#"
19397 use some::mod1;
19398 use some::mod2;
19399
19400 const A: u32 = 42;
19401 + const B: u32 = 42;
19402 + const C: u32 = 42;
19403 + const D: u32 = 42;
19404 + const E: u32 = 42;
19405 ˇ
19406 fn main() {
19407 println!("hello");
19408
19409 println!("world");
19410 }
19411 "#
19412 .unindent(),
19413 );
19414
19415 cx.update_editor(|editor, window, cx| {
19416 editor.move_up(&MoveUp, window, cx);
19417 editor.delete_line(&DeleteLine, window, cx);
19418 editor.move_up(&MoveUp, window, cx);
19419 editor.delete_line(&DeleteLine, window, cx);
19420 editor.move_up(&MoveUp, window, cx);
19421 editor.delete_line(&DeleteLine, window, cx);
19422 });
19423 executor.run_until_parked();
19424 cx.assert_state_with_diff(
19425 r#"
19426 use some::mod1;
19427 use some::mod2;
19428
19429 const A: u32 = 42;
19430 + const B: u32 = 42;
19431 ˇ
19432 fn main() {
19433 println!("hello");
19434
19435 println!("world");
19436 }
19437 "#
19438 .unindent(),
19439 );
19440
19441 cx.update_editor(|editor, window, cx| {
19442 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19443 editor.delete_line(&DeleteLine, window, cx);
19444 });
19445 executor.run_until_parked();
19446 cx.assert_state_with_diff(
19447 r#"
19448 ˇ
19449 fn main() {
19450 println!("hello");
19451
19452 println!("world");
19453 }
19454 "#
19455 .unindent(),
19456 );
19457}
19458
19459#[gpui::test]
19460async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19461 init_test(cx, |_| {});
19462
19463 let mut cx = EditorTestContext::new(cx).await;
19464 cx.set_head_text(indoc! { "
19465 one
19466 two
19467 three
19468 four
19469 five
19470 "
19471 });
19472 cx.set_state(indoc! { "
19473 one
19474 ˇthree
19475 five
19476 "});
19477 cx.run_until_parked();
19478 cx.update_editor(|editor, window, cx| {
19479 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19480 });
19481 cx.assert_state_with_diff(
19482 indoc! { "
19483 one
19484 - two
19485 ˇthree
19486 - four
19487 five
19488 "}
19489 .to_string(),
19490 );
19491 cx.update_editor(|editor, window, cx| {
19492 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19493 });
19494
19495 cx.assert_state_with_diff(
19496 indoc! { "
19497 one
19498 ˇthree
19499 five
19500 "}
19501 .to_string(),
19502 );
19503
19504 cx.set_state(indoc! { "
19505 one
19506 ˇTWO
19507 three
19508 four
19509 five
19510 "});
19511 cx.run_until_parked();
19512 cx.update_editor(|editor, window, cx| {
19513 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19514 });
19515
19516 cx.assert_state_with_diff(
19517 indoc! { "
19518 one
19519 - two
19520 + ˇTWO
19521 three
19522 four
19523 five
19524 "}
19525 .to_string(),
19526 );
19527 cx.update_editor(|editor, window, cx| {
19528 editor.move_up(&Default::default(), window, cx);
19529 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19530 });
19531 cx.assert_state_with_diff(
19532 indoc! { "
19533 one
19534 ˇTWO
19535 three
19536 four
19537 five
19538 "}
19539 .to_string(),
19540 );
19541}
19542
19543#[gpui::test]
19544async fn test_edits_around_expanded_deletion_hunks(
19545 executor: BackgroundExecutor,
19546 cx: &mut TestAppContext,
19547) {
19548 init_test(cx, |_| {});
19549
19550 let mut cx = EditorTestContext::new(cx).await;
19551
19552 let diff_base = r#"
19553 use some::mod1;
19554 use some::mod2;
19555
19556 const A: u32 = 42;
19557 const B: u32 = 42;
19558 const C: u32 = 42;
19559
19560
19561 fn main() {
19562 println!("hello");
19563
19564 println!("world");
19565 }
19566 "#
19567 .unindent();
19568 executor.run_until_parked();
19569 cx.set_state(
19570 &r#"
19571 use some::mod1;
19572 use some::mod2;
19573
19574 ˇconst B: u32 = 42;
19575 const C: u32 = 42;
19576
19577
19578 fn main() {
19579 println!("hello");
19580
19581 println!("world");
19582 }
19583 "#
19584 .unindent(),
19585 );
19586
19587 cx.set_head_text(&diff_base);
19588 executor.run_until_parked();
19589
19590 cx.update_editor(|editor, window, cx| {
19591 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19592 });
19593 executor.run_until_parked();
19594
19595 cx.assert_state_with_diff(
19596 r#"
19597 use some::mod1;
19598 use some::mod2;
19599
19600 - const A: u32 = 42;
19601 ˇconst B: u32 = 42;
19602 const C: u32 = 42;
19603
19604
19605 fn main() {
19606 println!("hello");
19607
19608 println!("world");
19609 }
19610 "#
19611 .unindent(),
19612 );
19613
19614 cx.update_editor(|editor, window, cx| {
19615 editor.delete_line(&DeleteLine, window, cx);
19616 });
19617 executor.run_until_parked();
19618 cx.assert_state_with_diff(
19619 r#"
19620 use some::mod1;
19621 use some::mod2;
19622
19623 - const A: u32 = 42;
19624 - const B: u32 = 42;
19625 ˇconst C: u32 = 42;
19626
19627
19628 fn main() {
19629 println!("hello");
19630
19631 println!("world");
19632 }
19633 "#
19634 .unindent(),
19635 );
19636
19637 cx.update_editor(|editor, window, cx| {
19638 editor.delete_line(&DeleteLine, window, cx);
19639 });
19640 executor.run_until_parked();
19641 cx.assert_state_with_diff(
19642 r#"
19643 use some::mod1;
19644 use some::mod2;
19645
19646 - const A: u32 = 42;
19647 - const B: u32 = 42;
19648 - const C: u32 = 42;
19649 ˇ
19650
19651 fn main() {
19652 println!("hello");
19653
19654 println!("world");
19655 }
19656 "#
19657 .unindent(),
19658 );
19659
19660 cx.update_editor(|editor, window, cx| {
19661 editor.handle_input("replacement", window, cx);
19662 });
19663 executor.run_until_parked();
19664 cx.assert_state_with_diff(
19665 r#"
19666 use some::mod1;
19667 use some::mod2;
19668
19669 - const A: u32 = 42;
19670 - const B: u32 = 42;
19671 - const C: u32 = 42;
19672 -
19673 + replacementˇ
19674
19675 fn main() {
19676 println!("hello");
19677
19678 println!("world");
19679 }
19680 "#
19681 .unindent(),
19682 );
19683}
19684
19685#[gpui::test]
19686async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19687 init_test(cx, |_| {});
19688
19689 let mut cx = EditorTestContext::new(cx).await;
19690
19691 let base_text = r#"
19692 one
19693 two
19694 three
19695 four
19696 five
19697 "#
19698 .unindent();
19699 executor.run_until_parked();
19700 cx.set_state(
19701 &r#"
19702 one
19703 two
19704 fˇour
19705 five
19706 "#
19707 .unindent(),
19708 );
19709
19710 cx.set_head_text(&base_text);
19711 executor.run_until_parked();
19712
19713 cx.update_editor(|editor, window, cx| {
19714 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19715 });
19716 executor.run_until_parked();
19717
19718 cx.assert_state_with_diff(
19719 r#"
19720 one
19721 two
19722 - three
19723 fˇour
19724 five
19725 "#
19726 .unindent(),
19727 );
19728
19729 cx.update_editor(|editor, window, cx| {
19730 editor.backspace(&Backspace, window, cx);
19731 editor.backspace(&Backspace, window, cx);
19732 });
19733 executor.run_until_parked();
19734 cx.assert_state_with_diff(
19735 r#"
19736 one
19737 two
19738 - threeˇ
19739 - four
19740 + our
19741 five
19742 "#
19743 .unindent(),
19744 );
19745}
19746
19747#[gpui::test]
19748async fn test_edit_after_expanded_modification_hunk(
19749 executor: BackgroundExecutor,
19750 cx: &mut TestAppContext,
19751) {
19752 init_test(cx, |_| {});
19753
19754 let mut cx = EditorTestContext::new(cx).await;
19755
19756 let diff_base = r#"
19757 use some::mod1;
19758 use some::mod2;
19759
19760 const A: u32 = 42;
19761 const B: u32 = 42;
19762 const C: u32 = 42;
19763 const D: u32 = 42;
19764
19765
19766 fn main() {
19767 println!("hello");
19768
19769 println!("world");
19770 }"#
19771 .unindent();
19772
19773 cx.set_state(
19774 &r#"
19775 use some::mod1;
19776 use some::mod2;
19777
19778 const A: u32 = 42;
19779 const B: u32 = 42;
19780 const C: u32 = 43ˇ
19781 const D: u32 = 42;
19782
19783
19784 fn main() {
19785 println!("hello");
19786
19787 println!("world");
19788 }"#
19789 .unindent(),
19790 );
19791
19792 cx.set_head_text(&diff_base);
19793 executor.run_until_parked();
19794 cx.update_editor(|editor, window, cx| {
19795 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19796 });
19797 executor.run_until_parked();
19798
19799 cx.assert_state_with_diff(
19800 r#"
19801 use some::mod1;
19802 use some::mod2;
19803
19804 const A: u32 = 42;
19805 const B: u32 = 42;
19806 - const C: u32 = 42;
19807 + const C: u32 = 43ˇ
19808 const D: u32 = 42;
19809
19810
19811 fn main() {
19812 println!("hello");
19813
19814 println!("world");
19815 }"#
19816 .unindent(),
19817 );
19818
19819 cx.update_editor(|editor, window, cx| {
19820 editor.handle_input("\nnew_line\n", window, cx);
19821 });
19822 executor.run_until_parked();
19823
19824 cx.assert_state_with_diff(
19825 r#"
19826 use some::mod1;
19827 use some::mod2;
19828
19829 const A: u32 = 42;
19830 const B: u32 = 42;
19831 - const C: u32 = 42;
19832 + const C: u32 = 43
19833 + new_line
19834 + ˇ
19835 const D: u32 = 42;
19836
19837
19838 fn main() {
19839 println!("hello");
19840
19841 println!("world");
19842 }"#
19843 .unindent(),
19844 );
19845}
19846
19847#[gpui::test]
19848async fn test_stage_and_unstage_added_file_hunk(
19849 executor: BackgroundExecutor,
19850 cx: &mut TestAppContext,
19851) {
19852 init_test(cx, |_| {});
19853
19854 let mut cx = EditorTestContext::new(cx).await;
19855 cx.update_editor(|editor, _, cx| {
19856 editor.set_expand_all_diff_hunks(cx);
19857 });
19858
19859 let working_copy = r#"
19860 ˇfn main() {
19861 println!("hello, world!");
19862 }
19863 "#
19864 .unindent();
19865
19866 cx.set_state(&working_copy);
19867 executor.run_until_parked();
19868
19869 cx.assert_state_with_diff(
19870 r#"
19871 + ˇfn main() {
19872 + println!("hello, world!");
19873 + }
19874 "#
19875 .unindent(),
19876 );
19877 cx.assert_index_text(None);
19878
19879 cx.update_editor(|editor, window, cx| {
19880 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19881 });
19882 executor.run_until_parked();
19883 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19884 cx.assert_state_with_diff(
19885 r#"
19886 + ˇfn main() {
19887 + println!("hello, world!");
19888 + }
19889 "#
19890 .unindent(),
19891 );
19892
19893 cx.update_editor(|editor, window, cx| {
19894 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19895 });
19896 executor.run_until_parked();
19897 cx.assert_index_text(None);
19898}
19899
19900async fn setup_indent_guides_editor(
19901 text: &str,
19902 cx: &mut TestAppContext,
19903) -> (BufferId, EditorTestContext) {
19904 init_test(cx, |_| {});
19905
19906 let mut cx = EditorTestContext::new(cx).await;
19907
19908 let buffer_id = cx.update_editor(|editor, window, cx| {
19909 editor.set_text(text, window, cx);
19910 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19911
19912 buffer_ids[0]
19913 });
19914
19915 (buffer_id, cx)
19916}
19917
19918fn assert_indent_guides(
19919 range: Range<u32>,
19920 expected: Vec<IndentGuide>,
19921 active_indices: Option<Vec<usize>>,
19922 cx: &mut EditorTestContext,
19923) {
19924 let indent_guides = cx.update_editor(|editor, window, cx| {
19925 let snapshot = editor.snapshot(window, cx).display_snapshot;
19926 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19927 editor,
19928 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19929 true,
19930 &snapshot,
19931 cx,
19932 );
19933
19934 indent_guides.sort_by(|a, b| {
19935 a.depth.cmp(&b.depth).then(
19936 a.start_row
19937 .cmp(&b.start_row)
19938 .then(a.end_row.cmp(&b.end_row)),
19939 )
19940 });
19941 indent_guides
19942 });
19943
19944 if let Some(expected) = active_indices {
19945 let active_indices = cx.update_editor(|editor, window, cx| {
19946 let snapshot = editor.snapshot(window, cx).display_snapshot;
19947 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19948 });
19949
19950 assert_eq!(
19951 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19952 expected,
19953 "Active indent guide indices do not match"
19954 );
19955 }
19956
19957 assert_eq!(indent_guides, expected, "Indent guides do not match");
19958}
19959
19960fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19961 IndentGuide {
19962 buffer_id,
19963 start_row: MultiBufferRow(start_row),
19964 end_row: MultiBufferRow(end_row),
19965 depth,
19966 tab_size: 4,
19967 settings: IndentGuideSettings {
19968 enabled: true,
19969 line_width: 1,
19970 active_line_width: 1,
19971 coloring: IndentGuideColoring::default(),
19972 background_coloring: IndentGuideBackgroundColoring::default(),
19973 },
19974 }
19975}
19976
19977#[gpui::test]
19978async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19979 let (buffer_id, mut cx) = setup_indent_guides_editor(
19980 &"
19981 fn main() {
19982 let a = 1;
19983 }"
19984 .unindent(),
19985 cx,
19986 )
19987 .await;
19988
19989 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19990}
19991
19992#[gpui::test]
19993async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19994 let (buffer_id, mut cx) = setup_indent_guides_editor(
19995 &"
19996 fn main() {
19997 let a = 1;
19998 let b = 2;
19999 }"
20000 .unindent(),
20001 cx,
20002 )
20003 .await;
20004
20005 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20006}
20007
20008#[gpui::test]
20009async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20010 let (buffer_id, mut cx) = setup_indent_guides_editor(
20011 &"
20012 fn main() {
20013 let a = 1;
20014 if a == 3 {
20015 let b = 2;
20016 } else {
20017 let c = 3;
20018 }
20019 }"
20020 .unindent(),
20021 cx,
20022 )
20023 .await;
20024
20025 assert_indent_guides(
20026 0..8,
20027 vec![
20028 indent_guide(buffer_id, 1, 6, 0),
20029 indent_guide(buffer_id, 3, 3, 1),
20030 indent_guide(buffer_id, 5, 5, 1),
20031 ],
20032 None,
20033 &mut cx,
20034 );
20035}
20036
20037#[gpui::test]
20038async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20039 let (buffer_id, mut cx) = setup_indent_guides_editor(
20040 &"
20041 fn main() {
20042 let a = 1;
20043 let b = 2;
20044 let c = 3;
20045 }"
20046 .unindent(),
20047 cx,
20048 )
20049 .await;
20050
20051 assert_indent_guides(
20052 0..5,
20053 vec![
20054 indent_guide(buffer_id, 1, 3, 0),
20055 indent_guide(buffer_id, 2, 2, 1),
20056 ],
20057 None,
20058 &mut cx,
20059 );
20060}
20061
20062#[gpui::test]
20063async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20064 let (buffer_id, mut cx) = setup_indent_guides_editor(
20065 &"
20066 fn main() {
20067 let a = 1;
20068
20069 let c = 3;
20070 }"
20071 .unindent(),
20072 cx,
20073 )
20074 .await;
20075
20076 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20077}
20078
20079#[gpui::test]
20080async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20081 let (buffer_id, mut cx) = setup_indent_guides_editor(
20082 &"
20083 fn main() {
20084 let a = 1;
20085
20086 let c = 3;
20087
20088 if a == 3 {
20089 let b = 2;
20090 } else {
20091 let c = 3;
20092 }
20093 }"
20094 .unindent(),
20095 cx,
20096 )
20097 .await;
20098
20099 assert_indent_guides(
20100 0..11,
20101 vec![
20102 indent_guide(buffer_id, 1, 9, 0),
20103 indent_guide(buffer_id, 6, 6, 1),
20104 indent_guide(buffer_id, 8, 8, 1),
20105 ],
20106 None,
20107 &mut cx,
20108 );
20109}
20110
20111#[gpui::test]
20112async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20113 let (buffer_id, mut cx) = setup_indent_guides_editor(
20114 &"
20115 fn main() {
20116 let a = 1;
20117
20118 let c = 3;
20119
20120 if a == 3 {
20121 let b = 2;
20122 } else {
20123 let c = 3;
20124 }
20125 }"
20126 .unindent(),
20127 cx,
20128 )
20129 .await;
20130
20131 assert_indent_guides(
20132 1..11,
20133 vec![
20134 indent_guide(buffer_id, 1, 9, 0),
20135 indent_guide(buffer_id, 6, 6, 1),
20136 indent_guide(buffer_id, 8, 8, 1),
20137 ],
20138 None,
20139 &mut cx,
20140 );
20141}
20142
20143#[gpui::test]
20144async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20145 let (buffer_id, mut cx) = setup_indent_guides_editor(
20146 &"
20147 fn main() {
20148 let a = 1;
20149
20150 let c = 3;
20151
20152 if a == 3 {
20153 let b = 2;
20154 } else {
20155 let c = 3;
20156 }
20157 }"
20158 .unindent(),
20159 cx,
20160 )
20161 .await;
20162
20163 assert_indent_guides(
20164 1..10,
20165 vec![
20166 indent_guide(buffer_id, 1, 9, 0),
20167 indent_guide(buffer_id, 6, 6, 1),
20168 indent_guide(buffer_id, 8, 8, 1),
20169 ],
20170 None,
20171 &mut cx,
20172 );
20173}
20174
20175#[gpui::test]
20176async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20177 let (buffer_id, mut cx) = setup_indent_guides_editor(
20178 &"
20179 fn main() {
20180 if a {
20181 b(
20182 c,
20183 d,
20184 )
20185 } else {
20186 e(
20187 f
20188 )
20189 }
20190 }"
20191 .unindent(),
20192 cx,
20193 )
20194 .await;
20195
20196 assert_indent_guides(
20197 0..11,
20198 vec![
20199 indent_guide(buffer_id, 1, 10, 0),
20200 indent_guide(buffer_id, 2, 5, 1),
20201 indent_guide(buffer_id, 7, 9, 1),
20202 indent_guide(buffer_id, 3, 4, 2),
20203 indent_guide(buffer_id, 8, 8, 2),
20204 ],
20205 None,
20206 &mut cx,
20207 );
20208
20209 cx.update_editor(|editor, window, cx| {
20210 editor.fold_at(MultiBufferRow(2), window, cx);
20211 assert_eq!(
20212 editor.display_text(cx),
20213 "
20214 fn main() {
20215 if a {
20216 b(⋯
20217 )
20218 } else {
20219 e(
20220 f
20221 )
20222 }
20223 }"
20224 .unindent()
20225 );
20226 });
20227
20228 assert_indent_guides(
20229 0..11,
20230 vec![
20231 indent_guide(buffer_id, 1, 10, 0),
20232 indent_guide(buffer_id, 2, 5, 1),
20233 indent_guide(buffer_id, 7, 9, 1),
20234 indent_guide(buffer_id, 8, 8, 2),
20235 ],
20236 None,
20237 &mut cx,
20238 );
20239}
20240
20241#[gpui::test]
20242async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20243 let (buffer_id, mut cx) = setup_indent_guides_editor(
20244 &"
20245 block1
20246 block2
20247 block3
20248 block4
20249 block2
20250 block1
20251 block1"
20252 .unindent(),
20253 cx,
20254 )
20255 .await;
20256
20257 assert_indent_guides(
20258 1..10,
20259 vec![
20260 indent_guide(buffer_id, 1, 4, 0),
20261 indent_guide(buffer_id, 2, 3, 1),
20262 indent_guide(buffer_id, 3, 3, 2),
20263 ],
20264 None,
20265 &mut cx,
20266 );
20267}
20268
20269#[gpui::test]
20270async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20271 let (buffer_id, mut cx) = setup_indent_guides_editor(
20272 &"
20273 block1
20274 block2
20275 block3
20276
20277 block1
20278 block1"
20279 .unindent(),
20280 cx,
20281 )
20282 .await;
20283
20284 assert_indent_guides(
20285 0..6,
20286 vec![
20287 indent_guide(buffer_id, 1, 2, 0),
20288 indent_guide(buffer_id, 2, 2, 1),
20289 ],
20290 None,
20291 &mut cx,
20292 );
20293}
20294
20295#[gpui::test]
20296async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20297 let (buffer_id, mut cx) = setup_indent_guides_editor(
20298 &"
20299 function component() {
20300 \treturn (
20301 \t\t\t
20302 \t\t<div>
20303 \t\t\t<abc></abc>
20304 \t\t</div>
20305 \t)
20306 }"
20307 .unindent(),
20308 cx,
20309 )
20310 .await;
20311
20312 assert_indent_guides(
20313 0..8,
20314 vec![
20315 indent_guide(buffer_id, 1, 6, 0),
20316 indent_guide(buffer_id, 2, 5, 1),
20317 indent_guide(buffer_id, 4, 4, 2),
20318 ],
20319 None,
20320 &mut cx,
20321 );
20322}
20323
20324#[gpui::test]
20325async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20326 let (buffer_id, mut cx) = setup_indent_guides_editor(
20327 &"
20328 function component() {
20329 \treturn (
20330 \t
20331 \t\t<div>
20332 \t\t\t<abc></abc>
20333 \t\t</div>
20334 \t)
20335 }"
20336 .unindent(),
20337 cx,
20338 )
20339 .await;
20340
20341 assert_indent_guides(
20342 0..8,
20343 vec![
20344 indent_guide(buffer_id, 1, 6, 0),
20345 indent_guide(buffer_id, 2, 5, 1),
20346 indent_guide(buffer_id, 4, 4, 2),
20347 ],
20348 None,
20349 &mut cx,
20350 );
20351}
20352
20353#[gpui::test]
20354async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20355 let (buffer_id, mut cx) = setup_indent_guides_editor(
20356 &"
20357 block1
20358
20359
20360
20361 block2
20362 "
20363 .unindent(),
20364 cx,
20365 )
20366 .await;
20367
20368 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20369}
20370
20371#[gpui::test]
20372async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20373 let (buffer_id, mut cx) = setup_indent_guides_editor(
20374 &"
20375 def a:
20376 \tb = 3
20377 \tif True:
20378 \t\tc = 4
20379 \t\td = 5
20380 \tprint(b)
20381 "
20382 .unindent(),
20383 cx,
20384 )
20385 .await;
20386
20387 assert_indent_guides(
20388 0..6,
20389 vec![
20390 indent_guide(buffer_id, 1, 5, 0),
20391 indent_guide(buffer_id, 3, 4, 1),
20392 ],
20393 None,
20394 &mut cx,
20395 );
20396}
20397
20398#[gpui::test]
20399async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20400 let (buffer_id, mut cx) = setup_indent_guides_editor(
20401 &"
20402 fn main() {
20403 let a = 1;
20404 }"
20405 .unindent(),
20406 cx,
20407 )
20408 .await;
20409
20410 cx.update_editor(|editor, window, cx| {
20411 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20412 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20413 });
20414 });
20415
20416 assert_indent_guides(
20417 0..3,
20418 vec![indent_guide(buffer_id, 1, 1, 0)],
20419 Some(vec![0]),
20420 &mut cx,
20421 );
20422}
20423
20424#[gpui::test]
20425async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20426 let (buffer_id, mut cx) = setup_indent_guides_editor(
20427 &"
20428 fn main() {
20429 if 1 == 2 {
20430 let a = 1;
20431 }
20432 }"
20433 .unindent(),
20434 cx,
20435 )
20436 .await;
20437
20438 cx.update_editor(|editor, window, cx| {
20439 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20440 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20441 });
20442 });
20443
20444 assert_indent_guides(
20445 0..4,
20446 vec![
20447 indent_guide(buffer_id, 1, 3, 0),
20448 indent_guide(buffer_id, 2, 2, 1),
20449 ],
20450 Some(vec![1]),
20451 &mut cx,
20452 );
20453
20454 cx.update_editor(|editor, window, cx| {
20455 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20456 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20457 });
20458 });
20459
20460 assert_indent_guides(
20461 0..4,
20462 vec![
20463 indent_guide(buffer_id, 1, 3, 0),
20464 indent_guide(buffer_id, 2, 2, 1),
20465 ],
20466 Some(vec![1]),
20467 &mut cx,
20468 );
20469
20470 cx.update_editor(|editor, window, cx| {
20471 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20472 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20473 });
20474 });
20475
20476 assert_indent_guides(
20477 0..4,
20478 vec![
20479 indent_guide(buffer_id, 1, 3, 0),
20480 indent_guide(buffer_id, 2, 2, 1),
20481 ],
20482 Some(vec![0]),
20483 &mut cx,
20484 );
20485}
20486
20487#[gpui::test]
20488async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20489 let (buffer_id, mut cx) = setup_indent_guides_editor(
20490 &"
20491 fn main() {
20492 let a = 1;
20493
20494 let b = 2;
20495 }"
20496 .unindent(),
20497 cx,
20498 )
20499 .await;
20500
20501 cx.update_editor(|editor, window, cx| {
20502 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20503 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20504 });
20505 });
20506
20507 assert_indent_guides(
20508 0..5,
20509 vec![indent_guide(buffer_id, 1, 3, 0)],
20510 Some(vec![0]),
20511 &mut cx,
20512 );
20513}
20514
20515#[gpui::test]
20516async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20517 let (buffer_id, mut cx) = setup_indent_guides_editor(
20518 &"
20519 def m:
20520 a = 1
20521 pass"
20522 .unindent(),
20523 cx,
20524 )
20525 .await;
20526
20527 cx.update_editor(|editor, window, cx| {
20528 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20529 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20530 });
20531 });
20532
20533 assert_indent_guides(
20534 0..3,
20535 vec![indent_guide(buffer_id, 1, 2, 0)],
20536 Some(vec![0]),
20537 &mut cx,
20538 );
20539}
20540
20541#[gpui::test]
20542async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20543 init_test(cx, |_| {});
20544 let mut cx = EditorTestContext::new(cx).await;
20545 let text = indoc! {
20546 "
20547 impl A {
20548 fn b() {
20549 0;
20550 3;
20551 5;
20552 6;
20553 7;
20554 }
20555 }
20556 "
20557 };
20558 let base_text = indoc! {
20559 "
20560 impl A {
20561 fn b() {
20562 0;
20563 1;
20564 2;
20565 3;
20566 4;
20567 }
20568 fn c() {
20569 5;
20570 6;
20571 7;
20572 }
20573 }
20574 "
20575 };
20576
20577 cx.update_editor(|editor, window, cx| {
20578 editor.set_text(text, window, cx);
20579
20580 editor.buffer().update(cx, |multibuffer, cx| {
20581 let buffer = multibuffer.as_singleton().unwrap();
20582 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20583
20584 multibuffer.set_all_diff_hunks_expanded(cx);
20585 multibuffer.add_diff(diff, cx);
20586
20587 buffer.read(cx).remote_id()
20588 })
20589 });
20590 cx.run_until_parked();
20591
20592 cx.assert_state_with_diff(
20593 indoc! { "
20594 impl A {
20595 fn b() {
20596 0;
20597 - 1;
20598 - 2;
20599 3;
20600 - 4;
20601 - }
20602 - fn c() {
20603 5;
20604 6;
20605 7;
20606 }
20607 }
20608 ˇ"
20609 }
20610 .to_string(),
20611 );
20612
20613 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20614 editor
20615 .snapshot(window, cx)
20616 .buffer_snapshot
20617 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20618 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20619 .collect::<Vec<_>>()
20620 });
20621 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20622 assert_eq!(
20623 actual_guides,
20624 vec![
20625 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20626 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20627 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20628 ]
20629 );
20630}
20631
20632#[gpui::test]
20633async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20634 init_test(cx, |_| {});
20635 let mut cx = EditorTestContext::new(cx).await;
20636
20637 let diff_base = r#"
20638 a
20639 b
20640 c
20641 "#
20642 .unindent();
20643
20644 cx.set_state(
20645 &r#"
20646 ˇA
20647 b
20648 C
20649 "#
20650 .unindent(),
20651 );
20652 cx.set_head_text(&diff_base);
20653 cx.update_editor(|editor, window, cx| {
20654 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20655 });
20656 executor.run_until_parked();
20657
20658 let both_hunks_expanded = r#"
20659 - a
20660 + ˇA
20661 b
20662 - c
20663 + C
20664 "#
20665 .unindent();
20666
20667 cx.assert_state_with_diff(both_hunks_expanded.clone());
20668
20669 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20670 let snapshot = editor.snapshot(window, cx);
20671 let hunks = editor
20672 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20673 .collect::<Vec<_>>();
20674 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20675 let buffer_id = hunks[0].buffer_id;
20676 hunks
20677 .into_iter()
20678 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20679 .collect::<Vec<_>>()
20680 });
20681 assert_eq!(hunk_ranges.len(), 2);
20682
20683 cx.update_editor(|editor, _, cx| {
20684 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20685 });
20686 executor.run_until_parked();
20687
20688 let second_hunk_expanded = r#"
20689 ˇA
20690 b
20691 - c
20692 + C
20693 "#
20694 .unindent();
20695
20696 cx.assert_state_with_diff(second_hunk_expanded);
20697
20698 cx.update_editor(|editor, _, cx| {
20699 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20700 });
20701 executor.run_until_parked();
20702
20703 cx.assert_state_with_diff(both_hunks_expanded.clone());
20704
20705 cx.update_editor(|editor, _, cx| {
20706 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20707 });
20708 executor.run_until_parked();
20709
20710 let first_hunk_expanded = r#"
20711 - a
20712 + ˇA
20713 b
20714 C
20715 "#
20716 .unindent();
20717
20718 cx.assert_state_with_diff(first_hunk_expanded);
20719
20720 cx.update_editor(|editor, _, cx| {
20721 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20722 });
20723 executor.run_until_parked();
20724
20725 cx.assert_state_with_diff(both_hunks_expanded);
20726
20727 cx.set_state(
20728 &r#"
20729 ˇA
20730 b
20731 "#
20732 .unindent(),
20733 );
20734 cx.run_until_parked();
20735
20736 // TODO this cursor position seems bad
20737 cx.assert_state_with_diff(
20738 r#"
20739 - ˇa
20740 + A
20741 b
20742 "#
20743 .unindent(),
20744 );
20745
20746 cx.update_editor(|editor, window, cx| {
20747 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20748 });
20749
20750 cx.assert_state_with_diff(
20751 r#"
20752 - ˇa
20753 + A
20754 b
20755 - c
20756 "#
20757 .unindent(),
20758 );
20759
20760 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20761 let snapshot = editor.snapshot(window, cx);
20762 let hunks = editor
20763 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20764 .collect::<Vec<_>>();
20765 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20766 let buffer_id = hunks[0].buffer_id;
20767 hunks
20768 .into_iter()
20769 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20770 .collect::<Vec<_>>()
20771 });
20772 assert_eq!(hunk_ranges.len(), 2);
20773
20774 cx.update_editor(|editor, _, cx| {
20775 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20776 });
20777 executor.run_until_parked();
20778
20779 cx.assert_state_with_diff(
20780 r#"
20781 - ˇa
20782 + A
20783 b
20784 "#
20785 .unindent(),
20786 );
20787}
20788
20789#[gpui::test]
20790async fn test_toggle_deletion_hunk_at_start_of_file(
20791 executor: BackgroundExecutor,
20792 cx: &mut TestAppContext,
20793) {
20794 init_test(cx, |_| {});
20795 let mut cx = EditorTestContext::new(cx).await;
20796
20797 let diff_base = r#"
20798 a
20799 b
20800 c
20801 "#
20802 .unindent();
20803
20804 cx.set_state(
20805 &r#"
20806 ˇb
20807 c
20808 "#
20809 .unindent(),
20810 );
20811 cx.set_head_text(&diff_base);
20812 cx.update_editor(|editor, window, cx| {
20813 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20814 });
20815 executor.run_until_parked();
20816
20817 let hunk_expanded = r#"
20818 - a
20819 ˇb
20820 c
20821 "#
20822 .unindent();
20823
20824 cx.assert_state_with_diff(hunk_expanded.clone());
20825
20826 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20827 let snapshot = editor.snapshot(window, cx);
20828 let hunks = editor
20829 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20830 .collect::<Vec<_>>();
20831 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20832 let buffer_id = hunks[0].buffer_id;
20833 hunks
20834 .into_iter()
20835 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20836 .collect::<Vec<_>>()
20837 });
20838 assert_eq!(hunk_ranges.len(), 1);
20839
20840 cx.update_editor(|editor, _, cx| {
20841 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20842 });
20843 executor.run_until_parked();
20844
20845 let hunk_collapsed = r#"
20846 ˇb
20847 c
20848 "#
20849 .unindent();
20850
20851 cx.assert_state_with_diff(hunk_collapsed);
20852
20853 cx.update_editor(|editor, _, cx| {
20854 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20855 });
20856 executor.run_until_parked();
20857
20858 cx.assert_state_with_diff(hunk_expanded);
20859}
20860
20861#[gpui::test]
20862async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20863 init_test(cx, |_| {});
20864
20865 let fs = FakeFs::new(cx.executor());
20866 fs.insert_tree(
20867 path!("/test"),
20868 json!({
20869 ".git": {},
20870 "file-1": "ONE\n",
20871 "file-2": "TWO\n",
20872 "file-3": "THREE\n",
20873 }),
20874 )
20875 .await;
20876
20877 fs.set_head_for_repo(
20878 path!("/test/.git").as_ref(),
20879 &[
20880 ("file-1".into(), "one\n".into()),
20881 ("file-2".into(), "two\n".into()),
20882 ("file-3".into(), "three\n".into()),
20883 ],
20884 "deadbeef",
20885 );
20886
20887 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20888 let mut buffers = vec![];
20889 for i in 1..=3 {
20890 let buffer = project
20891 .update(cx, |project, cx| {
20892 let path = format!(path!("/test/file-{}"), i);
20893 project.open_local_buffer(path, cx)
20894 })
20895 .await
20896 .unwrap();
20897 buffers.push(buffer);
20898 }
20899
20900 let multibuffer = cx.new(|cx| {
20901 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20902 multibuffer.set_all_diff_hunks_expanded(cx);
20903 for buffer in &buffers {
20904 let snapshot = buffer.read(cx).snapshot();
20905 multibuffer.set_excerpts_for_path(
20906 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20907 buffer.clone(),
20908 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20909 2,
20910 cx,
20911 );
20912 }
20913 multibuffer
20914 });
20915
20916 let editor = cx.add_window(|window, cx| {
20917 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20918 });
20919 cx.run_until_parked();
20920
20921 let snapshot = editor
20922 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20923 .unwrap();
20924 let hunks = snapshot
20925 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20926 .map(|hunk| match hunk {
20927 DisplayDiffHunk::Unfolded {
20928 display_row_range, ..
20929 } => display_row_range,
20930 DisplayDiffHunk::Folded { .. } => unreachable!(),
20931 })
20932 .collect::<Vec<_>>();
20933 assert_eq!(
20934 hunks,
20935 [
20936 DisplayRow(2)..DisplayRow(4),
20937 DisplayRow(7)..DisplayRow(9),
20938 DisplayRow(12)..DisplayRow(14),
20939 ]
20940 );
20941}
20942
20943#[gpui::test]
20944async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20945 init_test(cx, |_| {});
20946
20947 let mut cx = EditorTestContext::new(cx).await;
20948 cx.set_head_text(indoc! { "
20949 one
20950 two
20951 three
20952 four
20953 five
20954 "
20955 });
20956 cx.set_index_text(indoc! { "
20957 one
20958 two
20959 three
20960 four
20961 five
20962 "
20963 });
20964 cx.set_state(indoc! {"
20965 one
20966 TWO
20967 ˇTHREE
20968 FOUR
20969 five
20970 "});
20971 cx.run_until_parked();
20972 cx.update_editor(|editor, window, cx| {
20973 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20974 });
20975 cx.run_until_parked();
20976 cx.assert_index_text(Some(indoc! {"
20977 one
20978 TWO
20979 THREE
20980 FOUR
20981 five
20982 "}));
20983 cx.set_state(indoc! { "
20984 one
20985 TWO
20986 ˇTHREE-HUNDRED
20987 FOUR
20988 five
20989 "});
20990 cx.run_until_parked();
20991 cx.update_editor(|editor, window, cx| {
20992 let snapshot = editor.snapshot(window, cx);
20993 let hunks = editor
20994 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20995 .collect::<Vec<_>>();
20996 assert_eq!(hunks.len(), 1);
20997 assert_eq!(
20998 hunks[0].status(),
20999 DiffHunkStatus {
21000 kind: DiffHunkStatusKind::Modified,
21001 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21002 }
21003 );
21004
21005 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21006 });
21007 cx.run_until_parked();
21008 cx.assert_index_text(Some(indoc! {"
21009 one
21010 TWO
21011 THREE-HUNDRED
21012 FOUR
21013 five
21014 "}));
21015}
21016
21017#[gpui::test]
21018fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21019 init_test(cx, |_| {});
21020
21021 let editor = cx.add_window(|window, cx| {
21022 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21023 build_editor(buffer, window, cx)
21024 });
21025
21026 let render_args = Arc::new(Mutex::new(None));
21027 let snapshot = editor
21028 .update(cx, |editor, window, cx| {
21029 let snapshot = editor.buffer().read(cx).snapshot(cx);
21030 let range =
21031 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21032
21033 struct RenderArgs {
21034 row: MultiBufferRow,
21035 folded: bool,
21036 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21037 }
21038
21039 let crease = Crease::inline(
21040 range,
21041 FoldPlaceholder::test(),
21042 {
21043 let toggle_callback = render_args.clone();
21044 move |row, folded, callback, _window, _cx| {
21045 *toggle_callback.lock() = Some(RenderArgs {
21046 row,
21047 folded,
21048 callback,
21049 });
21050 div()
21051 }
21052 },
21053 |_row, _folded, _window, _cx| div(),
21054 );
21055
21056 editor.insert_creases(Some(crease), cx);
21057 let snapshot = editor.snapshot(window, cx);
21058 let _div =
21059 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21060 snapshot
21061 })
21062 .unwrap();
21063
21064 let render_args = render_args.lock().take().unwrap();
21065 assert_eq!(render_args.row, MultiBufferRow(1));
21066 assert!(!render_args.folded);
21067 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21068
21069 cx.update_window(*editor, |_, window, cx| {
21070 (render_args.callback)(true, window, cx)
21071 })
21072 .unwrap();
21073 let snapshot = editor
21074 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21075 .unwrap();
21076 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21077
21078 cx.update_window(*editor, |_, window, cx| {
21079 (render_args.callback)(false, window, cx)
21080 })
21081 .unwrap();
21082 let snapshot = editor
21083 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21084 .unwrap();
21085 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21086}
21087
21088#[gpui::test]
21089async fn test_input_text(cx: &mut TestAppContext) {
21090 init_test(cx, |_| {});
21091 let mut cx = EditorTestContext::new(cx).await;
21092
21093 cx.set_state(
21094 &r#"ˇone
21095 two
21096
21097 three
21098 fourˇ
21099 five
21100
21101 siˇx"#
21102 .unindent(),
21103 );
21104
21105 cx.dispatch_action(HandleInput(String::new()));
21106 cx.assert_editor_state(
21107 &r#"ˇone
21108 two
21109
21110 three
21111 fourˇ
21112 five
21113
21114 siˇx"#
21115 .unindent(),
21116 );
21117
21118 cx.dispatch_action(HandleInput("AAAA".to_string()));
21119 cx.assert_editor_state(
21120 &r#"AAAAˇone
21121 two
21122
21123 three
21124 fourAAAAˇ
21125 five
21126
21127 siAAAAˇx"#
21128 .unindent(),
21129 );
21130}
21131
21132#[gpui::test]
21133async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21134 init_test(cx, |_| {});
21135
21136 let mut cx = EditorTestContext::new(cx).await;
21137 cx.set_state(
21138 r#"let foo = 1;
21139let foo = 2;
21140let foo = 3;
21141let fooˇ = 4;
21142let foo = 5;
21143let foo = 6;
21144let foo = 7;
21145let foo = 8;
21146let foo = 9;
21147let foo = 10;
21148let foo = 11;
21149let foo = 12;
21150let foo = 13;
21151let foo = 14;
21152let foo = 15;"#,
21153 );
21154
21155 cx.update_editor(|e, window, cx| {
21156 assert_eq!(
21157 e.next_scroll_position,
21158 NextScrollCursorCenterTopBottom::Center,
21159 "Default next scroll direction is center",
21160 );
21161
21162 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21163 assert_eq!(
21164 e.next_scroll_position,
21165 NextScrollCursorCenterTopBottom::Top,
21166 "After center, next scroll direction should be top",
21167 );
21168
21169 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21170 assert_eq!(
21171 e.next_scroll_position,
21172 NextScrollCursorCenterTopBottom::Bottom,
21173 "After top, next scroll direction should be bottom",
21174 );
21175
21176 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21177 assert_eq!(
21178 e.next_scroll_position,
21179 NextScrollCursorCenterTopBottom::Center,
21180 "After bottom, scrolling should start over",
21181 );
21182
21183 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21184 assert_eq!(
21185 e.next_scroll_position,
21186 NextScrollCursorCenterTopBottom::Top,
21187 "Scrolling continues if retriggered fast enough"
21188 );
21189 });
21190
21191 cx.executor()
21192 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21193 cx.executor().run_until_parked();
21194 cx.update_editor(|e, _, _| {
21195 assert_eq!(
21196 e.next_scroll_position,
21197 NextScrollCursorCenterTopBottom::Center,
21198 "If scrolling is not triggered fast enough, it should reset"
21199 );
21200 });
21201}
21202
21203#[gpui::test]
21204async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21205 init_test(cx, |_| {});
21206 let mut cx = EditorLspTestContext::new_rust(
21207 lsp::ServerCapabilities {
21208 definition_provider: Some(lsp::OneOf::Left(true)),
21209 references_provider: Some(lsp::OneOf::Left(true)),
21210 ..lsp::ServerCapabilities::default()
21211 },
21212 cx,
21213 )
21214 .await;
21215
21216 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21217 let go_to_definition = cx
21218 .lsp
21219 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21220 move |params, _| async move {
21221 if empty_go_to_definition {
21222 Ok(None)
21223 } else {
21224 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21225 uri: params.text_document_position_params.text_document.uri,
21226 range: lsp::Range::new(
21227 lsp::Position::new(4, 3),
21228 lsp::Position::new(4, 6),
21229 ),
21230 })))
21231 }
21232 },
21233 );
21234 let references = cx
21235 .lsp
21236 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21237 Ok(Some(vec![lsp::Location {
21238 uri: params.text_document_position.text_document.uri,
21239 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21240 }]))
21241 });
21242 (go_to_definition, references)
21243 };
21244
21245 cx.set_state(
21246 &r#"fn one() {
21247 let mut a = ˇtwo();
21248 }
21249
21250 fn two() {}"#
21251 .unindent(),
21252 );
21253 set_up_lsp_handlers(false, &mut cx);
21254 let navigated = cx
21255 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21256 .await
21257 .expect("Failed to navigate to definition");
21258 assert_eq!(
21259 navigated,
21260 Navigated::Yes,
21261 "Should have navigated to definition from the GetDefinition response"
21262 );
21263 cx.assert_editor_state(
21264 &r#"fn one() {
21265 let mut a = two();
21266 }
21267
21268 fn «twoˇ»() {}"#
21269 .unindent(),
21270 );
21271
21272 let editors = cx.update_workspace(|workspace, _, cx| {
21273 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21274 });
21275 cx.update_editor(|_, _, test_editor_cx| {
21276 assert_eq!(
21277 editors.len(),
21278 1,
21279 "Initially, only one, test, editor should be open in the workspace"
21280 );
21281 assert_eq!(
21282 test_editor_cx.entity(),
21283 editors.last().expect("Asserted len is 1").clone()
21284 );
21285 });
21286
21287 set_up_lsp_handlers(true, &mut cx);
21288 let navigated = cx
21289 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21290 .await
21291 .expect("Failed to navigate to lookup references");
21292 assert_eq!(
21293 navigated,
21294 Navigated::Yes,
21295 "Should have navigated to references as a fallback after empty GoToDefinition response"
21296 );
21297 // We should not change the selections in the existing file,
21298 // if opening another milti buffer with the references
21299 cx.assert_editor_state(
21300 &r#"fn one() {
21301 let mut a = two();
21302 }
21303
21304 fn «twoˇ»() {}"#
21305 .unindent(),
21306 );
21307 let editors = cx.update_workspace(|workspace, _, cx| {
21308 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21309 });
21310 cx.update_editor(|_, _, test_editor_cx| {
21311 assert_eq!(
21312 editors.len(),
21313 2,
21314 "After falling back to references search, we open a new editor with the results"
21315 );
21316 let references_fallback_text = editors
21317 .into_iter()
21318 .find(|new_editor| *new_editor != test_editor_cx.entity())
21319 .expect("Should have one non-test editor now")
21320 .read(test_editor_cx)
21321 .text(test_editor_cx);
21322 assert_eq!(
21323 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21324 "Should use the range from the references response and not the GoToDefinition one"
21325 );
21326 });
21327}
21328
21329#[gpui::test]
21330async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21331 init_test(cx, |_| {});
21332 cx.update(|cx| {
21333 let mut editor_settings = EditorSettings::get_global(cx).clone();
21334 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21335 EditorSettings::override_global(editor_settings, cx);
21336 });
21337 let mut cx = EditorLspTestContext::new_rust(
21338 lsp::ServerCapabilities {
21339 definition_provider: Some(lsp::OneOf::Left(true)),
21340 references_provider: Some(lsp::OneOf::Left(true)),
21341 ..lsp::ServerCapabilities::default()
21342 },
21343 cx,
21344 )
21345 .await;
21346 let original_state = r#"fn one() {
21347 let mut a = ˇtwo();
21348 }
21349
21350 fn two() {}"#
21351 .unindent();
21352 cx.set_state(&original_state);
21353
21354 let mut go_to_definition = cx
21355 .lsp
21356 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21357 move |_, _| async move { Ok(None) },
21358 );
21359 let _references = cx
21360 .lsp
21361 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21362 panic!("Should not call for references with no go to definition fallback")
21363 });
21364
21365 let navigated = cx
21366 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21367 .await
21368 .expect("Failed to navigate to lookup references");
21369 go_to_definition
21370 .next()
21371 .await
21372 .expect("Should have called the go_to_definition handler");
21373
21374 assert_eq!(
21375 navigated,
21376 Navigated::No,
21377 "Should have navigated to references as a fallback after empty GoToDefinition response"
21378 );
21379 cx.assert_editor_state(&original_state);
21380 let editors = cx.update_workspace(|workspace, _, cx| {
21381 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21382 });
21383 cx.update_editor(|_, _, _| {
21384 assert_eq!(
21385 editors.len(),
21386 1,
21387 "After unsuccessful fallback, no other editor should have been opened"
21388 );
21389 });
21390}
21391
21392#[gpui::test]
21393async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21394 init_test(cx, |_| {});
21395 let mut cx = EditorLspTestContext::new_rust(
21396 lsp::ServerCapabilities {
21397 references_provider: Some(lsp::OneOf::Left(true)),
21398 ..lsp::ServerCapabilities::default()
21399 },
21400 cx,
21401 )
21402 .await;
21403
21404 cx.set_state(
21405 &r#"
21406 fn one() {
21407 let mut a = two();
21408 }
21409
21410 fn ˇtwo() {}"#
21411 .unindent(),
21412 );
21413 cx.lsp
21414 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21415 Ok(Some(vec![
21416 lsp::Location {
21417 uri: params.text_document_position.text_document.uri.clone(),
21418 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21419 },
21420 lsp::Location {
21421 uri: params.text_document_position.text_document.uri,
21422 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21423 },
21424 ]))
21425 });
21426 let navigated = cx
21427 .update_editor(|editor, window, cx| {
21428 editor.find_all_references(&FindAllReferences, window, cx)
21429 })
21430 .unwrap()
21431 .await
21432 .expect("Failed to navigate to references");
21433 assert_eq!(
21434 navigated,
21435 Navigated::Yes,
21436 "Should have navigated to references from the FindAllReferences response"
21437 );
21438 cx.assert_editor_state(
21439 &r#"fn one() {
21440 let mut a = two();
21441 }
21442
21443 fn ˇtwo() {}"#
21444 .unindent(),
21445 );
21446
21447 let editors = cx.update_workspace(|workspace, _, cx| {
21448 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21449 });
21450 cx.update_editor(|_, _, _| {
21451 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21452 });
21453
21454 cx.set_state(
21455 &r#"fn one() {
21456 let mut a = ˇtwo();
21457 }
21458
21459 fn two() {}"#
21460 .unindent(),
21461 );
21462 let navigated = cx
21463 .update_editor(|editor, window, cx| {
21464 editor.find_all_references(&FindAllReferences, window, cx)
21465 })
21466 .unwrap()
21467 .await
21468 .expect("Failed to navigate to references");
21469 assert_eq!(
21470 navigated,
21471 Navigated::Yes,
21472 "Should have navigated to references from the FindAllReferences response"
21473 );
21474 cx.assert_editor_state(
21475 &r#"fn one() {
21476 let mut a = ˇtwo();
21477 }
21478
21479 fn two() {}"#
21480 .unindent(),
21481 );
21482 let editors = cx.update_workspace(|workspace, _, cx| {
21483 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21484 });
21485 cx.update_editor(|_, _, _| {
21486 assert_eq!(
21487 editors.len(),
21488 2,
21489 "should have re-used the previous multibuffer"
21490 );
21491 });
21492
21493 cx.set_state(
21494 &r#"fn one() {
21495 let mut a = ˇtwo();
21496 }
21497 fn three() {}
21498 fn two() {}"#
21499 .unindent(),
21500 );
21501 cx.lsp
21502 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21503 Ok(Some(vec![
21504 lsp::Location {
21505 uri: params.text_document_position.text_document.uri.clone(),
21506 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21507 },
21508 lsp::Location {
21509 uri: params.text_document_position.text_document.uri,
21510 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21511 },
21512 ]))
21513 });
21514 let navigated = cx
21515 .update_editor(|editor, window, cx| {
21516 editor.find_all_references(&FindAllReferences, window, cx)
21517 })
21518 .unwrap()
21519 .await
21520 .expect("Failed to navigate to references");
21521 assert_eq!(
21522 navigated,
21523 Navigated::Yes,
21524 "Should have navigated to references from the FindAllReferences response"
21525 );
21526 cx.assert_editor_state(
21527 &r#"fn one() {
21528 let mut a = ˇtwo();
21529 }
21530 fn three() {}
21531 fn two() {}"#
21532 .unindent(),
21533 );
21534 let editors = cx.update_workspace(|workspace, _, cx| {
21535 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21536 });
21537 cx.update_editor(|_, _, _| {
21538 assert_eq!(
21539 editors.len(),
21540 3,
21541 "should have used a new multibuffer as offsets changed"
21542 );
21543 });
21544}
21545#[gpui::test]
21546async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21547 init_test(cx, |_| {});
21548
21549 let language = Arc::new(Language::new(
21550 LanguageConfig::default(),
21551 Some(tree_sitter_rust::LANGUAGE.into()),
21552 ));
21553
21554 let text = r#"
21555 #[cfg(test)]
21556 mod tests() {
21557 #[test]
21558 fn runnable_1() {
21559 let a = 1;
21560 }
21561
21562 #[test]
21563 fn runnable_2() {
21564 let a = 1;
21565 let b = 2;
21566 }
21567 }
21568 "#
21569 .unindent();
21570
21571 let fs = FakeFs::new(cx.executor());
21572 fs.insert_file("/file.rs", Default::default()).await;
21573
21574 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21575 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21576 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21577 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21578 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21579
21580 let editor = cx.new_window_entity(|window, cx| {
21581 Editor::new(
21582 EditorMode::full(),
21583 multi_buffer,
21584 Some(project.clone()),
21585 window,
21586 cx,
21587 )
21588 });
21589
21590 editor.update_in(cx, |editor, window, cx| {
21591 let snapshot = editor.buffer().read(cx).snapshot(cx);
21592 editor.tasks.insert(
21593 (buffer.read(cx).remote_id(), 3),
21594 RunnableTasks {
21595 templates: vec![],
21596 offset: snapshot.anchor_before(43),
21597 column: 0,
21598 extra_variables: HashMap::default(),
21599 context_range: BufferOffset(43)..BufferOffset(85),
21600 },
21601 );
21602 editor.tasks.insert(
21603 (buffer.read(cx).remote_id(), 8),
21604 RunnableTasks {
21605 templates: vec![],
21606 offset: snapshot.anchor_before(86),
21607 column: 0,
21608 extra_variables: HashMap::default(),
21609 context_range: BufferOffset(86)..BufferOffset(191),
21610 },
21611 );
21612
21613 // Test finding task when cursor is inside function body
21614 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21615 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21616 });
21617 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21618 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21619
21620 // Test finding task when cursor is on function name
21621 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21622 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21623 });
21624 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21625 assert_eq!(row, 8, "Should find task when cursor is on function name");
21626 });
21627}
21628
21629#[gpui::test]
21630async fn test_folding_buffers(cx: &mut TestAppContext) {
21631 init_test(cx, |_| {});
21632
21633 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21634 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21635 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21636
21637 let fs = FakeFs::new(cx.executor());
21638 fs.insert_tree(
21639 path!("/a"),
21640 json!({
21641 "first.rs": sample_text_1,
21642 "second.rs": sample_text_2,
21643 "third.rs": sample_text_3,
21644 }),
21645 )
21646 .await;
21647 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21648 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21649 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21650 let worktree = project.update(cx, |project, cx| {
21651 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21652 assert_eq!(worktrees.len(), 1);
21653 worktrees.pop().unwrap()
21654 });
21655 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21656
21657 let buffer_1 = project
21658 .update(cx, |project, cx| {
21659 project.open_buffer((worktree_id, "first.rs"), cx)
21660 })
21661 .await
21662 .unwrap();
21663 let buffer_2 = project
21664 .update(cx, |project, cx| {
21665 project.open_buffer((worktree_id, "second.rs"), cx)
21666 })
21667 .await
21668 .unwrap();
21669 let buffer_3 = project
21670 .update(cx, |project, cx| {
21671 project.open_buffer((worktree_id, "third.rs"), cx)
21672 })
21673 .await
21674 .unwrap();
21675
21676 let multi_buffer = cx.new(|cx| {
21677 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21678 multi_buffer.push_excerpts(
21679 buffer_1.clone(),
21680 [
21681 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21682 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21683 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21684 ],
21685 cx,
21686 );
21687 multi_buffer.push_excerpts(
21688 buffer_2.clone(),
21689 [
21690 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21691 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21692 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21693 ],
21694 cx,
21695 );
21696 multi_buffer.push_excerpts(
21697 buffer_3.clone(),
21698 [
21699 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21700 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21701 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21702 ],
21703 cx,
21704 );
21705 multi_buffer
21706 });
21707 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21708 Editor::new(
21709 EditorMode::full(),
21710 multi_buffer.clone(),
21711 Some(project.clone()),
21712 window,
21713 cx,
21714 )
21715 });
21716
21717 assert_eq!(
21718 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21719 "\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",
21720 );
21721
21722 multi_buffer_editor.update(cx, |editor, cx| {
21723 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21724 });
21725 assert_eq!(
21726 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21727 "\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",
21728 "After folding the first buffer, its text should not be displayed"
21729 );
21730
21731 multi_buffer_editor.update(cx, |editor, cx| {
21732 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21733 });
21734 assert_eq!(
21735 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21736 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21737 "After folding the second buffer, its text should not be displayed"
21738 );
21739
21740 multi_buffer_editor.update(cx, |editor, cx| {
21741 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21742 });
21743 assert_eq!(
21744 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21745 "\n\n\n\n\n",
21746 "After folding the third buffer, its text should not be displayed"
21747 );
21748
21749 // Emulate selection inside the fold logic, that should work
21750 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21751 editor
21752 .snapshot(window, cx)
21753 .next_line_boundary(Point::new(0, 4));
21754 });
21755
21756 multi_buffer_editor.update(cx, |editor, cx| {
21757 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21758 });
21759 assert_eq!(
21760 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21761 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21762 "After unfolding the second buffer, its text should be displayed"
21763 );
21764
21765 // Typing inside of buffer 1 causes that buffer to be unfolded.
21766 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21767 assert_eq!(
21768 multi_buffer
21769 .read(cx)
21770 .snapshot(cx)
21771 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21772 .collect::<String>(),
21773 "bbbb"
21774 );
21775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21776 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21777 });
21778 editor.handle_input("B", window, cx);
21779 });
21780
21781 assert_eq!(
21782 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21783 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21784 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21785 );
21786
21787 multi_buffer_editor.update(cx, |editor, cx| {
21788 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21789 });
21790 assert_eq!(
21791 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21792 "\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",
21793 "After unfolding the all buffers, all original text should be displayed"
21794 );
21795}
21796
21797#[gpui::test]
21798async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21799 init_test(cx, |_| {});
21800
21801 let sample_text_1 = "1111\n2222\n3333".to_string();
21802 let sample_text_2 = "4444\n5555\n6666".to_string();
21803 let sample_text_3 = "7777\n8888\n9999".to_string();
21804
21805 let fs = FakeFs::new(cx.executor());
21806 fs.insert_tree(
21807 path!("/a"),
21808 json!({
21809 "first.rs": sample_text_1,
21810 "second.rs": sample_text_2,
21811 "third.rs": sample_text_3,
21812 }),
21813 )
21814 .await;
21815 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21817 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21818 let worktree = project.update(cx, |project, cx| {
21819 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21820 assert_eq!(worktrees.len(), 1);
21821 worktrees.pop().unwrap()
21822 });
21823 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21824
21825 let buffer_1 = project
21826 .update(cx, |project, cx| {
21827 project.open_buffer((worktree_id, "first.rs"), cx)
21828 })
21829 .await
21830 .unwrap();
21831 let buffer_2 = project
21832 .update(cx, |project, cx| {
21833 project.open_buffer((worktree_id, "second.rs"), cx)
21834 })
21835 .await
21836 .unwrap();
21837 let buffer_3 = project
21838 .update(cx, |project, cx| {
21839 project.open_buffer((worktree_id, "third.rs"), cx)
21840 })
21841 .await
21842 .unwrap();
21843
21844 let multi_buffer = cx.new(|cx| {
21845 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21846 multi_buffer.push_excerpts(
21847 buffer_1.clone(),
21848 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21849 cx,
21850 );
21851 multi_buffer.push_excerpts(
21852 buffer_2.clone(),
21853 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21854 cx,
21855 );
21856 multi_buffer.push_excerpts(
21857 buffer_3.clone(),
21858 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21859 cx,
21860 );
21861 multi_buffer
21862 });
21863
21864 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21865 Editor::new(
21866 EditorMode::full(),
21867 multi_buffer,
21868 Some(project.clone()),
21869 window,
21870 cx,
21871 )
21872 });
21873
21874 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21875 assert_eq!(
21876 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21877 full_text,
21878 );
21879
21880 multi_buffer_editor.update(cx, |editor, cx| {
21881 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21882 });
21883 assert_eq!(
21884 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21885 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21886 "After folding the first buffer, its text should not be displayed"
21887 );
21888
21889 multi_buffer_editor.update(cx, |editor, cx| {
21890 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21891 });
21892
21893 assert_eq!(
21894 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21895 "\n\n\n\n\n\n7777\n8888\n9999",
21896 "After folding the second buffer, its text should not be displayed"
21897 );
21898
21899 multi_buffer_editor.update(cx, |editor, cx| {
21900 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21901 });
21902 assert_eq!(
21903 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21904 "\n\n\n\n\n",
21905 "After folding the third buffer, its text should not be displayed"
21906 );
21907
21908 multi_buffer_editor.update(cx, |editor, cx| {
21909 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21910 });
21911 assert_eq!(
21912 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21913 "\n\n\n\n4444\n5555\n6666\n\n",
21914 "After unfolding the second buffer, its text should be displayed"
21915 );
21916
21917 multi_buffer_editor.update(cx, |editor, cx| {
21918 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21919 });
21920 assert_eq!(
21921 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21922 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21923 "After unfolding the first buffer, its text should be displayed"
21924 );
21925
21926 multi_buffer_editor.update(cx, |editor, cx| {
21927 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21928 });
21929 assert_eq!(
21930 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21931 full_text,
21932 "After unfolding all buffers, all original text should be displayed"
21933 );
21934}
21935
21936#[gpui::test]
21937async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21938 init_test(cx, |_| {});
21939
21940 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21941
21942 let fs = FakeFs::new(cx.executor());
21943 fs.insert_tree(
21944 path!("/a"),
21945 json!({
21946 "main.rs": sample_text,
21947 }),
21948 )
21949 .await;
21950 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21951 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21952 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21953 let worktree = project.update(cx, |project, cx| {
21954 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21955 assert_eq!(worktrees.len(), 1);
21956 worktrees.pop().unwrap()
21957 });
21958 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21959
21960 let buffer_1 = project
21961 .update(cx, |project, cx| {
21962 project.open_buffer((worktree_id, "main.rs"), cx)
21963 })
21964 .await
21965 .unwrap();
21966
21967 let multi_buffer = cx.new(|cx| {
21968 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21969 multi_buffer.push_excerpts(
21970 buffer_1.clone(),
21971 [ExcerptRange::new(
21972 Point::new(0, 0)
21973 ..Point::new(
21974 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21975 0,
21976 ),
21977 )],
21978 cx,
21979 );
21980 multi_buffer
21981 });
21982 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21983 Editor::new(
21984 EditorMode::full(),
21985 multi_buffer,
21986 Some(project.clone()),
21987 window,
21988 cx,
21989 )
21990 });
21991
21992 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21993 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21994 enum TestHighlight {}
21995 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21996 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21997 editor.highlight_text::<TestHighlight>(
21998 vec![highlight_range.clone()],
21999 HighlightStyle::color(Hsla::green()),
22000 cx,
22001 );
22002 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22003 s.select_ranges(Some(highlight_range))
22004 });
22005 });
22006
22007 let full_text = format!("\n\n{sample_text}");
22008 assert_eq!(
22009 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22010 full_text,
22011 );
22012}
22013
22014#[gpui::test]
22015async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22016 init_test(cx, |_| {});
22017 cx.update(|cx| {
22018 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22019 "keymaps/default-linux.json",
22020 cx,
22021 )
22022 .unwrap();
22023 cx.bind_keys(default_key_bindings);
22024 });
22025
22026 let (editor, cx) = cx.add_window_view(|window, cx| {
22027 let multi_buffer = MultiBuffer::build_multi(
22028 [
22029 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22030 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22031 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22032 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22033 ],
22034 cx,
22035 );
22036 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22037
22038 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22039 // fold all but the second buffer, so that we test navigating between two
22040 // adjacent folded buffers, as well as folded buffers at the start and
22041 // end the multibuffer
22042 editor.fold_buffer(buffer_ids[0], cx);
22043 editor.fold_buffer(buffer_ids[2], cx);
22044 editor.fold_buffer(buffer_ids[3], cx);
22045
22046 editor
22047 });
22048 cx.simulate_resize(size(px(1000.), px(1000.)));
22049
22050 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22051 cx.assert_excerpts_with_selections(indoc! {"
22052 [EXCERPT]
22053 ˇ[FOLDED]
22054 [EXCERPT]
22055 a1
22056 b1
22057 [EXCERPT]
22058 [FOLDED]
22059 [EXCERPT]
22060 [FOLDED]
22061 "
22062 });
22063 cx.simulate_keystroke("down");
22064 cx.assert_excerpts_with_selections(indoc! {"
22065 [EXCERPT]
22066 [FOLDED]
22067 [EXCERPT]
22068 ˇa1
22069 b1
22070 [EXCERPT]
22071 [FOLDED]
22072 [EXCERPT]
22073 [FOLDED]
22074 "
22075 });
22076 cx.simulate_keystroke("down");
22077 cx.assert_excerpts_with_selections(indoc! {"
22078 [EXCERPT]
22079 [FOLDED]
22080 [EXCERPT]
22081 a1
22082 ˇb1
22083 [EXCERPT]
22084 [FOLDED]
22085 [EXCERPT]
22086 [FOLDED]
22087 "
22088 });
22089 cx.simulate_keystroke("down");
22090 cx.assert_excerpts_with_selections(indoc! {"
22091 [EXCERPT]
22092 [FOLDED]
22093 [EXCERPT]
22094 a1
22095 b1
22096 ˇ[EXCERPT]
22097 [FOLDED]
22098 [EXCERPT]
22099 [FOLDED]
22100 "
22101 });
22102 cx.simulate_keystroke("down");
22103 cx.assert_excerpts_with_selections(indoc! {"
22104 [EXCERPT]
22105 [FOLDED]
22106 [EXCERPT]
22107 a1
22108 b1
22109 [EXCERPT]
22110 ˇ[FOLDED]
22111 [EXCERPT]
22112 [FOLDED]
22113 "
22114 });
22115 for _ in 0..5 {
22116 cx.simulate_keystroke("down");
22117 cx.assert_excerpts_with_selections(indoc! {"
22118 [EXCERPT]
22119 [FOLDED]
22120 [EXCERPT]
22121 a1
22122 b1
22123 [EXCERPT]
22124 [FOLDED]
22125 [EXCERPT]
22126 ˇ[FOLDED]
22127 "
22128 });
22129 }
22130
22131 cx.simulate_keystroke("up");
22132 cx.assert_excerpts_with_selections(indoc! {"
22133 [EXCERPT]
22134 [FOLDED]
22135 [EXCERPT]
22136 a1
22137 b1
22138 [EXCERPT]
22139 ˇ[FOLDED]
22140 [EXCERPT]
22141 [FOLDED]
22142 "
22143 });
22144 cx.simulate_keystroke("up");
22145 cx.assert_excerpts_with_selections(indoc! {"
22146 [EXCERPT]
22147 [FOLDED]
22148 [EXCERPT]
22149 a1
22150 b1
22151 ˇ[EXCERPT]
22152 [FOLDED]
22153 [EXCERPT]
22154 [FOLDED]
22155 "
22156 });
22157 cx.simulate_keystroke("up");
22158 cx.assert_excerpts_with_selections(indoc! {"
22159 [EXCERPT]
22160 [FOLDED]
22161 [EXCERPT]
22162 a1
22163 ˇb1
22164 [EXCERPT]
22165 [FOLDED]
22166 [EXCERPT]
22167 [FOLDED]
22168 "
22169 });
22170 cx.simulate_keystroke("up");
22171 cx.assert_excerpts_with_selections(indoc! {"
22172 [EXCERPT]
22173 [FOLDED]
22174 [EXCERPT]
22175 ˇa1
22176 b1
22177 [EXCERPT]
22178 [FOLDED]
22179 [EXCERPT]
22180 [FOLDED]
22181 "
22182 });
22183 for _ in 0..5 {
22184 cx.simulate_keystroke("up");
22185 cx.assert_excerpts_with_selections(indoc! {"
22186 [EXCERPT]
22187 ˇ[FOLDED]
22188 [EXCERPT]
22189 a1
22190 b1
22191 [EXCERPT]
22192 [FOLDED]
22193 [EXCERPT]
22194 [FOLDED]
22195 "
22196 });
22197 }
22198}
22199
22200#[gpui::test]
22201async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22202 init_test(cx, |_| {});
22203
22204 // Simple insertion
22205 assert_highlighted_edits(
22206 "Hello, world!",
22207 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22208 true,
22209 cx,
22210 |highlighted_edits, cx| {
22211 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22212 assert_eq!(highlighted_edits.highlights.len(), 1);
22213 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22214 assert_eq!(
22215 highlighted_edits.highlights[0].1.background_color,
22216 Some(cx.theme().status().created_background)
22217 );
22218 },
22219 )
22220 .await;
22221
22222 // Replacement
22223 assert_highlighted_edits(
22224 "This is a test.",
22225 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22226 false,
22227 cx,
22228 |highlighted_edits, cx| {
22229 assert_eq!(highlighted_edits.text, "That is a test.");
22230 assert_eq!(highlighted_edits.highlights.len(), 1);
22231 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22232 assert_eq!(
22233 highlighted_edits.highlights[0].1.background_color,
22234 Some(cx.theme().status().created_background)
22235 );
22236 },
22237 )
22238 .await;
22239
22240 // Multiple edits
22241 assert_highlighted_edits(
22242 "Hello, world!",
22243 vec![
22244 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22245 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22246 ],
22247 false,
22248 cx,
22249 |highlighted_edits, cx| {
22250 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22251 assert_eq!(highlighted_edits.highlights.len(), 2);
22252 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22253 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22254 assert_eq!(
22255 highlighted_edits.highlights[0].1.background_color,
22256 Some(cx.theme().status().created_background)
22257 );
22258 assert_eq!(
22259 highlighted_edits.highlights[1].1.background_color,
22260 Some(cx.theme().status().created_background)
22261 );
22262 },
22263 )
22264 .await;
22265
22266 // Multiple lines with edits
22267 assert_highlighted_edits(
22268 "First line\nSecond line\nThird line\nFourth line",
22269 vec![
22270 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22271 (
22272 Point::new(2, 0)..Point::new(2, 10),
22273 "New third line".to_string(),
22274 ),
22275 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22276 ],
22277 false,
22278 cx,
22279 |highlighted_edits, cx| {
22280 assert_eq!(
22281 highlighted_edits.text,
22282 "Second modified\nNew third line\nFourth updated line"
22283 );
22284 assert_eq!(highlighted_edits.highlights.len(), 3);
22285 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22286 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22287 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22288 for highlight in &highlighted_edits.highlights {
22289 assert_eq!(
22290 highlight.1.background_color,
22291 Some(cx.theme().status().created_background)
22292 );
22293 }
22294 },
22295 )
22296 .await;
22297}
22298
22299#[gpui::test]
22300async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22301 init_test(cx, |_| {});
22302
22303 // Deletion
22304 assert_highlighted_edits(
22305 "Hello, world!",
22306 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22307 true,
22308 cx,
22309 |highlighted_edits, cx| {
22310 assert_eq!(highlighted_edits.text, "Hello, world!");
22311 assert_eq!(highlighted_edits.highlights.len(), 1);
22312 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22313 assert_eq!(
22314 highlighted_edits.highlights[0].1.background_color,
22315 Some(cx.theme().status().deleted_background)
22316 );
22317 },
22318 )
22319 .await;
22320
22321 // Insertion
22322 assert_highlighted_edits(
22323 "Hello, world!",
22324 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22325 true,
22326 cx,
22327 |highlighted_edits, cx| {
22328 assert_eq!(highlighted_edits.highlights.len(), 1);
22329 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22330 assert_eq!(
22331 highlighted_edits.highlights[0].1.background_color,
22332 Some(cx.theme().status().created_background)
22333 );
22334 },
22335 )
22336 .await;
22337}
22338
22339async fn assert_highlighted_edits(
22340 text: &str,
22341 edits: Vec<(Range<Point>, String)>,
22342 include_deletions: bool,
22343 cx: &mut TestAppContext,
22344 assertion_fn: impl Fn(HighlightedText, &App),
22345) {
22346 let window = cx.add_window(|window, cx| {
22347 let buffer = MultiBuffer::build_simple(text, cx);
22348 Editor::new(EditorMode::full(), buffer, None, window, cx)
22349 });
22350 let cx = &mut VisualTestContext::from_window(*window, cx);
22351
22352 let (buffer, snapshot) = window
22353 .update(cx, |editor, _window, cx| {
22354 (
22355 editor.buffer().clone(),
22356 editor.buffer().read(cx).snapshot(cx),
22357 )
22358 })
22359 .unwrap();
22360
22361 let edits = edits
22362 .into_iter()
22363 .map(|(range, edit)| {
22364 (
22365 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22366 edit,
22367 )
22368 })
22369 .collect::<Vec<_>>();
22370
22371 let text_anchor_edits = edits
22372 .clone()
22373 .into_iter()
22374 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22375 .collect::<Vec<_>>();
22376
22377 let edit_preview = window
22378 .update(cx, |_, _window, cx| {
22379 buffer
22380 .read(cx)
22381 .as_singleton()
22382 .unwrap()
22383 .read(cx)
22384 .preview_edits(text_anchor_edits.into(), cx)
22385 })
22386 .unwrap()
22387 .await;
22388
22389 cx.update(|_window, cx| {
22390 let highlighted_edits = edit_prediction_edit_text(
22391 snapshot.as_singleton().unwrap().2,
22392 &edits,
22393 &edit_preview,
22394 include_deletions,
22395 cx,
22396 );
22397 assertion_fn(highlighted_edits, cx)
22398 });
22399}
22400
22401#[track_caller]
22402fn assert_breakpoint(
22403 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22404 path: &Arc<Path>,
22405 expected: Vec<(u32, Breakpoint)>,
22406) {
22407 if expected.is_empty() {
22408 assert!(!breakpoints.contains_key(path), "{}", path.display());
22409 } else {
22410 let mut breakpoint = breakpoints
22411 .get(path)
22412 .unwrap()
22413 .iter()
22414 .map(|breakpoint| {
22415 (
22416 breakpoint.row,
22417 Breakpoint {
22418 message: breakpoint.message.clone(),
22419 state: breakpoint.state,
22420 condition: breakpoint.condition.clone(),
22421 hit_condition: breakpoint.hit_condition.clone(),
22422 },
22423 )
22424 })
22425 .collect::<Vec<_>>();
22426
22427 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22428
22429 assert_eq!(expected, breakpoint);
22430 }
22431}
22432
22433fn add_log_breakpoint_at_cursor(
22434 editor: &mut Editor,
22435 log_message: &str,
22436 window: &mut Window,
22437 cx: &mut Context<Editor>,
22438) {
22439 let (anchor, bp) = editor
22440 .breakpoints_at_cursors(window, cx)
22441 .first()
22442 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22443 .unwrap_or_else(|| {
22444 let cursor_position: Point = editor.selections.newest(cx).head();
22445
22446 let breakpoint_position = editor
22447 .snapshot(window, cx)
22448 .display_snapshot
22449 .buffer_snapshot
22450 .anchor_before(Point::new(cursor_position.row, 0));
22451
22452 (breakpoint_position, Breakpoint::new_log(log_message))
22453 });
22454
22455 editor.edit_breakpoint_at_anchor(
22456 anchor,
22457 bp,
22458 BreakpointEditAction::EditLogMessage(log_message.into()),
22459 cx,
22460 );
22461}
22462
22463#[gpui::test]
22464async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22465 init_test(cx, |_| {});
22466
22467 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22468 let fs = FakeFs::new(cx.executor());
22469 fs.insert_tree(
22470 path!("/a"),
22471 json!({
22472 "main.rs": sample_text,
22473 }),
22474 )
22475 .await;
22476 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22477 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22478 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22479
22480 let fs = FakeFs::new(cx.executor());
22481 fs.insert_tree(
22482 path!("/a"),
22483 json!({
22484 "main.rs": sample_text,
22485 }),
22486 )
22487 .await;
22488 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22489 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22490 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22491 let worktree_id = workspace
22492 .update(cx, |workspace, _window, cx| {
22493 workspace.project().update(cx, |project, cx| {
22494 project.worktrees(cx).next().unwrap().read(cx).id()
22495 })
22496 })
22497 .unwrap();
22498
22499 let buffer = project
22500 .update(cx, |project, cx| {
22501 project.open_buffer((worktree_id, "main.rs"), cx)
22502 })
22503 .await
22504 .unwrap();
22505
22506 let (editor, cx) = cx.add_window_view(|window, cx| {
22507 Editor::new(
22508 EditorMode::full(),
22509 MultiBuffer::build_from_buffer(buffer, cx),
22510 Some(project.clone()),
22511 window,
22512 cx,
22513 )
22514 });
22515
22516 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22517 let abs_path = project.read_with(cx, |project, cx| {
22518 project
22519 .absolute_path(&project_path, cx)
22520 .map(Arc::from)
22521 .unwrap()
22522 });
22523
22524 // assert we can add breakpoint on the first line
22525 editor.update_in(cx, |editor, window, cx| {
22526 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22527 editor.move_to_end(&MoveToEnd, window, cx);
22528 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22529 });
22530
22531 let breakpoints = editor.update(cx, |editor, cx| {
22532 editor
22533 .breakpoint_store()
22534 .as_ref()
22535 .unwrap()
22536 .read(cx)
22537 .all_source_breakpoints(cx)
22538 });
22539
22540 assert_eq!(1, breakpoints.len());
22541 assert_breakpoint(
22542 &breakpoints,
22543 &abs_path,
22544 vec![
22545 (0, Breakpoint::new_standard()),
22546 (3, Breakpoint::new_standard()),
22547 ],
22548 );
22549
22550 editor.update_in(cx, |editor, window, cx| {
22551 editor.move_to_beginning(&MoveToBeginning, window, cx);
22552 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22553 });
22554
22555 let breakpoints = editor.update(cx, |editor, cx| {
22556 editor
22557 .breakpoint_store()
22558 .as_ref()
22559 .unwrap()
22560 .read(cx)
22561 .all_source_breakpoints(cx)
22562 });
22563
22564 assert_eq!(1, breakpoints.len());
22565 assert_breakpoint(
22566 &breakpoints,
22567 &abs_path,
22568 vec![(3, Breakpoint::new_standard())],
22569 );
22570
22571 editor.update_in(cx, |editor, window, cx| {
22572 editor.move_to_end(&MoveToEnd, window, cx);
22573 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22574 });
22575
22576 let breakpoints = editor.update(cx, |editor, cx| {
22577 editor
22578 .breakpoint_store()
22579 .as_ref()
22580 .unwrap()
22581 .read(cx)
22582 .all_source_breakpoints(cx)
22583 });
22584
22585 assert_eq!(0, breakpoints.len());
22586 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22587}
22588
22589#[gpui::test]
22590async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22591 init_test(cx, |_| {});
22592
22593 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22594
22595 let fs = FakeFs::new(cx.executor());
22596 fs.insert_tree(
22597 path!("/a"),
22598 json!({
22599 "main.rs": sample_text,
22600 }),
22601 )
22602 .await;
22603 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22604 let (workspace, cx) =
22605 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22606
22607 let worktree_id = workspace.update(cx, |workspace, cx| {
22608 workspace.project().update(cx, |project, cx| {
22609 project.worktrees(cx).next().unwrap().read(cx).id()
22610 })
22611 });
22612
22613 let buffer = project
22614 .update(cx, |project, cx| {
22615 project.open_buffer((worktree_id, "main.rs"), cx)
22616 })
22617 .await
22618 .unwrap();
22619
22620 let (editor, cx) = cx.add_window_view(|window, cx| {
22621 Editor::new(
22622 EditorMode::full(),
22623 MultiBuffer::build_from_buffer(buffer, cx),
22624 Some(project.clone()),
22625 window,
22626 cx,
22627 )
22628 });
22629
22630 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22631 let abs_path = project.read_with(cx, |project, cx| {
22632 project
22633 .absolute_path(&project_path, cx)
22634 .map(Arc::from)
22635 .unwrap()
22636 });
22637
22638 editor.update_in(cx, |editor, window, cx| {
22639 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22640 });
22641
22642 let breakpoints = editor.update(cx, |editor, cx| {
22643 editor
22644 .breakpoint_store()
22645 .as_ref()
22646 .unwrap()
22647 .read(cx)
22648 .all_source_breakpoints(cx)
22649 });
22650
22651 assert_breakpoint(
22652 &breakpoints,
22653 &abs_path,
22654 vec![(0, Breakpoint::new_log("hello world"))],
22655 );
22656
22657 // Removing a log message from a log breakpoint should remove it
22658 editor.update_in(cx, |editor, window, cx| {
22659 add_log_breakpoint_at_cursor(editor, "", window, cx);
22660 });
22661
22662 let breakpoints = editor.update(cx, |editor, cx| {
22663 editor
22664 .breakpoint_store()
22665 .as_ref()
22666 .unwrap()
22667 .read(cx)
22668 .all_source_breakpoints(cx)
22669 });
22670
22671 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22672
22673 editor.update_in(cx, |editor, window, cx| {
22674 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22675 editor.move_to_end(&MoveToEnd, window, cx);
22676 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22677 // Not adding a log message to a standard breakpoint shouldn't remove it
22678 add_log_breakpoint_at_cursor(editor, "", window, cx);
22679 });
22680
22681 let breakpoints = editor.update(cx, |editor, cx| {
22682 editor
22683 .breakpoint_store()
22684 .as_ref()
22685 .unwrap()
22686 .read(cx)
22687 .all_source_breakpoints(cx)
22688 });
22689
22690 assert_breakpoint(
22691 &breakpoints,
22692 &abs_path,
22693 vec![
22694 (0, Breakpoint::new_standard()),
22695 (3, Breakpoint::new_standard()),
22696 ],
22697 );
22698
22699 editor.update_in(cx, |editor, window, cx| {
22700 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22701 });
22702
22703 let breakpoints = editor.update(cx, |editor, cx| {
22704 editor
22705 .breakpoint_store()
22706 .as_ref()
22707 .unwrap()
22708 .read(cx)
22709 .all_source_breakpoints(cx)
22710 });
22711
22712 assert_breakpoint(
22713 &breakpoints,
22714 &abs_path,
22715 vec![
22716 (0, Breakpoint::new_standard()),
22717 (3, Breakpoint::new_log("hello world")),
22718 ],
22719 );
22720
22721 editor.update_in(cx, |editor, window, cx| {
22722 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22723 });
22724
22725 let breakpoints = editor.update(cx, |editor, cx| {
22726 editor
22727 .breakpoint_store()
22728 .as_ref()
22729 .unwrap()
22730 .read(cx)
22731 .all_source_breakpoints(cx)
22732 });
22733
22734 assert_breakpoint(
22735 &breakpoints,
22736 &abs_path,
22737 vec![
22738 (0, Breakpoint::new_standard()),
22739 (3, Breakpoint::new_log("hello Earth!!")),
22740 ],
22741 );
22742}
22743
22744/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22745/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22746/// or when breakpoints were placed out of order. This tests for a regression too
22747#[gpui::test]
22748async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22749 init_test(cx, |_| {});
22750
22751 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22752 let fs = FakeFs::new(cx.executor());
22753 fs.insert_tree(
22754 path!("/a"),
22755 json!({
22756 "main.rs": sample_text,
22757 }),
22758 )
22759 .await;
22760 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22761 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22762 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22763
22764 let fs = FakeFs::new(cx.executor());
22765 fs.insert_tree(
22766 path!("/a"),
22767 json!({
22768 "main.rs": sample_text,
22769 }),
22770 )
22771 .await;
22772 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22773 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22774 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22775 let worktree_id = workspace
22776 .update(cx, |workspace, _window, cx| {
22777 workspace.project().update(cx, |project, cx| {
22778 project.worktrees(cx).next().unwrap().read(cx).id()
22779 })
22780 })
22781 .unwrap();
22782
22783 let buffer = project
22784 .update(cx, |project, cx| {
22785 project.open_buffer((worktree_id, "main.rs"), cx)
22786 })
22787 .await
22788 .unwrap();
22789
22790 let (editor, cx) = cx.add_window_view(|window, cx| {
22791 Editor::new(
22792 EditorMode::full(),
22793 MultiBuffer::build_from_buffer(buffer, cx),
22794 Some(project.clone()),
22795 window,
22796 cx,
22797 )
22798 });
22799
22800 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22801 let abs_path = project.read_with(cx, |project, cx| {
22802 project
22803 .absolute_path(&project_path, cx)
22804 .map(Arc::from)
22805 .unwrap()
22806 });
22807
22808 // assert we can add breakpoint on the first line
22809 editor.update_in(cx, |editor, window, cx| {
22810 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22811 editor.move_to_end(&MoveToEnd, window, cx);
22812 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22813 editor.move_up(&MoveUp, window, cx);
22814 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22815 });
22816
22817 let breakpoints = editor.update(cx, |editor, cx| {
22818 editor
22819 .breakpoint_store()
22820 .as_ref()
22821 .unwrap()
22822 .read(cx)
22823 .all_source_breakpoints(cx)
22824 });
22825
22826 assert_eq!(1, breakpoints.len());
22827 assert_breakpoint(
22828 &breakpoints,
22829 &abs_path,
22830 vec![
22831 (0, Breakpoint::new_standard()),
22832 (2, Breakpoint::new_standard()),
22833 (3, Breakpoint::new_standard()),
22834 ],
22835 );
22836
22837 editor.update_in(cx, |editor, window, cx| {
22838 editor.move_to_beginning(&MoveToBeginning, window, cx);
22839 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22840 editor.move_to_end(&MoveToEnd, window, cx);
22841 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22842 // Disabling a breakpoint that doesn't exist should do nothing
22843 editor.move_up(&MoveUp, window, cx);
22844 editor.move_up(&MoveUp, window, cx);
22845 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22846 });
22847
22848 let breakpoints = editor.update(cx, |editor, cx| {
22849 editor
22850 .breakpoint_store()
22851 .as_ref()
22852 .unwrap()
22853 .read(cx)
22854 .all_source_breakpoints(cx)
22855 });
22856
22857 let disable_breakpoint = {
22858 let mut bp = Breakpoint::new_standard();
22859 bp.state = BreakpointState::Disabled;
22860 bp
22861 };
22862
22863 assert_eq!(1, breakpoints.len());
22864 assert_breakpoint(
22865 &breakpoints,
22866 &abs_path,
22867 vec![
22868 (0, disable_breakpoint.clone()),
22869 (2, Breakpoint::new_standard()),
22870 (3, disable_breakpoint.clone()),
22871 ],
22872 );
22873
22874 editor.update_in(cx, |editor, window, cx| {
22875 editor.move_to_beginning(&MoveToBeginning, window, cx);
22876 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22877 editor.move_to_end(&MoveToEnd, window, cx);
22878 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22879 editor.move_up(&MoveUp, window, cx);
22880 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22881 });
22882
22883 let breakpoints = editor.update(cx, |editor, cx| {
22884 editor
22885 .breakpoint_store()
22886 .as_ref()
22887 .unwrap()
22888 .read(cx)
22889 .all_source_breakpoints(cx)
22890 });
22891
22892 assert_eq!(1, breakpoints.len());
22893 assert_breakpoint(
22894 &breakpoints,
22895 &abs_path,
22896 vec![
22897 (0, Breakpoint::new_standard()),
22898 (2, disable_breakpoint),
22899 (3, Breakpoint::new_standard()),
22900 ],
22901 );
22902}
22903
22904#[gpui::test]
22905async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22906 init_test(cx, |_| {});
22907 let capabilities = lsp::ServerCapabilities {
22908 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22909 prepare_provider: Some(true),
22910 work_done_progress_options: Default::default(),
22911 })),
22912 ..Default::default()
22913 };
22914 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22915
22916 cx.set_state(indoc! {"
22917 struct Fˇoo {}
22918 "});
22919
22920 cx.update_editor(|editor, _, cx| {
22921 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22922 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22923 editor.highlight_background::<DocumentHighlightRead>(
22924 &[highlight_range],
22925 |theme| theme.colors().editor_document_highlight_read_background,
22926 cx,
22927 );
22928 });
22929
22930 let mut prepare_rename_handler = cx
22931 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22932 move |_, _, _| async move {
22933 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22934 start: lsp::Position {
22935 line: 0,
22936 character: 7,
22937 },
22938 end: lsp::Position {
22939 line: 0,
22940 character: 10,
22941 },
22942 })))
22943 },
22944 );
22945 let prepare_rename_task = cx
22946 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22947 .expect("Prepare rename was not started");
22948 prepare_rename_handler.next().await.unwrap();
22949 prepare_rename_task.await.expect("Prepare rename failed");
22950
22951 let mut rename_handler =
22952 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22953 let edit = lsp::TextEdit {
22954 range: lsp::Range {
22955 start: lsp::Position {
22956 line: 0,
22957 character: 7,
22958 },
22959 end: lsp::Position {
22960 line: 0,
22961 character: 10,
22962 },
22963 },
22964 new_text: "FooRenamed".to_string(),
22965 };
22966 Ok(Some(lsp::WorkspaceEdit::new(
22967 // Specify the same edit twice
22968 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22969 )))
22970 });
22971 let rename_task = cx
22972 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22973 .expect("Confirm rename was not started");
22974 rename_handler.next().await.unwrap();
22975 rename_task.await.expect("Confirm rename failed");
22976 cx.run_until_parked();
22977
22978 // Despite two edits, only one is actually applied as those are identical
22979 cx.assert_editor_state(indoc! {"
22980 struct FooRenamedˇ {}
22981 "});
22982}
22983
22984#[gpui::test]
22985async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22986 init_test(cx, |_| {});
22987 // These capabilities indicate that the server does not support prepare rename.
22988 let capabilities = lsp::ServerCapabilities {
22989 rename_provider: Some(lsp::OneOf::Left(true)),
22990 ..Default::default()
22991 };
22992 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22993
22994 cx.set_state(indoc! {"
22995 struct Fˇoo {}
22996 "});
22997
22998 cx.update_editor(|editor, _window, cx| {
22999 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23000 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23001 editor.highlight_background::<DocumentHighlightRead>(
23002 &[highlight_range],
23003 |theme| theme.colors().editor_document_highlight_read_background,
23004 cx,
23005 );
23006 });
23007
23008 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23009 .expect("Prepare rename was not started")
23010 .await
23011 .expect("Prepare rename failed");
23012
23013 let mut rename_handler =
23014 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23015 let edit = lsp::TextEdit {
23016 range: lsp::Range {
23017 start: lsp::Position {
23018 line: 0,
23019 character: 7,
23020 },
23021 end: lsp::Position {
23022 line: 0,
23023 character: 10,
23024 },
23025 },
23026 new_text: "FooRenamed".to_string(),
23027 };
23028 Ok(Some(lsp::WorkspaceEdit::new(
23029 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23030 )))
23031 });
23032 let rename_task = cx
23033 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23034 .expect("Confirm rename was not started");
23035 rename_handler.next().await.unwrap();
23036 rename_task.await.expect("Confirm rename failed");
23037 cx.run_until_parked();
23038
23039 // Correct range is renamed, as `surrounding_word` is used to find it.
23040 cx.assert_editor_state(indoc! {"
23041 struct FooRenamedˇ {}
23042 "});
23043}
23044
23045#[gpui::test]
23046async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23047 init_test(cx, |_| {});
23048 let mut cx = EditorTestContext::new(cx).await;
23049
23050 let language = Arc::new(
23051 Language::new(
23052 LanguageConfig::default(),
23053 Some(tree_sitter_html::LANGUAGE.into()),
23054 )
23055 .with_brackets_query(
23056 r#"
23057 ("<" @open "/>" @close)
23058 ("</" @open ">" @close)
23059 ("<" @open ">" @close)
23060 ("\"" @open "\"" @close)
23061 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23062 "#,
23063 )
23064 .unwrap(),
23065 );
23066 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23067
23068 cx.set_state(indoc! {"
23069 <span>ˇ</span>
23070 "});
23071 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23072 cx.assert_editor_state(indoc! {"
23073 <span>
23074 ˇ
23075 </span>
23076 "});
23077
23078 cx.set_state(indoc! {"
23079 <span><span></span>ˇ</span>
23080 "});
23081 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23082 cx.assert_editor_state(indoc! {"
23083 <span><span></span>
23084 ˇ</span>
23085 "});
23086
23087 cx.set_state(indoc! {"
23088 <span>ˇ
23089 </span>
23090 "});
23091 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23092 cx.assert_editor_state(indoc! {"
23093 <span>
23094 ˇ
23095 </span>
23096 "});
23097}
23098
23099#[gpui::test(iterations = 10)]
23100async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23101 init_test(cx, |_| {});
23102
23103 let fs = FakeFs::new(cx.executor());
23104 fs.insert_tree(
23105 path!("/dir"),
23106 json!({
23107 "a.ts": "a",
23108 }),
23109 )
23110 .await;
23111
23112 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23113 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23114 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23115
23116 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23117 language_registry.add(Arc::new(Language::new(
23118 LanguageConfig {
23119 name: "TypeScript".into(),
23120 matcher: LanguageMatcher {
23121 path_suffixes: vec!["ts".to_string()],
23122 ..Default::default()
23123 },
23124 ..Default::default()
23125 },
23126 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23127 )));
23128 let mut fake_language_servers = language_registry.register_fake_lsp(
23129 "TypeScript",
23130 FakeLspAdapter {
23131 capabilities: lsp::ServerCapabilities {
23132 code_lens_provider: Some(lsp::CodeLensOptions {
23133 resolve_provider: Some(true),
23134 }),
23135 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23136 commands: vec!["_the/command".to_string()],
23137 ..lsp::ExecuteCommandOptions::default()
23138 }),
23139 ..lsp::ServerCapabilities::default()
23140 },
23141 ..FakeLspAdapter::default()
23142 },
23143 );
23144
23145 let editor = workspace
23146 .update(cx, |workspace, window, cx| {
23147 workspace.open_abs_path(
23148 PathBuf::from(path!("/dir/a.ts")),
23149 OpenOptions::default(),
23150 window,
23151 cx,
23152 )
23153 })
23154 .unwrap()
23155 .await
23156 .unwrap()
23157 .downcast::<Editor>()
23158 .unwrap();
23159 cx.executor().run_until_parked();
23160
23161 let fake_server = fake_language_servers.next().await.unwrap();
23162
23163 let buffer = editor.update(cx, |editor, cx| {
23164 editor
23165 .buffer()
23166 .read(cx)
23167 .as_singleton()
23168 .expect("have opened a single file by path")
23169 });
23170
23171 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23172 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23173 drop(buffer_snapshot);
23174 let actions = cx
23175 .update_window(*workspace, |_, window, cx| {
23176 project.code_actions(&buffer, anchor..anchor, window, cx)
23177 })
23178 .unwrap();
23179
23180 fake_server
23181 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23182 Ok(Some(vec![
23183 lsp::CodeLens {
23184 range: lsp::Range::default(),
23185 command: Some(lsp::Command {
23186 title: "Code lens command".to_owned(),
23187 command: "_the/command".to_owned(),
23188 arguments: None,
23189 }),
23190 data: None,
23191 },
23192 lsp::CodeLens {
23193 range: lsp::Range::default(),
23194 command: Some(lsp::Command {
23195 title: "Command not in capabilities".to_owned(),
23196 command: "not in capabilities".to_owned(),
23197 arguments: None,
23198 }),
23199 data: None,
23200 },
23201 lsp::CodeLens {
23202 range: lsp::Range {
23203 start: lsp::Position {
23204 line: 1,
23205 character: 1,
23206 },
23207 end: lsp::Position {
23208 line: 1,
23209 character: 1,
23210 },
23211 },
23212 command: Some(lsp::Command {
23213 title: "Command not in range".to_owned(),
23214 command: "_the/command".to_owned(),
23215 arguments: None,
23216 }),
23217 data: None,
23218 },
23219 ]))
23220 })
23221 .next()
23222 .await;
23223
23224 let actions = actions.await.unwrap();
23225 assert_eq!(
23226 actions.len(),
23227 1,
23228 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23229 );
23230 let action = actions[0].clone();
23231 let apply = project.update(cx, |project, cx| {
23232 project.apply_code_action(buffer.clone(), action, true, cx)
23233 });
23234
23235 // Resolving the code action does not populate its edits. In absence of
23236 // edits, we must execute the given command.
23237 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23238 |mut lens, _| async move {
23239 let lens_command = lens.command.as_mut().expect("should have a command");
23240 assert_eq!(lens_command.title, "Code lens command");
23241 lens_command.arguments = Some(vec![json!("the-argument")]);
23242 Ok(lens)
23243 },
23244 );
23245
23246 // While executing the command, the language server sends the editor
23247 // a `workspaceEdit` request.
23248 fake_server
23249 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23250 let fake = fake_server.clone();
23251 move |params, _| {
23252 assert_eq!(params.command, "_the/command");
23253 let fake = fake.clone();
23254 async move {
23255 fake.server
23256 .request::<lsp::request::ApplyWorkspaceEdit>(
23257 lsp::ApplyWorkspaceEditParams {
23258 label: None,
23259 edit: lsp::WorkspaceEdit {
23260 changes: Some(
23261 [(
23262 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23263 vec![lsp::TextEdit {
23264 range: lsp::Range::new(
23265 lsp::Position::new(0, 0),
23266 lsp::Position::new(0, 0),
23267 ),
23268 new_text: "X".into(),
23269 }],
23270 )]
23271 .into_iter()
23272 .collect(),
23273 ),
23274 ..lsp::WorkspaceEdit::default()
23275 },
23276 },
23277 )
23278 .await
23279 .into_response()
23280 .unwrap();
23281 Ok(Some(json!(null)))
23282 }
23283 }
23284 })
23285 .next()
23286 .await;
23287
23288 // Applying the code lens command returns a project transaction containing the edits
23289 // sent by the language server in its `workspaceEdit` request.
23290 let transaction = apply.await.unwrap();
23291 assert!(transaction.0.contains_key(&buffer));
23292 buffer.update(cx, |buffer, cx| {
23293 assert_eq!(buffer.text(), "Xa");
23294 buffer.undo(cx);
23295 assert_eq!(buffer.text(), "a");
23296 });
23297
23298 let actions_after_edits = cx
23299 .update_window(*workspace, |_, window, cx| {
23300 project.code_actions(&buffer, anchor..anchor, window, cx)
23301 })
23302 .unwrap()
23303 .await
23304 .unwrap();
23305 assert_eq!(
23306 actions, actions_after_edits,
23307 "For the same selection, same code lens actions should be returned"
23308 );
23309
23310 let _responses =
23311 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23312 panic!("No more code lens requests are expected");
23313 });
23314 editor.update_in(cx, |editor, window, cx| {
23315 editor.select_all(&SelectAll, window, cx);
23316 });
23317 cx.executor().run_until_parked();
23318 let new_actions = cx
23319 .update_window(*workspace, |_, window, cx| {
23320 project.code_actions(&buffer, anchor..anchor, window, cx)
23321 })
23322 .unwrap()
23323 .await
23324 .unwrap();
23325 assert_eq!(
23326 actions, new_actions,
23327 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23328 );
23329}
23330
23331#[gpui::test]
23332async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23333 init_test(cx, |_| {});
23334
23335 let fs = FakeFs::new(cx.executor());
23336 let main_text = r#"fn main() {
23337println!("1");
23338println!("2");
23339println!("3");
23340println!("4");
23341println!("5");
23342}"#;
23343 let lib_text = "mod foo {}";
23344 fs.insert_tree(
23345 path!("/a"),
23346 json!({
23347 "lib.rs": lib_text,
23348 "main.rs": main_text,
23349 }),
23350 )
23351 .await;
23352
23353 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23354 let (workspace, cx) =
23355 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23356 let worktree_id = workspace.update(cx, |workspace, cx| {
23357 workspace.project().update(cx, |project, cx| {
23358 project.worktrees(cx).next().unwrap().read(cx).id()
23359 })
23360 });
23361
23362 let expected_ranges = vec![
23363 Point::new(0, 0)..Point::new(0, 0),
23364 Point::new(1, 0)..Point::new(1, 1),
23365 Point::new(2, 0)..Point::new(2, 2),
23366 Point::new(3, 0)..Point::new(3, 3),
23367 ];
23368
23369 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23370 let editor_1 = workspace
23371 .update_in(cx, |workspace, window, cx| {
23372 workspace.open_path(
23373 (worktree_id, "main.rs"),
23374 Some(pane_1.downgrade()),
23375 true,
23376 window,
23377 cx,
23378 )
23379 })
23380 .unwrap()
23381 .await
23382 .downcast::<Editor>()
23383 .unwrap();
23384 pane_1.update(cx, |pane, cx| {
23385 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23386 open_editor.update(cx, |editor, cx| {
23387 assert_eq!(
23388 editor.display_text(cx),
23389 main_text,
23390 "Original main.rs text on initial open",
23391 );
23392 assert_eq!(
23393 editor
23394 .selections
23395 .all::<Point>(cx)
23396 .into_iter()
23397 .map(|s| s.range())
23398 .collect::<Vec<_>>(),
23399 vec![Point::zero()..Point::zero()],
23400 "Default selections on initial open",
23401 );
23402 })
23403 });
23404 editor_1.update_in(cx, |editor, window, cx| {
23405 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23406 s.select_ranges(expected_ranges.clone());
23407 });
23408 });
23409
23410 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23411 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23412 });
23413 let editor_2 = workspace
23414 .update_in(cx, |workspace, window, cx| {
23415 workspace.open_path(
23416 (worktree_id, "main.rs"),
23417 Some(pane_2.downgrade()),
23418 true,
23419 window,
23420 cx,
23421 )
23422 })
23423 .unwrap()
23424 .await
23425 .downcast::<Editor>()
23426 .unwrap();
23427 pane_2.update(cx, |pane, cx| {
23428 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23429 open_editor.update(cx, |editor, cx| {
23430 assert_eq!(
23431 editor.display_text(cx),
23432 main_text,
23433 "Original main.rs text on initial open in another panel",
23434 );
23435 assert_eq!(
23436 editor
23437 .selections
23438 .all::<Point>(cx)
23439 .into_iter()
23440 .map(|s| s.range())
23441 .collect::<Vec<_>>(),
23442 vec![Point::zero()..Point::zero()],
23443 "Default selections on initial open in another panel",
23444 );
23445 })
23446 });
23447
23448 editor_2.update_in(cx, |editor, window, cx| {
23449 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23450 });
23451
23452 let _other_editor_1 = workspace
23453 .update_in(cx, |workspace, window, cx| {
23454 workspace.open_path(
23455 (worktree_id, "lib.rs"),
23456 Some(pane_1.downgrade()),
23457 true,
23458 window,
23459 cx,
23460 )
23461 })
23462 .unwrap()
23463 .await
23464 .downcast::<Editor>()
23465 .unwrap();
23466 pane_1
23467 .update_in(cx, |pane, window, cx| {
23468 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23469 })
23470 .await
23471 .unwrap();
23472 drop(editor_1);
23473 pane_1.update(cx, |pane, cx| {
23474 pane.active_item()
23475 .unwrap()
23476 .downcast::<Editor>()
23477 .unwrap()
23478 .update(cx, |editor, cx| {
23479 assert_eq!(
23480 editor.display_text(cx),
23481 lib_text,
23482 "Other file should be open and active",
23483 );
23484 });
23485 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23486 });
23487
23488 let _other_editor_2 = workspace
23489 .update_in(cx, |workspace, window, cx| {
23490 workspace.open_path(
23491 (worktree_id, "lib.rs"),
23492 Some(pane_2.downgrade()),
23493 true,
23494 window,
23495 cx,
23496 )
23497 })
23498 .unwrap()
23499 .await
23500 .downcast::<Editor>()
23501 .unwrap();
23502 pane_2
23503 .update_in(cx, |pane, window, cx| {
23504 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23505 })
23506 .await
23507 .unwrap();
23508 drop(editor_2);
23509 pane_2.update(cx, |pane, cx| {
23510 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23511 open_editor.update(cx, |editor, cx| {
23512 assert_eq!(
23513 editor.display_text(cx),
23514 lib_text,
23515 "Other file should be open and active in another panel too",
23516 );
23517 });
23518 assert_eq!(
23519 pane.items().count(),
23520 1,
23521 "No other editors should be open in another pane",
23522 );
23523 });
23524
23525 let _editor_1_reopened = workspace
23526 .update_in(cx, |workspace, window, cx| {
23527 workspace.open_path(
23528 (worktree_id, "main.rs"),
23529 Some(pane_1.downgrade()),
23530 true,
23531 window,
23532 cx,
23533 )
23534 })
23535 .unwrap()
23536 .await
23537 .downcast::<Editor>()
23538 .unwrap();
23539 let _editor_2_reopened = workspace
23540 .update_in(cx, |workspace, window, cx| {
23541 workspace.open_path(
23542 (worktree_id, "main.rs"),
23543 Some(pane_2.downgrade()),
23544 true,
23545 window,
23546 cx,
23547 )
23548 })
23549 .unwrap()
23550 .await
23551 .downcast::<Editor>()
23552 .unwrap();
23553 pane_1.update(cx, |pane, cx| {
23554 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23555 open_editor.update(cx, |editor, cx| {
23556 assert_eq!(
23557 editor.display_text(cx),
23558 main_text,
23559 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23560 );
23561 assert_eq!(
23562 editor
23563 .selections
23564 .all::<Point>(cx)
23565 .into_iter()
23566 .map(|s| s.range())
23567 .collect::<Vec<_>>(),
23568 expected_ranges,
23569 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23570 );
23571 })
23572 });
23573 pane_2.update(cx, |pane, cx| {
23574 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23575 open_editor.update(cx, |editor, cx| {
23576 assert_eq!(
23577 editor.display_text(cx),
23578 r#"fn main() {
23579⋯rintln!("1");
23580⋯intln!("2");
23581⋯ntln!("3");
23582println!("4");
23583println!("5");
23584}"#,
23585 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23586 );
23587 assert_eq!(
23588 editor
23589 .selections
23590 .all::<Point>(cx)
23591 .into_iter()
23592 .map(|s| s.range())
23593 .collect::<Vec<_>>(),
23594 vec![Point::zero()..Point::zero()],
23595 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23596 );
23597 })
23598 });
23599}
23600
23601#[gpui::test]
23602async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23603 init_test(cx, |_| {});
23604
23605 let fs = FakeFs::new(cx.executor());
23606 let main_text = r#"fn main() {
23607println!("1");
23608println!("2");
23609println!("3");
23610println!("4");
23611println!("5");
23612}"#;
23613 let lib_text = "mod foo {}";
23614 fs.insert_tree(
23615 path!("/a"),
23616 json!({
23617 "lib.rs": lib_text,
23618 "main.rs": main_text,
23619 }),
23620 )
23621 .await;
23622
23623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23624 let (workspace, cx) =
23625 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23626 let worktree_id = workspace.update(cx, |workspace, cx| {
23627 workspace.project().update(cx, |project, cx| {
23628 project.worktrees(cx).next().unwrap().read(cx).id()
23629 })
23630 });
23631
23632 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23633 let editor = workspace
23634 .update_in(cx, |workspace, window, cx| {
23635 workspace.open_path(
23636 (worktree_id, "main.rs"),
23637 Some(pane.downgrade()),
23638 true,
23639 window,
23640 cx,
23641 )
23642 })
23643 .unwrap()
23644 .await
23645 .downcast::<Editor>()
23646 .unwrap();
23647 pane.update(cx, |pane, cx| {
23648 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23649 open_editor.update(cx, |editor, cx| {
23650 assert_eq!(
23651 editor.display_text(cx),
23652 main_text,
23653 "Original main.rs text on initial open",
23654 );
23655 })
23656 });
23657 editor.update_in(cx, |editor, window, cx| {
23658 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23659 });
23660
23661 cx.update_global(|store: &mut SettingsStore, cx| {
23662 store.update_user_settings(cx, |s| {
23663 s.workspace.restore_on_file_reopen = Some(false);
23664 });
23665 });
23666 editor.update_in(cx, |editor, window, cx| {
23667 editor.fold_ranges(
23668 vec![
23669 Point::new(1, 0)..Point::new(1, 1),
23670 Point::new(2, 0)..Point::new(2, 2),
23671 Point::new(3, 0)..Point::new(3, 3),
23672 ],
23673 false,
23674 window,
23675 cx,
23676 );
23677 });
23678 pane.update_in(cx, |pane, window, cx| {
23679 pane.close_all_items(&CloseAllItems::default(), window, cx)
23680 })
23681 .await
23682 .unwrap();
23683 pane.update(cx, |pane, _| {
23684 assert!(pane.active_item().is_none());
23685 });
23686 cx.update_global(|store: &mut SettingsStore, cx| {
23687 store.update_user_settings(cx, |s| {
23688 s.workspace.restore_on_file_reopen = Some(true);
23689 });
23690 });
23691
23692 let _editor_reopened = workspace
23693 .update_in(cx, |workspace, window, cx| {
23694 workspace.open_path(
23695 (worktree_id, "main.rs"),
23696 Some(pane.downgrade()),
23697 true,
23698 window,
23699 cx,
23700 )
23701 })
23702 .unwrap()
23703 .await
23704 .downcast::<Editor>()
23705 .unwrap();
23706 pane.update(cx, |pane, cx| {
23707 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23708 open_editor.update(cx, |editor, cx| {
23709 assert_eq!(
23710 editor.display_text(cx),
23711 main_text,
23712 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23713 );
23714 })
23715 });
23716}
23717
23718#[gpui::test]
23719async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23720 struct EmptyModalView {
23721 focus_handle: gpui::FocusHandle,
23722 }
23723 impl EventEmitter<DismissEvent> for EmptyModalView {}
23724 impl Render for EmptyModalView {
23725 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23726 div()
23727 }
23728 }
23729 impl Focusable for EmptyModalView {
23730 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23731 self.focus_handle.clone()
23732 }
23733 }
23734 impl workspace::ModalView for EmptyModalView {}
23735 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23736 EmptyModalView {
23737 focus_handle: cx.focus_handle(),
23738 }
23739 }
23740
23741 init_test(cx, |_| {});
23742
23743 let fs = FakeFs::new(cx.executor());
23744 let project = Project::test(fs, [], cx).await;
23745 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23746 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23747 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23748 let editor = cx.new_window_entity(|window, cx| {
23749 Editor::new(
23750 EditorMode::full(),
23751 buffer,
23752 Some(project.clone()),
23753 window,
23754 cx,
23755 )
23756 });
23757 workspace
23758 .update(cx, |workspace, window, cx| {
23759 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23760 })
23761 .unwrap();
23762 editor.update_in(cx, |editor, window, cx| {
23763 editor.open_context_menu(&OpenContextMenu, window, cx);
23764 assert!(editor.mouse_context_menu.is_some());
23765 });
23766 workspace
23767 .update(cx, |workspace, window, cx| {
23768 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23769 })
23770 .unwrap();
23771 cx.read(|cx| {
23772 assert!(editor.read(cx).mouse_context_menu.is_none());
23773 });
23774}
23775
23776#[gpui::test]
23777async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23778 init_test(cx, |_| {});
23779
23780 let fs = FakeFs::new(cx.executor());
23781 fs.insert_file(path!("/file.html"), Default::default())
23782 .await;
23783
23784 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23785
23786 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23787 let html_language = Arc::new(Language::new(
23788 LanguageConfig {
23789 name: "HTML".into(),
23790 matcher: LanguageMatcher {
23791 path_suffixes: vec!["html".to_string()],
23792 ..LanguageMatcher::default()
23793 },
23794 brackets: BracketPairConfig {
23795 pairs: vec![BracketPair {
23796 start: "<".into(),
23797 end: ">".into(),
23798 close: true,
23799 ..Default::default()
23800 }],
23801 ..Default::default()
23802 },
23803 ..Default::default()
23804 },
23805 Some(tree_sitter_html::LANGUAGE.into()),
23806 ));
23807 language_registry.add(html_language);
23808 let mut fake_servers = language_registry.register_fake_lsp(
23809 "HTML",
23810 FakeLspAdapter {
23811 capabilities: lsp::ServerCapabilities {
23812 completion_provider: Some(lsp::CompletionOptions {
23813 resolve_provider: Some(true),
23814 ..Default::default()
23815 }),
23816 ..Default::default()
23817 },
23818 ..Default::default()
23819 },
23820 );
23821
23822 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23823 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23824
23825 let worktree_id = workspace
23826 .update(cx, |workspace, _window, cx| {
23827 workspace.project().update(cx, |project, cx| {
23828 project.worktrees(cx).next().unwrap().read(cx).id()
23829 })
23830 })
23831 .unwrap();
23832 project
23833 .update(cx, |project, cx| {
23834 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23835 })
23836 .await
23837 .unwrap();
23838 let editor = workspace
23839 .update(cx, |workspace, window, cx| {
23840 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23841 })
23842 .unwrap()
23843 .await
23844 .unwrap()
23845 .downcast::<Editor>()
23846 .unwrap();
23847
23848 let fake_server = fake_servers.next().await.unwrap();
23849 editor.update_in(cx, |editor, window, cx| {
23850 editor.set_text("<ad></ad>", window, cx);
23851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23852 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23853 });
23854 let Some((buffer, _)) = editor
23855 .buffer
23856 .read(cx)
23857 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23858 else {
23859 panic!("Failed to get buffer for selection position");
23860 };
23861 let buffer = buffer.read(cx);
23862 let buffer_id = buffer.remote_id();
23863 let opening_range =
23864 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23865 let closing_range =
23866 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23867 let mut linked_ranges = HashMap::default();
23868 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23869 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23870 });
23871 let mut completion_handle =
23872 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23873 Ok(Some(lsp::CompletionResponse::Array(vec![
23874 lsp::CompletionItem {
23875 label: "head".to_string(),
23876 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23877 lsp::InsertReplaceEdit {
23878 new_text: "head".to_string(),
23879 insert: lsp::Range::new(
23880 lsp::Position::new(0, 1),
23881 lsp::Position::new(0, 3),
23882 ),
23883 replace: lsp::Range::new(
23884 lsp::Position::new(0, 1),
23885 lsp::Position::new(0, 3),
23886 ),
23887 },
23888 )),
23889 ..Default::default()
23890 },
23891 ])))
23892 });
23893 editor.update_in(cx, |editor, window, cx| {
23894 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23895 });
23896 cx.run_until_parked();
23897 completion_handle.next().await.unwrap();
23898 editor.update(cx, |editor, _| {
23899 assert!(
23900 editor.context_menu_visible(),
23901 "Completion menu should be visible"
23902 );
23903 });
23904 editor.update_in(cx, |editor, window, cx| {
23905 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23906 });
23907 cx.executor().run_until_parked();
23908 editor.update(cx, |editor, cx| {
23909 assert_eq!(editor.text(cx), "<head></head>");
23910 });
23911}
23912
23913#[gpui::test]
23914async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23915 init_test(cx, |_| {});
23916
23917 let fs = FakeFs::new(cx.executor());
23918 fs.insert_tree(
23919 path!("/root"),
23920 json!({
23921 "a": {
23922 "main.rs": "fn main() {}",
23923 },
23924 "foo": {
23925 "bar": {
23926 "external_file.rs": "pub mod external {}",
23927 }
23928 }
23929 }),
23930 )
23931 .await;
23932
23933 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23935 language_registry.add(rust_lang());
23936 let _fake_servers = language_registry.register_fake_lsp(
23937 "Rust",
23938 FakeLspAdapter {
23939 ..FakeLspAdapter::default()
23940 },
23941 );
23942 let (workspace, cx) =
23943 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23944 let worktree_id = workspace.update(cx, |workspace, cx| {
23945 workspace.project().update(cx, |project, cx| {
23946 project.worktrees(cx).next().unwrap().read(cx).id()
23947 })
23948 });
23949
23950 let assert_language_servers_count =
23951 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23952 project.update(cx, |project, cx| {
23953 let current = project
23954 .lsp_store()
23955 .read(cx)
23956 .as_local()
23957 .unwrap()
23958 .language_servers
23959 .len();
23960 assert_eq!(expected, current, "{context}");
23961 });
23962 };
23963
23964 assert_language_servers_count(
23965 0,
23966 "No servers should be running before any file is open",
23967 cx,
23968 );
23969 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23970 let main_editor = workspace
23971 .update_in(cx, |workspace, window, cx| {
23972 workspace.open_path(
23973 (worktree_id, "main.rs"),
23974 Some(pane.downgrade()),
23975 true,
23976 window,
23977 cx,
23978 )
23979 })
23980 .unwrap()
23981 .await
23982 .downcast::<Editor>()
23983 .unwrap();
23984 pane.update(cx, |pane, cx| {
23985 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23986 open_editor.update(cx, |editor, cx| {
23987 assert_eq!(
23988 editor.display_text(cx),
23989 "fn main() {}",
23990 "Original main.rs text on initial open",
23991 );
23992 });
23993 assert_eq!(open_editor, main_editor);
23994 });
23995 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23996
23997 let external_editor = workspace
23998 .update_in(cx, |workspace, window, cx| {
23999 workspace.open_abs_path(
24000 PathBuf::from("/root/foo/bar/external_file.rs"),
24001 OpenOptions::default(),
24002 window,
24003 cx,
24004 )
24005 })
24006 .await
24007 .expect("opening external file")
24008 .downcast::<Editor>()
24009 .expect("downcasted external file's open element to editor");
24010 pane.update(cx, |pane, cx| {
24011 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24012 open_editor.update(cx, |editor, cx| {
24013 assert_eq!(
24014 editor.display_text(cx),
24015 "pub mod external {}",
24016 "External file is open now",
24017 );
24018 });
24019 assert_eq!(open_editor, external_editor);
24020 });
24021 assert_language_servers_count(
24022 1,
24023 "Second, external, *.rs file should join the existing server",
24024 cx,
24025 );
24026
24027 pane.update_in(cx, |pane, window, cx| {
24028 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24029 })
24030 .await
24031 .unwrap();
24032 pane.update_in(cx, |pane, window, cx| {
24033 pane.navigate_backward(&Default::default(), window, cx);
24034 });
24035 cx.run_until_parked();
24036 pane.update(cx, |pane, cx| {
24037 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24038 open_editor.update(cx, |editor, cx| {
24039 assert_eq!(
24040 editor.display_text(cx),
24041 "pub mod external {}",
24042 "External file is open now",
24043 );
24044 });
24045 });
24046 assert_language_servers_count(
24047 1,
24048 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24049 cx,
24050 );
24051
24052 cx.update(|_, cx| {
24053 workspace::reload(cx);
24054 });
24055 assert_language_servers_count(
24056 1,
24057 "After reloading the worktree with local and external files opened, only one project should be started",
24058 cx,
24059 );
24060}
24061
24062#[gpui::test]
24063async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24064 init_test(cx, |_| {});
24065
24066 let mut cx = EditorTestContext::new(cx).await;
24067 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24068 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24069
24070 // test cursor move to start of each line on tab
24071 // for `if`, `elif`, `else`, `while`, `with` and `for`
24072 cx.set_state(indoc! {"
24073 def main():
24074 ˇ for item in items:
24075 ˇ while item.active:
24076 ˇ if item.value > 10:
24077 ˇ continue
24078 ˇ elif item.value < 0:
24079 ˇ break
24080 ˇ else:
24081 ˇ with item.context() as ctx:
24082 ˇ yield count
24083 ˇ else:
24084 ˇ log('while else')
24085 ˇ else:
24086 ˇ log('for else')
24087 "});
24088 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24089 cx.assert_editor_state(indoc! {"
24090 def main():
24091 ˇfor item in items:
24092 ˇwhile item.active:
24093 ˇif item.value > 10:
24094 ˇcontinue
24095 ˇelif item.value < 0:
24096 ˇbreak
24097 ˇelse:
24098 ˇwith item.context() as ctx:
24099 ˇyield count
24100 ˇelse:
24101 ˇlog('while else')
24102 ˇelse:
24103 ˇlog('for else')
24104 "});
24105 // test relative indent is preserved when tab
24106 // for `if`, `elif`, `else`, `while`, `with` and `for`
24107 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24108 cx.assert_editor_state(indoc! {"
24109 def main():
24110 ˇfor item in items:
24111 ˇwhile item.active:
24112 ˇif item.value > 10:
24113 ˇcontinue
24114 ˇelif item.value < 0:
24115 ˇbreak
24116 ˇelse:
24117 ˇwith item.context() as ctx:
24118 ˇyield count
24119 ˇelse:
24120 ˇlog('while else')
24121 ˇelse:
24122 ˇlog('for else')
24123 "});
24124
24125 // test cursor move to start of each line on tab
24126 // for `try`, `except`, `else`, `finally`, `match` and `def`
24127 cx.set_state(indoc! {"
24128 def main():
24129 ˇ try:
24130 ˇ fetch()
24131 ˇ except ValueError:
24132 ˇ handle_error()
24133 ˇ else:
24134 ˇ match value:
24135 ˇ case _:
24136 ˇ finally:
24137 ˇ def status():
24138 ˇ return 0
24139 "});
24140 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24141 cx.assert_editor_state(indoc! {"
24142 def main():
24143 ˇtry:
24144 ˇfetch()
24145 ˇexcept ValueError:
24146 ˇhandle_error()
24147 ˇelse:
24148 ˇmatch value:
24149 ˇcase _:
24150 ˇfinally:
24151 ˇdef status():
24152 ˇreturn 0
24153 "});
24154 // test relative indent is preserved when tab
24155 // for `try`, `except`, `else`, `finally`, `match` and `def`
24156 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24157 cx.assert_editor_state(indoc! {"
24158 def main():
24159 ˇtry:
24160 ˇfetch()
24161 ˇexcept ValueError:
24162 ˇhandle_error()
24163 ˇelse:
24164 ˇmatch value:
24165 ˇcase _:
24166 ˇfinally:
24167 ˇdef status():
24168 ˇreturn 0
24169 "});
24170}
24171
24172#[gpui::test]
24173async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24174 init_test(cx, |_| {});
24175
24176 let mut cx = EditorTestContext::new(cx).await;
24177 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24178 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24179
24180 // test `else` auto outdents when typed inside `if` block
24181 cx.set_state(indoc! {"
24182 def main():
24183 if i == 2:
24184 return
24185 ˇ
24186 "});
24187 cx.update_editor(|editor, window, cx| {
24188 editor.handle_input("else:", window, cx);
24189 });
24190 cx.assert_editor_state(indoc! {"
24191 def main():
24192 if i == 2:
24193 return
24194 else:ˇ
24195 "});
24196
24197 // test `except` auto outdents when typed inside `try` block
24198 cx.set_state(indoc! {"
24199 def main():
24200 try:
24201 i = 2
24202 ˇ
24203 "});
24204 cx.update_editor(|editor, window, cx| {
24205 editor.handle_input("except:", window, cx);
24206 });
24207 cx.assert_editor_state(indoc! {"
24208 def main():
24209 try:
24210 i = 2
24211 except:ˇ
24212 "});
24213
24214 // test `else` auto outdents when typed inside `except` block
24215 cx.set_state(indoc! {"
24216 def main():
24217 try:
24218 i = 2
24219 except:
24220 j = 2
24221 ˇ
24222 "});
24223 cx.update_editor(|editor, window, cx| {
24224 editor.handle_input("else:", window, cx);
24225 });
24226 cx.assert_editor_state(indoc! {"
24227 def main():
24228 try:
24229 i = 2
24230 except:
24231 j = 2
24232 else:ˇ
24233 "});
24234
24235 // test `finally` auto outdents when typed inside `else` block
24236 cx.set_state(indoc! {"
24237 def main():
24238 try:
24239 i = 2
24240 except:
24241 j = 2
24242 else:
24243 k = 2
24244 ˇ
24245 "});
24246 cx.update_editor(|editor, window, cx| {
24247 editor.handle_input("finally:", window, cx);
24248 });
24249 cx.assert_editor_state(indoc! {"
24250 def main():
24251 try:
24252 i = 2
24253 except:
24254 j = 2
24255 else:
24256 k = 2
24257 finally:ˇ
24258 "});
24259
24260 // test `else` does not outdents when typed inside `except` block right after for block
24261 cx.set_state(indoc! {"
24262 def main():
24263 try:
24264 i = 2
24265 except:
24266 for i in range(n):
24267 pass
24268 ˇ
24269 "});
24270 cx.update_editor(|editor, window, cx| {
24271 editor.handle_input("else:", window, cx);
24272 });
24273 cx.assert_editor_state(indoc! {"
24274 def main():
24275 try:
24276 i = 2
24277 except:
24278 for i in range(n):
24279 pass
24280 else:ˇ
24281 "});
24282
24283 // test `finally` auto outdents when typed inside `else` block right after for block
24284 cx.set_state(indoc! {"
24285 def main():
24286 try:
24287 i = 2
24288 except:
24289 j = 2
24290 else:
24291 for i in range(n):
24292 pass
24293 ˇ
24294 "});
24295 cx.update_editor(|editor, window, cx| {
24296 editor.handle_input("finally:", window, cx);
24297 });
24298 cx.assert_editor_state(indoc! {"
24299 def main():
24300 try:
24301 i = 2
24302 except:
24303 j = 2
24304 else:
24305 for i in range(n):
24306 pass
24307 finally:ˇ
24308 "});
24309
24310 // test `except` outdents to inner "try" block
24311 cx.set_state(indoc! {"
24312 def main():
24313 try:
24314 i = 2
24315 if i == 2:
24316 try:
24317 i = 3
24318 ˇ
24319 "});
24320 cx.update_editor(|editor, window, cx| {
24321 editor.handle_input("except:", window, cx);
24322 });
24323 cx.assert_editor_state(indoc! {"
24324 def main():
24325 try:
24326 i = 2
24327 if i == 2:
24328 try:
24329 i = 3
24330 except:ˇ
24331 "});
24332
24333 // test `except` outdents to outer "try" block
24334 cx.set_state(indoc! {"
24335 def main():
24336 try:
24337 i = 2
24338 if i == 2:
24339 try:
24340 i = 3
24341 ˇ
24342 "});
24343 cx.update_editor(|editor, window, cx| {
24344 editor.handle_input("except:", window, cx);
24345 });
24346 cx.assert_editor_state(indoc! {"
24347 def main():
24348 try:
24349 i = 2
24350 if i == 2:
24351 try:
24352 i = 3
24353 except:ˇ
24354 "});
24355
24356 // test `else` stays at correct indent when typed after `for` block
24357 cx.set_state(indoc! {"
24358 def main():
24359 for i in range(10):
24360 if i == 3:
24361 break
24362 ˇ
24363 "});
24364 cx.update_editor(|editor, window, cx| {
24365 editor.handle_input("else:", window, cx);
24366 });
24367 cx.assert_editor_state(indoc! {"
24368 def main():
24369 for i in range(10):
24370 if i == 3:
24371 break
24372 else:ˇ
24373 "});
24374
24375 // test does not outdent on typing after line with square brackets
24376 cx.set_state(indoc! {"
24377 def f() -> list[str]:
24378 ˇ
24379 "});
24380 cx.update_editor(|editor, window, cx| {
24381 editor.handle_input("a", window, cx);
24382 });
24383 cx.assert_editor_state(indoc! {"
24384 def f() -> list[str]:
24385 aˇ
24386 "});
24387
24388 // test does not outdent on typing : after case keyword
24389 cx.set_state(indoc! {"
24390 match 1:
24391 caseˇ
24392 "});
24393 cx.update_editor(|editor, window, cx| {
24394 editor.handle_input(":", window, cx);
24395 });
24396 cx.assert_editor_state(indoc! {"
24397 match 1:
24398 case:ˇ
24399 "});
24400}
24401
24402#[gpui::test]
24403async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24404 init_test(cx, |_| {});
24405 update_test_language_settings(cx, |settings| {
24406 settings.defaults.extend_comment_on_newline = Some(false);
24407 });
24408 let mut cx = EditorTestContext::new(cx).await;
24409 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24410 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24411
24412 // test correct indent after newline on comment
24413 cx.set_state(indoc! {"
24414 # COMMENT:ˇ
24415 "});
24416 cx.update_editor(|editor, window, cx| {
24417 editor.newline(&Newline, window, cx);
24418 });
24419 cx.assert_editor_state(indoc! {"
24420 # COMMENT:
24421 ˇ
24422 "});
24423
24424 // test correct indent after newline in brackets
24425 cx.set_state(indoc! {"
24426 {ˇ}
24427 "});
24428 cx.update_editor(|editor, window, cx| {
24429 editor.newline(&Newline, window, cx);
24430 });
24431 cx.run_until_parked();
24432 cx.assert_editor_state(indoc! {"
24433 {
24434 ˇ
24435 }
24436 "});
24437
24438 cx.set_state(indoc! {"
24439 (ˇ)
24440 "});
24441 cx.update_editor(|editor, window, cx| {
24442 editor.newline(&Newline, window, cx);
24443 });
24444 cx.run_until_parked();
24445 cx.assert_editor_state(indoc! {"
24446 (
24447 ˇ
24448 )
24449 "});
24450
24451 // do not indent after empty lists or dictionaries
24452 cx.set_state(indoc! {"
24453 a = []ˇ
24454 "});
24455 cx.update_editor(|editor, window, cx| {
24456 editor.newline(&Newline, window, cx);
24457 });
24458 cx.run_until_parked();
24459 cx.assert_editor_state(indoc! {"
24460 a = []
24461 ˇ
24462 "});
24463}
24464
24465#[gpui::test]
24466async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24467 init_test(cx, |_| {});
24468
24469 let mut cx = EditorTestContext::new(cx).await;
24470 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24471 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24472
24473 // test cursor move to start of each line on tab
24474 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24475 cx.set_state(indoc! {"
24476 function main() {
24477 ˇ for item in $items; do
24478 ˇ while [ -n \"$item\" ]; do
24479 ˇ if [ \"$value\" -gt 10 ]; then
24480 ˇ continue
24481 ˇ elif [ \"$value\" -lt 0 ]; then
24482 ˇ break
24483 ˇ else
24484 ˇ echo \"$item\"
24485 ˇ fi
24486 ˇ done
24487 ˇ done
24488 ˇ}
24489 "});
24490 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24491 cx.assert_editor_state(indoc! {"
24492 function main() {
24493 ˇfor item in $items; do
24494 ˇwhile [ -n \"$item\" ]; do
24495 ˇif [ \"$value\" -gt 10 ]; then
24496 ˇcontinue
24497 ˇelif [ \"$value\" -lt 0 ]; then
24498 ˇbreak
24499 ˇelse
24500 ˇecho \"$item\"
24501 ˇfi
24502 ˇdone
24503 ˇdone
24504 ˇ}
24505 "});
24506 // test relative indent is preserved when tab
24507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24508 cx.assert_editor_state(indoc! {"
24509 function main() {
24510 ˇfor item in $items; do
24511 ˇwhile [ -n \"$item\" ]; do
24512 ˇif [ \"$value\" -gt 10 ]; then
24513 ˇcontinue
24514 ˇelif [ \"$value\" -lt 0 ]; then
24515 ˇbreak
24516 ˇelse
24517 ˇecho \"$item\"
24518 ˇfi
24519 ˇdone
24520 ˇdone
24521 ˇ}
24522 "});
24523
24524 // test cursor move to start of each line on tab
24525 // for `case` statement with patterns
24526 cx.set_state(indoc! {"
24527 function handle() {
24528 ˇ case \"$1\" in
24529 ˇ start)
24530 ˇ echo \"a\"
24531 ˇ ;;
24532 ˇ stop)
24533 ˇ echo \"b\"
24534 ˇ ;;
24535 ˇ *)
24536 ˇ echo \"c\"
24537 ˇ ;;
24538 ˇ esac
24539 ˇ}
24540 "});
24541 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24542 cx.assert_editor_state(indoc! {"
24543 function handle() {
24544 ˇcase \"$1\" in
24545 ˇstart)
24546 ˇecho \"a\"
24547 ˇ;;
24548 ˇstop)
24549 ˇecho \"b\"
24550 ˇ;;
24551 ˇ*)
24552 ˇecho \"c\"
24553 ˇ;;
24554 ˇesac
24555 ˇ}
24556 "});
24557}
24558
24559#[gpui::test]
24560async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24561 init_test(cx, |_| {});
24562
24563 let mut cx = EditorTestContext::new(cx).await;
24564 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24565 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24566
24567 // test indents on comment insert
24568 cx.set_state(indoc! {"
24569 function main() {
24570 ˇ for item in $items; do
24571 ˇ while [ -n \"$item\" ]; do
24572 ˇ if [ \"$value\" -gt 10 ]; then
24573 ˇ continue
24574 ˇ elif [ \"$value\" -lt 0 ]; then
24575 ˇ break
24576 ˇ else
24577 ˇ echo \"$item\"
24578 ˇ fi
24579 ˇ done
24580 ˇ done
24581 ˇ}
24582 "});
24583 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24584 cx.assert_editor_state(indoc! {"
24585 function main() {
24586 #ˇ for item in $items; do
24587 #ˇ while [ -n \"$item\" ]; do
24588 #ˇ if [ \"$value\" -gt 10 ]; then
24589 #ˇ continue
24590 #ˇ elif [ \"$value\" -lt 0 ]; then
24591 #ˇ break
24592 #ˇ else
24593 #ˇ echo \"$item\"
24594 #ˇ fi
24595 #ˇ done
24596 #ˇ done
24597 #ˇ}
24598 "});
24599}
24600
24601#[gpui::test]
24602async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24603 init_test(cx, |_| {});
24604
24605 let mut cx = EditorTestContext::new(cx).await;
24606 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24607 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24608
24609 // test `else` auto outdents when typed inside `if` block
24610 cx.set_state(indoc! {"
24611 if [ \"$1\" = \"test\" ]; then
24612 echo \"foo bar\"
24613 ˇ
24614 "});
24615 cx.update_editor(|editor, window, cx| {
24616 editor.handle_input("else", window, cx);
24617 });
24618 cx.assert_editor_state(indoc! {"
24619 if [ \"$1\" = \"test\" ]; then
24620 echo \"foo bar\"
24621 elseˇ
24622 "});
24623
24624 // test `elif` auto outdents when typed inside `if` block
24625 cx.set_state(indoc! {"
24626 if [ \"$1\" = \"test\" ]; then
24627 echo \"foo bar\"
24628 ˇ
24629 "});
24630 cx.update_editor(|editor, window, cx| {
24631 editor.handle_input("elif", window, cx);
24632 });
24633 cx.assert_editor_state(indoc! {"
24634 if [ \"$1\" = \"test\" ]; then
24635 echo \"foo bar\"
24636 elifˇ
24637 "});
24638
24639 // test `fi` auto outdents when typed inside `else` block
24640 cx.set_state(indoc! {"
24641 if [ \"$1\" = \"test\" ]; then
24642 echo \"foo bar\"
24643 else
24644 echo \"bar baz\"
24645 ˇ
24646 "});
24647 cx.update_editor(|editor, window, cx| {
24648 editor.handle_input("fi", window, cx);
24649 });
24650 cx.assert_editor_state(indoc! {"
24651 if [ \"$1\" = \"test\" ]; then
24652 echo \"foo bar\"
24653 else
24654 echo \"bar baz\"
24655 fiˇ
24656 "});
24657
24658 // test `done` auto outdents when typed inside `while` block
24659 cx.set_state(indoc! {"
24660 while read line; do
24661 echo \"$line\"
24662 ˇ
24663 "});
24664 cx.update_editor(|editor, window, cx| {
24665 editor.handle_input("done", window, cx);
24666 });
24667 cx.assert_editor_state(indoc! {"
24668 while read line; do
24669 echo \"$line\"
24670 doneˇ
24671 "});
24672
24673 // test `done` auto outdents when typed inside `for` block
24674 cx.set_state(indoc! {"
24675 for file in *.txt; do
24676 cat \"$file\"
24677 ˇ
24678 "});
24679 cx.update_editor(|editor, window, cx| {
24680 editor.handle_input("done", window, cx);
24681 });
24682 cx.assert_editor_state(indoc! {"
24683 for file in *.txt; do
24684 cat \"$file\"
24685 doneˇ
24686 "});
24687
24688 // test `esac` auto outdents when typed inside `case` block
24689 cx.set_state(indoc! {"
24690 case \"$1\" in
24691 start)
24692 echo \"foo bar\"
24693 ;;
24694 stop)
24695 echo \"bar baz\"
24696 ;;
24697 ˇ
24698 "});
24699 cx.update_editor(|editor, window, cx| {
24700 editor.handle_input("esac", window, cx);
24701 });
24702 cx.assert_editor_state(indoc! {"
24703 case \"$1\" in
24704 start)
24705 echo \"foo bar\"
24706 ;;
24707 stop)
24708 echo \"bar baz\"
24709 ;;
24710 esacˇ
24711 "});
24712
24713 // test `*)` auto outdents when typed inside `case` block
24714 cx.set_state(indoc! {"
24715 case \"$1\" in
24716 start)
24717 echo \"foo bar\"
24718 ;;
24719 ˇ
24720 "});
24721 cx.update_editor(|editor, window, cx| {
24722 editor.handle_input("*)", window, cx);
24723 });
24724 cx.assert_editor_state(indoc! {"
24725 case \"$1\" in
24726 start)
24727 echo \"foo bar\"
24728 ;;
24729 *)ˇ
24730 "});
24731
24732 // test `fi` outdents to correct level with nested if blocks
24733 cx.set_state(indoc! {"
24734 if [ \"$1\" = \"test\" ]; then
24735 echo \"outer if\"
24736 if [ \"$2\" = \"debug\" ]; then
24737 echo \"inner if\"
24738 ˇ
24739 "});
24740 cx.update_editor(|editor, window, cx| {
24741 editor.handle_input("fi", window, cx);
24742 });
24743 cx.assert_editor_state(indoc! {"
24744 if [ \"$1\" = \"test\" ]; then
24745 echo \"outer if\"
24746 if [ \"$2\" = \"debug\" ]; then
24747 echo \"inner if\"
24748 fiˇ
24749 "});
24750}
24751
24752#[gpui::test]
24753async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24754 init_test(cx, |_| {});
24755 update_test_language_settings(cx, |settings| {
24756 settings.defaults.extend_comment_on_newline = Some(false);
24757 });
24758 let mut cx = EditorTestContext::new(cx).await;
24759 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24761
24762 // test correct indent after newline on comment
24763 cx.set_state(indoc! {"
24764 # COMMENT:ˇ
24765 "});
24766 cx.update_editor(|editor, window, cx| {
24767 editor.newline(&Newline, window, cx);
24768 });
24769 cx.assert_editor_state(indoc! {"
24770 # COMMENT:
24771 ˇ
24772 "});
24773
24774 // test correct indent after newline after `then`
24775 cx.set_state(indoc! {"
24776
24777 if [ \"$1\" = \"test\" ]; thenˇ
24778 "});
24779 cx.update_editor(|editor, window, cx| {
24780 editor.newline(&Newline, window, cx);
24781 });
24782 cx.run_until_parked();
24783 cx.assert_editor_state(indoc! {"
24784
24785 if [ \"$1\" = \"test\" ]; then
24786 ˇ
24787 "});
24788
24789 // test correct indent after newline after `else`
24790 cx.set_state(indoc! {"
24791 if [ \"$1\" = \"test\" ]; then
24792 elseˇ
24793 "});
24794 cx.update_editor(|editor, window, cx| {
24795 editor.newline(&Newline, window, cx);
24796 });
24797 cx.run_until_parked();
24798 cx.assert_editor_state(indoc! {"
24799 if [ \"$1\" = \"test\" ]; then
24800 else
24801 ˇ
24802 "});
24803
24804 // test correct indent after newline after `elif`
24805 cx.set_state(indoc! {"
24806 if [ \"$1\" = \"test\" ]; then
24807 elifˇ
24808 "});
24809 cx.update_editor(|editor, window, cx| {
24810 editor.newline(&Newline, window, cx);
24811 });
24812 cx.run_until_parked();
24813 cx.assert_editor_state(indoc! {"
24814 if [ \"$1\" = \"test\" ]; then
24815 elif
24816 ˇ
24817 "});
24818
24819 // test correct indent after newline after `do`
24820 cx.set_state(indoc! {"
24821 for file in *.txt; doˇ
24822 "});
24823 cx.update_editor(|editor, window, cx| {
24824 editor.newline(&Newline, window, cx);
24825 });
24826 cx.run_until_parked();
24827 cx.assert_editor_state(indoc! {"
24828 for file in *.txt; do
24829 ˇ
24830 "});
24831
24832 // test correct indent after newline after case pattern
24833 cx.set_state(indoc! {"
24834 case \"$1\" in
24835 start)ˇ
24836 "});
24837 cx.update_editor(|editor, window, cx| {
24838 editor.newline(&Newline, window, cx);
24839 });
24840 cx.run_until_parked();
24841 cx.assert_editor_state(indoc! {"
24842 case \"$1\" in
24843 start)
24844 ˇ
24845 "});
24846
24847 // test correct indent after newline after case pattern
24848 cx.set_state(indoc! {"
24849 case \"$1\" in
24850 start)
24851 ;;
24852 *)ˇ
24853 "});
24854 cx.update_editor(|editor, window, cx| {
24855 editor.newline(&Newline, window, cx);
24856 });
24857 cx.run_until_parked();
24858 cx.assert_editor_state(indoc! {"
24859 case \"$1\" in
24860 start)
24861 ;;
24862 *)
24863 ˇ
24864 "});
24865
24866 // test correct indent after newline after function opening brace
24867 cx.set_state(indoc! {"
24868 function test() {ˇ}
24869 "});
24870 cx.update_editor(|editor, window, cx| {
24871 editor.newline(&Newline, window, cx);
24872 });
24873 cx.run_until_parked();
24874 cx.assert_editor_state(indoc! {"
24875 function test() {
24876 ˇ
24877 }
24878 "});
24879
24880 // test no extra indent after semicolon on same line
24881 cx.set_state(indoc! {"
24882 echo \"test\";ˇ
24883 "});
24884 cx.update_editor(|editor, window, cx| {
24885 editor.newline(&Newline, window, cx);
24886 });
24887 cx.run_until_parked();
24888 cx.assert_editor_state(indoc! {"
24889 echo \"test\";
24890 ˇ
24891 "});
24892}
24893
24894fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24895 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24896 point..point
24897}
24898
24899#[track_caller]
24900fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24901 let (text, ranges) = marked_text_ranges(marked_text, true);
24902 assert_eq!(editor.text(cx), text);
24903 assert_eq!(
24904 editor.selections.ranges(cx),
24905 ranges,
24906 "Assert selections are {}",
24907 marked_text
24908 );
24909}
24910
24911pub fn handle_signature_help_request(
24912 cx: &mut EditorLspTestContext,
24913 mocked_response: lsp::SignatureHelp,
24914) -> impl Future<Output = ()> + use<> {
24915 let mut request =
24916 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24917 let mocked_response = mocked_response.clone();
24918 async move { Ok(Some(mocked_response)) }
24919 });
24920
24921 async move {
24922 request.next().await;
24923 }
24924}
24925
24926#[track_caller]
24927pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24928 cx.update_editor(|editor, _, _| {
24929 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24930 let entries = menu.entries.borrow();
24931 let entries = entries
24932 .iter()
24933 .map(|entry| entry.string.as_str())
24934 .collect::<Vec<_>>();
24935 assert_eq!(entries, expected);
24936 } else {
24937 panic!("Expected completions menu");
24938 }
24939 });
24940}
24941
24942/// Handle completion request passing a marked string specifying where the completion
24943/// should be triggered from using '|' character, what range should be replaced, and what completions
24944/// should be returned using '<' and '>' to delimit the range.
24945///
24946/// Also see `handle_completion_request_with_insert_and_replace`.
24947#[track_caller]
24948pub fn handle_completion_request(
24949 marked_string: &str,
24950 completions: Vec<&'static str>,
24951 is_incomplete: bool,
24952 counter: Arc<AtomicUsize>,
24953 cx: &mut EditorLspTestContext,
24954) -> impl Future<Output = ()> {
24955 let complete_from_marker: TextRangeMarker = '|'.into();
24956 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24957 let (_, mut marked_ranges) = marked_text_ranges_by(
24958 marked_string,
24959 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24960 );
24961
24962 let complete_from_position =
24963 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24964 let replace_range =
24965 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24966
24967 let mut request =
24968 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24969 let completions = completions.clone();
24970 counter.fetch_add(1, atomic::Ordering::Release);
24971 async move {
24972 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24973 assert_eq!(
24974 params.text_document_position.position,
24975 complete_from_position
24976 );
24977 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24978 is_incomplete,
24979 item_defaults: None,
24980 items: completions
24981 .iter()
24982 .map(|completion_text| lsp::CompletionItem {
24983 label: completion_text.to_string(),
24984 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24985 range: replace_range,
24986 new_text: completion_text.to_string(),
24987 })),
24988 ..Default::default()
24989 })
24990 .collect(),
24991 })))
24992 }
24993 });
24994
24995 async move {
24996 request.next().await;
24997 }
24998}
24999
25000/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25001/// given instead, which also contains an `insert` range.
25002///
25003/// This function uses markers to define ranges:
25004/// - `|` marks the cursor position
25005/// - `<>` marks the replace range
25006/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25007pub fn handle_completion_request_with_insert_and_replace(
25008 cx: &mut EditorLspTestContext,
25009 marked_string: &str,
25010 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25011 counter: Arc<AtomicUsize>,
25012) -> impl Future<Output = ()> {
25013 let complete_from_marker: TextRangeMarker = '|'.into();
25014 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25015 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25016
25017 let (_, mut marked_ranges) = marked_text_ranges_by(
25018 marked_string,
25019 vec![
25020 complete_from_marker.clone(),
25021 replace_range_marker.clone(),
25022 insert_range_marker.clone(),
25023 ],
25024 );
25025
25026 let complete_from_position =
25027 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25028 let replace_range =
25029 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25030
25031 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25032 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25033 _ => lsp::Range {
25034 start: replace_range.start,
25035 end: complete_from_position,
25036 },
25037 };
25038
25039 let mut request =
25040 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25041 let completions = completions.clone();
25042 counter.fetch_add(1, atomic::Ordering::Release);
25043 async move {
25044 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25045 assert_eq!(
25046 params.text_document_position.position, complete_from_position,
25047 "marker `|` position doesn't match",
25048 );
25049 Ok(Some(lsp::CompletionResponse::Array(
25050 completions
25051 .iter()
25052 .map(|(label, new_text)| lsp::CompletionItem {
25053 label: label.to_string(),
25054 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25055 lsp::InsertReplaceEdit {
25056 insert: insert_range,
25057 replace: replace_range,
25058 new_text: new_text.to_string(),
25059 },
25060 )),
25061 ..Default::default()
25062 })
25063 .collect(),
25064 )))
25065 }
25066 });
25067
25068 async move {
25069 request.next().await;
25070 }
25071}
25072
25073fn handle_resolve_completion_request(
25074 cx: &mut EditorLspTestContext,
25075 edits: Option<Vec<(&'static str, &'static str)>>,
25076) -> impl Future<Output = ()> {
25077 let edits = edits.map(|edits| {
25078 edits
25079 .iter()
25080 .map(|(marked_string, new_text)| {
25081 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25082 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25083 lsp::TextEdit::new(replace_range, new_text.to_string())
25084 })
25085 .collect::<Vec<_>>()
25086 });
25087
25088 let mut request =
25089 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25090 let edits = edits.clone();
25091 async move {
25092 Ok(lsp::CompletionItem {
25093 additional_text_edits: edits,
25094 ..Default::default()
25095 })
25096 }
25097 });
25098
25099 async move {
25100 request.next().await;
25101 }
25102}
25103
25104pub(crate) fn update_test_language_settings(
25105 cx: &mut TestAppContext,
25106 f: impl Fn(&mut AllLanguageSettingsContent),
25107) {
25108 cx.update(|cx| {
25109 SettingsStore::update_global(cx, |store, cx| {
25110 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25111 });
25112 });
25113}
25114
25115pub(crate) fn update_test_project_settings(
25116 cx: &mut TestAppContext,
25117 f: impl Fn(&mut ProjectSettingsContent),
25118) {
25119 cx.update(|cx| {
25120 SettingsStore::update_global(cx, |store, cx| {
25121 store.update_user_settings(cx, |settings| f(&mut settings.project));
25122 });
25123 });
25124}
25125
25126pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25127 cx.update(|cx| {
25128 assets::Assets.load_test_fonts(cx);
25129 let store = SettingsStore::test(cx);
25130 cx.set_global(store);
25131 theme::init(theme::LoadThemes::JustBase, cx);
25132 release_channel::init(SemanticVersion::default(), cx);
25133 client::init_settings(cx);
25134 language::init(cx);
25135 Project::init_settings(cx);
25136 workspace::init_settings(cx);
25137 crate::init(cx);
25138 });
25139 zlog::init_test();
25140 update_test_language_settings(cx, f);
25141}
25142
25143#[track_caller]
25144fn assert_hunk_revert(
25145 not_reverted_text_with_selections: &str,
25146 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25147 expected_reverted_text_with_selections: &str,
25148 base_text: &str,
25149 cx: &mut EditorLspTestContext,
25150) {
25151 cx.set_state(not_reverted_text_with_selections);
25152 cx.set_head_text(base_text);
25153 cx.executor().run_until_parked();
25154
25155 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25156 let snapshot = editor.snapshot(window, cx);
25157 let reverted_hunk_statuses = snapshot
25158 .buffer_snapshot
25159 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25160 .map(|hunk| hunk.status().kind)
25161 .collect::<Vec<_>>();
25162
25163 editor.git_restore(&Default::default(), window, cx);
25164 reverted_hunk_statuses
25165 });
25166 cx.executor().run_until_parked();
25167 cx.assert_editor_state(expected_reverted_text_with_selections);
25168 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25169}
25170
25171#[gpui::test(iterations = 10)]
25172async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25173 init_test(cx, |_| {});
25174
25175 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25176 let counter = diagnostic_requests.clone();
25177
25178 let fs = FakeFs::new(cx.executor());
25179 fs.insert_tree(
25180 path!("/a"),
25181 json!({
25182 "first.rs": "fn main() { let a = 5; }",
25183 "second.rs": "// Test file",
25184 }),
25185 )
25186 .await;
25187
25188 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25189 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25190 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25191
25192 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25193 language_registry.add(rust_lang());
25194 let mut fake_servers = language_registry.register_fake_lsp(
25195 "Rust",
25196 FakeLspAdapter {
25197 capabilities: lsp::ServerCapabilities {
25198 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25199 lsp::DiagnosticOptions {
25200 identifier: None,
25201 inter_file_dependencies: true,
25202 workspace_diagnostics: true,
25203 work_done_progress_options: Default::default(),
25204 },
25205 )),
25206 ..Default::default()
25207 },
25208 ..Default::default()
25209 },
25210 );
25211
25212 let editor = workspace
25213 .update(cx, |workspace, window, cx| {
25214 workspace.open_abs_path(
25215 PathBuf::from(path!("/a/first.rs")),
25216 OpenOptions::default(),
25217 window,
25218 cx,
25219 )
25220 })
25221 .unwrap()
25222 .await
25223 .unwrap()
25224 .downcast::<Editor>()
25225 .unwrap();
25226 let fake_server = fake_servers.next().await.unwrap();
25227 let server_id = fake_server.server.server_id();
25228 let mut first_request = fake_server
25229 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25230 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25231 let result_id = Some(new_result_id.to_string());
25232 assert_eq!(
25233 params.text_document.uri,
25234 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25235 );
25236 async move {
25237 Ok(lsp::DocumentDiagnosticReportResult::Report(
25238 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25239 related_documents: None,
25240 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25241 items: Vec::new(),
25242 result_id,
25243 },
25244 }),
25245 ))
25246 }
25247 });
25248
25249 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25250 project.update(cx, |project, cx| {
25251 let buffer_id = editor
25252 .read(cx)
25253 .buffer()
25254 .read(cx)
25255 .as_singleton()
25256 .expect("created a singleton buffer")
25257 .read(cx)
25258 .remote_id();
25259 let buffer_result_id = project
25260 .lsp_store()
25261 .read(cx)
25262 .result_id(server_id, buffer_id, cx);
25263 assert_eq!(expected, buffer_result_id);
25264 });
25265 };
25266
25267 ensure_result_id(None, cx);
25268 cx.executor().advance_clock(Duration::from_millis(60));
25269 cx.executor().run_until_parked();
25270 assert_eq!(
25271 diagnostic_requests.load(atomic::Ordering::Acquire),
25272 1,
25273 "Opening file should trigger diagnostic request"
25274 );
25275 first_request
25276 .next()
25277 .await
25278 .expect("should have sent the first diagnostics pull request");
25279 ensure_result_id(Some("1".to_string()), cx);
25280
25281 // Editing should trigger diagnostics
25282 editor.update_in(cx, |editor, window, cx| {
25283 editor.handle_input("2", window, cx)
25284 });
25285 cx.executor().advance_clock(Duration::from_millis(60));
25286 cx.executor().run_until_parked();
25287 assert_eq!(
25288 diagnostic_requests.load(atomic::Ordering::Acquire),
25289 2,
25290 "Editing should trigger diagnostic request"
25291 );
25292 ensure_result_id(Some("2".to_string()), cx);
25293
25294 // Moving cursor should not trigger diagnostic request
25295 editor.update_in(cx, |editor, window, cx| {
25296 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25297 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25298 });
25299 });
25300 cx.executor().advance_clock(Duration::from_millis(60));
25301 cx.executor().run_until_parked();
25302 assert_eq!(
25303 diagnostic_requests.load(atomic::Ordering::Acquire),
25304 2,
25305 "Cursor movement should not trigger diagnostic request"
25306 );
25307 ensure_result_id(Some("2".to_string()), cx);
25308 // Multiple rapid edits should be debounced
25309 for _ in 0..5 {
25310 editor.update_in(cx, |editor, window, cx| {
25311 editor.handle_input("x", window, cx)
25312 });
25313 }
25314 cx.executor().advance_clock(Duration::from_millis(60));
25315 cx.executor().run_until_parked();
25316
25317 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25318 assert!(
25319 final_requests <= 4,
25320 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25321 );
25322 ensure_result_id(Some(final_requests.to_string()), cx);
25323}
25324
25325#[gpui::test]
25326async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25327 // Regression test for issue #11671
25328 // Previously, adding a cursor after moving multiple cursors would reset
25329 // the cursor count instead of adding to the existing cursors.
25330 init_test(cx, |_| {});
25331 let mut cx = EditorTestContext::new(cx).await;
25332
25333 // Create a simple buffer with cursor at start
25334 cx.set_state(indoc! {"
25335 ˇaaaa
25336 bbbb
25337 cccc
25338 dddd
25339 eeee
25340 ffff
25341 gggg
25342 hhhh"});
25343
25344 // Add 2 cursors below (so we have 3 total)
25345 cx.update_editor(|editor, window, cx| {
25346 editor.add_selection_below(&Default::default(), window, cx);
25347 editor.add_selection_below(&Default::default(), window, cx);
25348 });
25349
25350 // Verify we have 3 cursors
25351 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25352 assert_eq!(
25353 initial_count, 3,
25354 "Should have 3 cursors after adding 2 below"
25355 );
25356
25357 // Move down one line
25358 cx.update_editor(|editor, window, cx| {
25359 editor.move_down(&MoveDown, window, cx);
25360 });
25361
25362 // Add another cursor below
25363 cx.update_editor(|editor, window, cx| {
25364 editor.add_selection_below(&Default::default(), window, cx);
25365 });
25366
25367 // Should now have 4 cursors (3 original + 1 new)
25368 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25369 assert_eq!(
25370 final_count, 4,
25371 "Should have 4 cursors after moving and adding another"
25372 );
25373}
25374
25375#[gpui::test(iterations = 10)]
25376async fn test_document_colors(cx: &mut TestAppContext) {
25377 let expected_color = Rgba {
25378 r: 0.33,
25379 g: 0.33,
25380 b: 0.33,
25381 a: 0.33,
25382 };
25383
25384 init_test(cx, |_| {});
25385
25386 let fs = FakeFs::new(cx.executor());
25387 fs.insert_tree(
25388 path!("/a"),
25389 json!({
25390 "first.rs": "fn main() { let a = 5; }",
25391 }),
25392 )
25393 .await;
25394
25395 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25396 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25397 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25398
25399 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25400 language_registry.add(rust_lang());
25401 let mut fake_servers = language_registry.register_fake_lsp(
25402 "Rust",
25403 FakeLspAdapter {
25404 capabilities: lsp::ServerCapabilities {
25405 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25406 ..lsp::ServerCapabilities::default()
25407 },
25408 name: "rust-analyzer",
25409 ..FakeLspAdapter::default()
25410 },
25411 );
25412 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25413 "Rust",
25414 FakeLspAdapter {
25415 capabilities: lsp::ServerCapabilities {
25416 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25417 ..lsp::ServerCapabilities::default()
25418 },
25419 name: "not-rust-analyzer",
25420 ..FakeLspAdapter::default()
25421 },
25422 );
25423
25424 let editor = workspace
25425 .update(cx, |workspace, window, cx| {
25426 workspace.open_abs_path(
25427 PathBuf::from(path!("/a/first.rs")),
25428 OpenOptions::default(),
25429 window,
25430 cx,
25431 )
25432 })
25433 .unwrap()
25434 .await
25435 .unwrap()
25436 .downcast::<Editor>()
25437 .unwrap();
25438 let fake_language_server = fake_servers.next().await.unwrap();
25439 let fake_language_server_without_capabilities =
25440 fake_servers_without_capabilities.next().await.unwrap();
25441 let requests_made = Arc::new(AtomicUsize::new(0));
25442 let closure_requests_made = Arc::clone(&requests_made);
25443 let mut color_request_handle = fake_language_server
25444 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25445 let requests_made = Arc::clone(&closure_requests_made);
25446 async move {
25447 assert_eq!(
25448 params.text_document.uri,
25449 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25450 );
25451 requests_made.fetch_add(1, atomic::Ordering::Release);
25452 Ok(vec![
25453 lsp::ColorInformation {
25454 range: lsp::Range {
25455 start: lsp::Position {
25456 line: 0,
25457 character: 0,
25458 },
25459 end: lsp::Position {
25460 line: 0,
25461 character: 1,
25462 },
25463 },
25464 color: lsp::Color {
25465 red: 0.33,
25466 green: 0.33,
25467 blue: 0.33,
25468 alpha: 0.33,
25469 },
25470 },
25471 lsp::ColorInformation {
25472 range: lsp::Range {
25473 start: lsp::Position {
25474 line: 0,
25475 character: 0,
25476 },
25477 end: lsp::Position {
25478 line: 0,
25479 character: 1,
25480 },
25481 },
25482 color: lsp::Color {
25483 red: 0.33,
25484 green: 0.33,
25485 blue: 0.33,
25486 alpha: 0.33,
25487 },
25488 },
25489 ])
25490 }
25491 });
25492
25493 let _handle = fake_language_server_without_capabilities
25494 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25495 panic!("Should not be called");
25496 });
25497 cx.executor().advance_clock(Duration::from_millis(100));
25498 color_request_handle.next().await.unwrap();
25499 cx.run_until_parked();
25500 assert_eq!(
25501 1,
25502 requests_made.load(atomic::Ordering::Acquire),
25503 "Should query for colors once per editor open"
25504 );
25505 editor.update_in(cx, |editor, _, cx| {
25506 assert_eq!(
25507 vec![expected_color],
25508 extract_color_inlays(editor, cx),
25509 "Should have an initial inlay"
25510 );
25511 });
25512
25513 // opening another file in a split should not influence the LSP query counter
25514 workspace
25515 .update(cx, |workspace, window, cx| {
25516 assert_eq!(
25517 workspace.panes().len(),
25518 1,
25519 "Should have one pane with one editor"
25520 );
25521 workspace.move_item_to_pane_in_direction(
25522 &MoveItemToPaneInDirection {
25523 direction: SplitDirection::Right,
25524 focus: false,
25525 clone: true,
25526 },
25527 window,
25528 cx,
25529 );
25530 })
25531 .unwrap();
25532 cx.run_until_parked();
25533 workspace
25534 .update(cx, |workspace, _, cx| {
25535 let panes = workspace.panes();
25536 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25537 for pane in panes {
25538 let editor = pane
25539 .read(cx)
25540 .active_item()
25541 .and_then(|item| item.downcast::<Editor>())
25542 .expect("Should have opened an editor in each split");
25543 let editor_file = editor
25544 .read(cx)
25545 .buffer()
25546 .read(cx)
25547 .as_singleton()
25548 .expect("test deals with singleton buffers")
25549 .read(cx)
25550 .file()
25551 .expect("test buffese should have a file")
25552 .path();
25553 assert_eq!(
25554 editor_file.as_ref(),
25555 Path::new("first.rs"),
25556 "Both editors should be opened for the same file"
25557 )
25558 }
25559 })
25560 .unwrap();
25561
25562 cx.executor().advance_clock(Duration::from_millis(500));
25563 let save = editor.update_in(cx, |editor, window, cx| {
25564 editor.move_to_end(&MoveToEnd, window, cx);
25565 editor.handle_input("dirty", window, cx);
25566 editor.save(
25567 SaveOptions {
25568 format: true,
25569 autosave: true,
25570 },
25571 project.clone(),
25572 window,
25573 cx,
25574 )
25575 });
25576 save.await.unwrap();
25577
25578 color_request_handle.next().await.unwrap();
25579 cx.run_until_parked();
25580 assert_eq!(
25581 3,
25582 requests_made.load(atomic::Ordering::Acquire),
25583 "Should query for colors once per save and once per formatting after save"
25584 );
25585
25586 drop(editor);
25587 let close = workspace
25588 .update(cx, |workspace, window, cx| {
25589 workspace.active_pane().update(cx, |pane, cx| {
25590 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25591 })
25592 })
25593 .unwrap();
25594 close.await.unwrap();
25595 let close = workspace
25596 .update(cx, |workspace, window, cx| {
25597 workspace.active_pane().update(cx, |pane, cx| {
25598 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25599 })
25600 })
25601 .unwrap();
25602 close.await.unwrap();
25603 assert_eq!(
25604 3,
25605 requests_made.load(atomic::Ordering::Acquire),
25606 "After saving and closing all editors, no extra requests should be made"
25607 );
25608 workspace
25609 .update(cx, |workspace, _, cx| {
25610 assert!(
25611 workspace.active_item(cx).is_none(),
25612 "Should close all editors"
25613 )
25614 })
25615 .unwrap();
25616
25617 workspace
25618 .update(cx, |workspace, window, cx| {
25619 workspace.active_pane().update(cx, |pane, cx| {
25620 pane.navigate_backward(&Default::default(), window, cx);
25621 })
25622 })
25623 .unwrap();
25624 cx.executor().advance_clock(Duration::from_millis(100));
25625 cx.run_until_parked();
25626 let editor = workspace
25627 .update(cx, |workspace, _, cx| {
25628 workspace
25629 .active_item(cx)
25630 .expect("Should have reopened the editor again after navigating back")
25631 .downcast::<Editor>()
25632 .expect("Should be an editor")
25633 })
25634 .unwrap();
25635 color_request_handle.next().await.unwrap();
25636 assert_eq!(
25637 3,
25638 requests_made.load(atomic::Ordering::Acquire),
25639 "Cache should be reused on buffer close and reopen"
25640 );
25641 editor.update(cx, |editor, cx| {
25642 assert_eq!(
25643 vec![expected_color],
25644 extract_color_inlays(editor, cx),
25645 "Should have an initial inlay"
25646 );
25647 });
25648}
25649
25650#[gpui::test]
25651async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25652 init_test(cx, |_| {});
25653 let (editor, cx) = cx.add_window_view(Editor::single_line);
25654 editor.update_in(cx, |editor, window, cx| {
25655 editor.set_text("oops\n\nwow\n", window, cx)
25656 });
25657 cx.run_until_parked();
25658 editor.update(cx, |editor, cx| {
25659 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25660 });
25661 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25662 cx.run_until_parked();
25663 editor.update(cx, |editor, cx| {
25664 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25665 });
25666}
25667
25668#[gpui::test]
25669async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25670 init_test(cx, |_| {});
25671
25672 cx.update(|cx| {
25673 register_project_item::<Editor>(cx);
25674 });
25675
25676 let fs = FakeFs::new(cx.executor());
25677 fs.insert_tree("/root1", json!({})).await;
25678 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25679 .await;
25680
25681 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25682 let (workspace, cx) =
25683 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25684
25685 let worktree_id = project.update(cx, |project, cx| {
25686 project.worktrees(cx).next().unwrap().read(cx).id()
25687 });
25688
25689 let handle = workspace
25690 .update_in(cx, |workspace, window, cx| {
25691 let project_path = (worktree_id, "one.pdf");
25692 workspace.open_path(project_path, None, true, window, cx)
25693 })
25694 .await
25695 .unwrap();
25696
25697 assert_eq!(
25698 handle.to_any().entity_type(),
25699 TypeId::of::<InvalidBufferView>()
25700 );
25701}
25702
25703#[gpui::test]
25704async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25705 init_test(cx, |_| {});
25706
25707 let language = Arc::new(Language::new(
25708 LanguageConfig::default(),
25709 Some(tree_sitter_rust::LANGUAGE.into()),
25710 ));
25711
25712 // Test hierarchical sibling navigation
25713 let text = r#"
25714 fn outer() {
25715 if condition {
25716 let a = 1;
25717 }
25718 let b = 2;
25719 }
25720
25721 fn another() {
25722 let c = 3;
25723 }
25724 "#;
25725
25726 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25727 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25728 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25729
25730 // Wait for parsing to complete
25731 editor
25732 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25733 .await;
25734
25735 editor.update_in(cx, |editor, window, cx| {
25736 // Start by selecting "let a = 1;" inside the if block
25737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25738 s.select_display_ranges([
25739 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25740 ]);
25741 });
25742
25743 let initial_selection = editor.selections.display_ranges(cx);
25744 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25745
25746 // Test select next sibling - should move up levels to find the next sibling
25747 // Since "let a = 1;" has no siblings in the if block, it should move up
25748 // to find "let b = 2;" which is a sibling of the if block
25749 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25750 let next_selection = editor.selections.display_ranges(cx);
25751
25752 // Should have a selection and it should be different from the initial
25753 assert_eq!(
25754 next_selection.len(),
25755 1,
25756 "Should have one selection after next"
25757 );
25758 assert_ne!(
25759 next_selection[0], initial_selection[0],
25760 "Next sibling selection should be different"
25761 );
25762
25763 // Test hierarchical navigation by going to the end of the current function
25764 // and trying to navigate to the next function
25765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25766 s.select_display_ranges([
25767 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25768 ]);
25769 });
25770
25771 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25772 let function_next_selection = editor.selections.display_ranges(cx);
25773
25774 // Should move to the next function
25775 assert_eq!(
25776 function_next_selection.len(),
25777 1,
25778 "Should have one selection after function next"
25779 );
25780
25781 // Test select previous sibling navigation
25782 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25783 let prev_selection = editor.selections.display_ranges(cx);
25784
25785 // Should have a selection and it should be different
25786 assert_eq!(
25787 prev_selection.len(),
25788 1,
25789 "Should have one selection after prev"
25790 );
25791 assert_ne!(
25792 prev_selection[0], function_next_selection[0],
25793 "Previous sibling selection should be different from next"
25794 );
25795 });
25796}
25797
25798#[gpui::test]
25799async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25800 init_test(cx, |_| {});
25801
25802 let mut cx = EditorTestContext::new(cx).await;
25803 cx.set_state(
25804 "let ˇvariable = 42;
25805let another = variable + 1;
25806let result = variable * 2;",
25807 );
25808
25809 // Set up document highlights manually (simulating LSP response)
25810 cx.update_editor(|editor, _window, cx| {
25811 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25812
25813 // Create highlights for "variable" occurrences
25814 let highlight_ranges = [
25815 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25816 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25817 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25818 ];
25819
25820 let anchor_ranges: Vec<_> = highlight_ranges
25821 .iter()
25822 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25823 .collect();
25824
25825 editor.highlight_background::<DocumentHighlightRead>(
25826 &anchor_ranges,
25827 |theme| theme.colors().editor_document_highlight_read_background,
25828 cx,
25829 );
25830 });
25831
25832 // Go to next highlight - should move to second "variable"
25833 cx.update_editor(|editor, window, cx| {
25834 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25835 });
25836 cx.assert_editor_state(
25837 "let variable = 42;
25838let another = ˇvariable + 1;
25839let result = variable * 2;",
25840 );
25841
25842 // Go to next highlight - should move to third "variable"
25843 cx.update_editor(|editor, window, cx| {
25844 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25845 });
25846 cx.assert_editor_state(
25847 "let variable = 42;
25848let another = variable + 1;
25849let result = ˇvariable * 2;",
25850 );
25851
25852 // Go to next highlight - should stay at third "variable" (no wrap-around)
25853 cx.update_editor(|editor, window, cx| {
25854 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25855 });
25856 cx.assert_editor_state(
25857 "let variable = 42;
25858let another = variable + 1;
25859let result = ˇvariable * 2;",
25860 );
25861
25862 // Now test going backwards from third position
25863 cx.update_editor(|editor, window, cx| {
25864 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25865 });
25866 cx.assert_editor_state(
25867 "let variable = 42;
25868let another = ˇvariable + 1;
25869let result = variable * 2;",
25870 );
25871
25872 // Go to previous highlight - should move to first "variable"
25873 cx.update_editor(|editor, window, cx| {
25874 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25875 });
25876 cx.assert_editor_state(
25877 "let ˇvariable = 42;
25878let another = variable + 1;
25879let result = variable * 2;",
25880 );
25881
25882 // Go to previous highlight - should stay on first "variable"
25883 cx.update_editor(|editor, window, cx| {
25884 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25885 });
25886 cx.assert_editor_state(
25887 "let ˇvariable = 42;
25888let another = variable + 1;
25889let result = variable * 2;",
25890 );
25891}
25892
25893#[track_caller]
25894fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25895 editor
25896 .all_inlays(cx)
25897 .into_iter()
25898 .filter_map(|inlay| inlay.get_color())
25899 .map(Rgba::from)
25900 .collect()
25901}