1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 ActivatePane, CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions,
59 ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
184
185 editor.backspace(&Backspace, window, cx);
186 });
187 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
188}
189
190#[gpui::test]
191fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
192 init_test(cx, |_| {});
193
194 let mut now = Instant::now();
195 let group_interval = Duration::from_millis(1);
196 let buffer = cx.new(|cx| {
197 let mut buf = language::Buffer::local("123456", cx);
198 buf.set_group_interval(group_interval);
199 buf
200 });
201 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
202 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
203
204 _ = editor.update(cx, |editor, window, cx| {
205 editor.start_transaction_at(now, window, cx);
206 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
207
208 editor.insert("cd", window, cx);
209 editor.end_transaction_at(now, cx);
210 assert_eq!(editor.text(cx), "12cd56");
211 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
212
213 editor.start_transaction_at(now, window, cx);
214 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
215 editor.insert("e", window, cx);
216 editor.end_transaction_at(now, cx);
217 assert_eq!(editor.text(cx), "12cde6");
218 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
219
220 now += group_interval + Duration::from_millis(1);
221 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
222
223 // Simulate an edit in another editor
224 buffer.update(cx, |buffer, cx| {
225 buffer.start_transaction_at(now, cx);
226 buffer.edit([(0..1, "a")], None, cx);
227 buffer.edit([(1..1, "b")], None, cx);
228 buffer.end_transaction_at(now, cx);
229 });
230
231 assert_eq!(editor.text(cx), "ab2cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
233
234 // Last transaction happened past the group interval in a different editor.
235 // Undo it individually and don't restore selections.
236 editor.undo(&Undo, window, cx);
237 assert_eq!(editor.text(cx), "12cde6");
238 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
239
240 // First two transactions happened within the group interval in this editor.
241 // Undo them together and restore selections.
242 editor.undo(&Undo, window, cx);
243 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
244 assert_eq!(editor.text(cx), "123456");
245 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
246
247 // Redo the first two transactions together.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "12cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
251
252 // Redo the last transaction on its own.
253 editor.redo(&Redo, window, cx);
254 assert_eq!(editor.text(cx), "ab2cde6");
255 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
256
257 // Test empty transactions.
258 editor.start_transaction_at(now, window, cx);
259 editor.end_transaction_at(now, cx);
260 editor.undo(&Undo, window, cx);
261 assert_eq!(editor.text(cx), "12cde6");
262 });
263}
264
265#[gpui::test]
266fn test_ime_composition(cx: &mut TestAppContext) {
267 init_test(cx, |_| {});
268
269 let buffer = cx.new(|cx| {
270 let mut buffer = language::Buffer::local("abcde", cx);
271 // Ensure automatic grouping doesn't occur.
272 buffer.set_group_interval(Duration::ZERO);
273 buffer
274 });
275
276 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
277 cx.add_window(|window, cx| {
278 let mut editor = build_editor(buffer.clone(), window, cx);
279
280 // Start a new IME composition.
281 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
282 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
283 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
284 assert_eq!(editor.text(cx), "äbcde");
285 assert_eq!(
286 editor.marked_text_ranges(cx),
287 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
288 );
289
290 // Finalize IME composition.
291 editor.replace_text_in_range(None, "ā", window, cx);
292 assert_eq!(editor.text(cx), "ābcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294
295 // IME composition edits are grouped and are undone/redone at once.
296 editor.undo(&Default::default(), window, cx);
297 assert_eq!(editor.text(cx), "abcde");
298 assert_eq!(editor.marked_text_ranges(cx), None);
299 editor.redo(&Default::default(), window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // Start a new IME composition.
304 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
305 assert_eq!(
306 editor.marked_text_ranges(cx),
307 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
308 );
309
310 // Undoing during an IME composition cancels it.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "ābcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314
315 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
316 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
317 assert_eq!(editor.text(cx), "ābcdè");
318 assert_eq!(
319 editor.marked_text_ranges(cx),
320 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
321 );
322
323 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
324 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
325 assert_eq!(editor.text(cx), "ābcdę");
326 assert_eq!(editor.marked_text_ranges(cx), None);
327
328 // Start a new IME composition with multiple cursors.
329 editor.change_selections(None, window, cx, |s| {
330 s.select_ranges([
331 OffsetUtf16(1)..OffsetUtf16(1),
332 OffsetUtf16(3)..OffsetUtf16(3),
333 OffsetUtf16(5)..OffsetUtf16(5),
334 ])
335 });
336 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
337 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
338 assert_eq!(
339 editor.marked_text_ranges(cx),
340 Some(vec![
341 OffsetUtf16(0)..OffsetUtf16(3),
342 OffsetUtf16(4)..OffsetUtf16(7),
343 OffsetUtf16(8)..OffsetUtf16(11)
344 ])
345 );
346
347 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
348 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
349 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
350 assert_eq!(
351 editor.marked_text_ranges(cx),
352 Some(vec![
353 OffsetUtf16(1)..OffsetUtf16(2),
354 OffsetUtf16(5)..OffsetUtf16(6),
355 OffsetUtf16(9)..OffsetUtf16(10)
356 ])
357 );
358
359 // Finalize IME composition with multiple cursors.
360 editor.replace_text_in_range(Some(9..10), "2", window, cx);
361 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
362 assert_eq!(editor.marked_text_ranges(cx), None);
363
364 editor
365 });
366}
367
368#[gpui::test]
369fn test_selection_with_mouse(cx: &mut TestAppContext) {
370 init_test(cx, |_| {});
371
372 let editor = cx.add_window(|window, cx| {
373 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
374 build_editor(buffer, window, cx)
375 });
376
377 _ = editor.update(cx, |editor, window, cx| {
378 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
379 });
380 assert_eq!(
381 editor
382 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
383 .unwrap(),
384 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
385 );
386
387 _ = editor.update(cx, |editor, window, cx| {
388 editor.update_selection(
389 DisplayPoint::new(DisplayRow(3), 3),
390 0,
391 gpui::Point::<f32>::default(),
392 window,
393 cx,
394 );
395 });
396
397 assert_eq!(
398 editor
399 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
400 .unwrap(),
401 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
402 );
403
404 _ = editor.update(cx, |editor, window, cx| {
405 editor.update_selection(
406 DisplayPoint::new(DisplayRow(1), 1),
407 0,
408 gpui::Point::<f32>::default(),
409 window,
410 cx,
411 );
412 });
413
414 assert_eq!(
415 editor
416 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
417 .unwrap(),
418 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
419 );
420
421 _ = editor.update(cx, |editor, window, cx| {
422 editor.end_selection(window, cx);
423 editor.update_selection(
424 DisplayPoint::new(DisplayRow(3), 3),
425 0,
426 gpui::Point::<f32>::default(),
427 window,
428 cx,
429 );
430 });
431
432 assert_eq!(
433 editor
434 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
435 .unwrap(),
436 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
437 );
438
439 _ = editor.update(cx, |editor, window, cx| {
440 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
441 editor.update_selection(
442 DisplayPoint::new(DisplayRow(0), 0),
443 0,
444 gpui::Point::<f32>::default(),
445 window,
446 cx,
447 );
448 });
449
450 assert_eq!(
451 editor
452 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
453 .unwrap(),
454 [
455 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
456 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
457 ]
458 );
459
460 _ = editor.update(cx, |editor, window, cx| {
461 editor.end_selection(window, cx);
462 });
463
464 assert_eq!(
465 editor
466 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
467 .unwrap(),
468 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
469 );
470}
471
472#[gpui::test]
473fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
474 init_test(cx, |_| {});
475
476 let editor = cx.add_window(|window, cx| {
477 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
478 build_editor(buffer, window, cx)
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
483 });
484
485 _ = editor.update(cx, |editor, window, cx| {
486 editor.end_selection(window, cx);
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 assert_eq!(
498 editor
499 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
500 .unwrap(),
501 [
502 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
503 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
504 ]
505 );
506
507 _ = editor.update(cx, |editor, window, cx| {
508 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
509 });
510
511 _ = editor.update(cx, |editor, window, cx| {
512 editor.end_selection(window, cx);
513 });
514
515 assert_eq!(
516 editor
517 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
518 .unwrap(),
519 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
520 );
521}
522
523#[gpui::test]
524fn test_canceling_pending_selection(cx: &mut TestAppContext) {
525 init_test(cx, |_| {});
526
527 let editor = cx.add_window(|window, cx| {
528 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
529 build_editor(buffer, window, cx)
530 });
531
532 _ = editor.update(cx, |editor, window, cx| {
533 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
534 assert_eq!(
535 editor.selections.display_ranges(cx),
536 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
537 );
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.update_selection(
542 DisplayPoint::new(DisplayRow(3), 3),
543 0,
544 gpui::Point::<f32>::default(),
545 window,
546 cx,
547 );
548 assert_eq!(
549 editor.selections.display_ranges(cx),
550 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
551 );
552 });
553
554 _ = editor.update(cx, |editor, window, cx| {
555 editor.cancel(&Cancel, window, cx);
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(1), 1),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568}
569
570#[gpui::test]
571fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
572 init_test(cx, |_| {});
573
574 let editor = cx.add_window(|window, cx| {
575 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
576 build_editor(buffer, window, cx)
577 });
578
579 _ = editor.update(cx, |editor, window, cx| {
580 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
581 assert_eq!(
582 editor.selections.display_ranges(cx),
583 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
584 );
585
586 editor.move_down(&Default::default(), window, cx);
587 assert_eq!(
588 editor.selections.display_ranges(cx),
589 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
590 );
591
592 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
593 assert_eq!(
594 editor.selections.display_ranges(cx),
595 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
596 );
597
598 editor.move_up(&Default::default(), window, cx);
599 assert_eq!(
600 editor.selections.display_ranges(cx),
601 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
602 );
603 });
604}
605
606#[gpui::test]
607fn test_clone(cx: &mut TestAppContext) {
608 init_test(cx, |_| {});
609
610 let (text, selection_ranges) = marked_text_ranges(
611 indoc! {"
612 one
613 two
614 threeˇ
615 four
616 fiveˇ
617 "},
618 true,
619 );
620
621 let editor = cx.add_window(|window, cx| {
622 let buffer = MultiBuffer::build_simple(&text, cx);
623 build_editor(buffer, window, cx)
624 });
625
626 _ = editor.update(cx, |editor, window, cx| {
627 editor.change_selections(None, window, cx, |s| {
628 s.select_ranges(selection_ranges.clone())
629 });
630 editor.fold_creases(
631 vec![
632 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
633 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
634 ],
635 true,
636 window,
637 cx,
638 );
639 });
640
641 let cloned_editor = editor
642 .update(cx, |editor, _, cx| {
643 cx.open_window(Default::default(), |window, cx| {
644 cx.new(|cx| editor.clone(window, cx))
645 })
646 })
647 .unwrap()
648 .unwrap();
649
650 let snapshot = editor
651 .update(cx, |e, window, cx| e.snapshot(window, cx))
652 .unwrap();
653 let cloned_snapshot = cloned_editor
654 .update(cx, |e, window, cx| e.snapshot(window, cx))
655 .unwrap();
656
657 assert_eq!(
658 cloned_editor
659 .update(cx, |e, _, cx| e.display_text(cx))
660 .unwrap(),
661 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
662 );
663 assert_eq!(
664 cloned_snapshot
665 .folds_in_range(0..text.len())
666 .collect::<Vec<_>>(),
667 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
668 );
669 assert_set_eq!(
670 cloned_editor
671 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
672 .unwrap(),
673 editor
674 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
675 .unwrap()
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
680 .unwrap(),
681 editor
682 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
683 .unwrap()
684 );
685}
686
687#[gpui::test]
688async fn test_navigation_history(cx: &mut TestAppContext) {
689 init_test(cx, |_| {});
690
691 use workspace::item::Item;
692
693 let fs = FakeFs::new(cx.executor());
694 let project = Project::test(fs, [], cx).await;
695 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
696 let pane = workspace
697 .update(cx, |workspace, _, _| workspace.active_pane().clone())
698 .unwrap();
699
700 _ = workspace.update(cx, |_v, window, cx| {
701 cx.new(|cx| {
702 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
703 let mut editor = build_editor(buffer.clone(), window, cx);
704 let handle = cx.entity();
705 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
706
707 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
708 editor.nav_history.as_mut().unwrap().pop_backward(cx)
709 }
710
711 // Move the cursor a small distance.
712 // Nothing is added to the navigation history.
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
716 ])
717 });
718 editor.change_selections(None, window, cx, |s| {
719 s.select_display_ranges([
720 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
721 ])
722 });
723 assert!(pop_history(&mut editor, cx).is_none());
724
725 // Move the cursor a large distance.
726 // The history can jump back to the previous position.
727 editor.change_selections(None, window, cx, |s| {
728 s.select_display_ranges([
729 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
730 ])
731 });
732 let nav_entry = pop_history(&mut editor, cx).unwrap();
733 editor.navigate(nav_entry.data.unwrap(), window, cx);
734 assert_eq!(nav_entry.item.id(), cx.entity_id());
735 assert_eq!(
736 editor.selections.display_ranges(cx),
737 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
738 );
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a small distance via the mouse.
742 // Nothing is added to the navigation history.
743 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
744 editor.end_selection(window, cx);
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
748 );
749 assert!(pop_history(&mut editor, cx).is_none());
750
751 // Move the cursor a large distance via the mouse.
752 // The history can jump back to the previous position.
753 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
754 editor.end_selection(window, cx);
755 assert_eq!(
756 editor.selections.display_ranges(cx),
757 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
758 );
759 let nav_entry = pop_history(&mut editor, cx).unwrap();
760 editor.navigate(nav_entry.data.unwrap(), window, cx);
761 assert_eq!(nav_entry.item.id(), cx.entity_id());
762 assert_eq!(
763 editor.selections.display_ranges(cx),
764 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
765 );
766 assert!(pop_history(&mut editor, cx).is_none());
767
768 // Set scroll position to check later
769 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
770 let original_scroll_position = editor.scroll_manager.anchor();
771
772 // Jump to the end of the document and adjust scroll
773 editor.move_to_end(&MoveToEnd, window, cx);
774 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
775 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
776
777 let nav_entry = pop_history(&mut editor, cx).unwrap();
778 editor.navigate(nav_entry.data.unwrap(), window, cx);
779 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
780
781 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
782 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
783 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
784 let invalid_point = Point::new(9999, 0);
785 editor.navigate(
786 Box::new(NavigationData {
787 cursor_anchor: invalid_anchor,
788 cursor_position: invalid_point,
789 scroll_anchor: ScrollAnchor {
790 anchor: invalid_anchor,
791 offset: Default::default(),
792 },
793 scroll_top_row: invalid_point.row,
794 }),
795 window,
796 cx,
797 );
798 assert_eq!(
799 editor.selections.display_ranges(cx),
800 &[editor.max_point(cx)..editor.max_point(cx)]
801 );
802 assert_eq!(
803 editor.scroll_position(cx),
804 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
805 );
806
807 editor
808 })
809 });
810}
811
812#[gpui::test]
813fn test_cancel(cx: &mut TestAppContext) {
814 init_test(cx, |_| {});
815
816 let editor = cx.add_window(|window, cx| {
817 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
818 build_editor(buffer, window, cx)
819 });
820
821 _ = editor.update(cx, |editor, window, cx| {
822 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
823 editor.update_selection(
824 DisplayPoint::new(DisplayRow(1), 1),
825 0,
826 gpui::Point::<f32>::default(),
827 window,
828 cx,
829 );
830 editor.end_selection(window, cx);
831
832 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
833 editor.update_selection(
834 DisplayPoint::new(DisplayRow(0), 3),
835 0,
836 gpui::Point::<f32>::default(),
837 window,
838 cx,
839 );
840 editor.end_selection(window, cx);
841 assert_eq!(
842 editor.selections.display_ranges(cx),
843 [
844 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
845 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
846 ]
847 );
848 });
849
850 _ = editor.update(cx, |editor, window, cx| {
851 editor.cancel(&Cancel, window, cx);
852 assert_eq!(
853 editor.selections.display_ranges(cx),
854 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865}
866
867#[gpui::test]
868fn test_fold_action(cx: &mut TestAppContext) {
869 init_test(cx, |_| {});
870
871 let editor = cx.add_window(|window, cx| {
872 let buffer = MultiBuffer::build_simple(
873 &"
874 impl Foo {
875 // Hello!
876
877 fn a() {
878 1
879 }
880
881 fn b() {
882 2
883 }
884
885 fn c() {
886 3
887 }
888 }
889 "
890 .unindent(),
891 cx,
892 );
893 build_editor(buffer.clone(), window, cx)
894 });
895
896 _ = editor.update(cx, |editor, window, cx| {
897 editor.change_selections(None, window, cx, |s| {
898 s.select_display_ranges([
899 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
900 ]);
901 });
902 editor.fold(&Fold, window, cx);
903 assert_eq!(
904 editor.display_text(cx),
905 "
906 impl Foo {
907 // Hello!
908
909 fn a() {
910 1
911 }
912
913 fn b() {⋯
914 }
915
916 fn c() {⋯
917 }
918 }
919 "
920 .unindent(),
921 );
922
923 editor.fold(&Fold, window, cx);
924 assert_eq!(
925 editor.display_text(cx),
926 "
927 impl Foo {⋯
928 }
929 "
930 .unindent(),
931 );
932
933 editor.unfold_lines(&UnfoldLines, window, cx);
934 assert_eq!(
935 editor.display_text(cx),
936 "
937 impl Foo {
938 // Hello!
939
940 fn a() {
941 1
942 }
943
944 fn b() {⋯
945 }
946
947 fn c() {⋯
948 }
949 }
950 "
951 .unindent(),
952 );
953
954 editor.unfold_lines(&UnfoldLines, window, cx);
955 assert_eq!(
956 editor.display_text(cx),
957 editor.buffer.read(cx).read(cx).text()
958 );
959 });
960}
961
962#[gpui::test]
963fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
964 init_test(cx, |_| {});
965
966 let editor = cx.add_window(|window, cx| {
967 let buffer = MultiBuffer::build_simple(
968 &"
969 class Foo:
970 # Hello!
971
972 def a():
973 print(1)
974
975 def b():
976 print(2)
977
978 def c():
979 print(3)
980 "
981 .unindent(),
982 cx,
983 );
984 build_editor(buffer.clone(), window, cx)
985 });
986
987 _ = editor.update(cx, |editor, window, cx| {
988 editor.change_selections(None, window, cx, |s| {
989 s.select_display_ranges([
990 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
991 ]);
992 });
993 editor.fold(&Fold, window, cx);
994 assert_eq!(
995 editor.display_text(cx),
996 "
997 class Foo:
998 # Hello!
999
1000 def a():
1001 print(1)
1002
1003 def b():⋯
1004
1005 def c():⋯
1006 "
1007 .unindent(),
1008 );
1009
1010 editor.fold(&Fold, window, cx);
1011 assert_eq!(
1012 editor.display_text(cx),
1013 "
1014 class Foo:⋯
1015 "
1016 .unindent(),
1017 );
1018
1019 editor.unfold_lines(&UnfoldLines, window, cx);
1020 assert_eq!(
1021 editor.display_text(cx),
1022 "
1023 class Foo:
1024 # Hello!
1025
1026 def a():
1027 print(1)
1028
1029 def b():⋯
1030
1031 def c():⋯
1032 "
1033 .unindent(),
1034 );
1035
1036 editor.unfold_lines(&UnfoldLines, window, cx);
1037 assert_eq!(
1038 editor.display_text(cx),
1039 editor.buffer.read(cx).read(cx).text()
1040 );
1041 });
1042}
1043
1044#[gpui::test]
1045fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1046 init_test(cx, |_| {});
1047
1048 let editor = cx.add_window(|window, cx| {
1049 let buffer = MultiBuffer::build_simple(
1050 &"
1051 class Foo:
1052 # Hello!
1053
1054 def a():
1055 print(1)
1056
1057 def b():
1058 print(2)
1059
1060
1061 def c():
1062 print(3)
1063
1064
1065 "
1066 .unindent(),
1067 cx,
1068 );
1069 build_editor(buffer.clone(), window, cx)
1070 });
1071
1072 _ = editor.update(cx, |editor, window, cx| {
1073 editor.change_selections(None, window, cx, |s| {
1074 s.select_display_ranges([
1075 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1076 ]);
1077 });
1078 editor.fold(&Fold, window, cx);
1079 assert_eq!(
1080 editor.display_text(cx),
1081 "
1082 class Foo:
1083 # Hello!
1084
1085 def a():
1086 print(1)
1087
1088 def b():⋯
1089
1090
1091 def c():⋯
1092
1093
1094 "
1095 .unindent(),
1096 );
1097
1098 editor.fold(&Fold, window, cx);
1099 assert_eq!(
1100 editor.display_text(cx),
1101 "
1102 class Foo:⋯
1103
1104
1105 "
1106 .unindent(),
1107 );
1108
1109 editor.unfold_lines(&UnfoldLines, window, cx);
1110 assert_eq!(
1111 editor.display_text(cx),
1112 "
1113 class Foo:
1114 # Hello!
1115
1116 def a():
1117 print(1)
1118
1119 def b():⋯
1120
1121
1122 def c():⋯
1123
1124
1125 "
1126 .unindent(),
1127 );
1128
1129 editor.unfold_lines(&UnfoldLines, window, cx);
1130 assert_eq!(
1131 editor.display_text(cx),
1132 editor.buffer.read(cx).read(cx).text()
1133 );
1134 });
1135}
1136
1137#[gpui::test]
1138fn test_fold_at_level(cx: &mut TestAppContext) {
1139 init_test(cx, |_| {});
1140
1141 let editor = cx.add_window(|window, cx| {
1142 let buffer = MultiBuffer::build_simple(
1143 &"
1144 class Foo:
1145 # Hello!
1146
1147 def a():
1148 print(1)
1149
1150 def b():
1151 print(2)
1152
1153
1154 class Bar:
1155 # World!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 "
1165 .unindent(),
1166 cx,
1167 );
1168 build_editor(buffer.clone(), window, cx)
1169 });
1170
1171 _ = editor.update(cx, |editor, window, cx| {
1172 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1173 assert_eq!(
1174 editor.display_text(cx),
1175 "
1176 class Foo:
1177 # Hello!
1178
1179 def a():⋯
1180
1181 def b():⋯
1182
1183
1184 class Bar:
1185 # World!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 "
1193 .unindent(),
1194 );
1195
1196 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1197 assert_eq!(
1198 editor.display_text(cx),
1199 "
1200 class Foo:⋯
1201
1202
1203 class Bar:⋯
1204
1205
1206 "
1207 .unindent(),
1208 );
1209
1210 editor.unfold_all(&UnfoldAll, window, cx);
1211 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:
1216 # Hello!
1217
1218 def a():
1219 print(1)
1220
1221 def b():
1222 print(2)
1223
1224
1225 class Bar:
1226 # World!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 "
1236 .unindent(),
1237 );
1238
1239 assert_eq!(
1240 editor.display_text(cx),
1241 editor.buffer.read(cx).read(cx).text()
1242 );
1243 });
1244}
1245
1246#[gpui::test]
1247fn test_move_cursor(cx: &mut TestAppContext) {
1248 init_test(cx, |_| {});
1249
1250 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1251 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1252
1253 buffer.update(cx, |buffer, cx| {
1254 buffer.edit(
1255 vec![
1256 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1257 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1258 ],
1259 None,
1260 cx,
1261 );
1262 });
1263 _ = editor.update(cx, |editor, window, cx| {
1264 assert_eq!(
1265 editor.selections.display_ranges(cx),
1266 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1267 );
1268
1269 editor.move_down(&MoveDown, window, cx);
1270 assert_eq!(
1271 editor.selections.display_ranges(cx),
1272 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1273 );
1274
1275 editor.move_right(&MoveRight, window, cx);
1276 assert_eq!(
1277 editor.selections.display_ranges(cx),
1278 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1279 );
1280
1281 editor.move_left(&MoveLeft, window, cx);
1282 assert_eq!(
1283 editor.selections.display_ranges(cx),
1284 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1285 );
1286
1287 editor.move_up(&MoveUp, window, cx);
1288 assert_eq!(
1289 editor.selections.display_ranges(cx),
1290 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1291 );
1292
1293 editor.move_to_end(&MoveToEnd, window, cx);
1294 assert_eq!(
1295 editor.selections.display_ranges(cx),
1296 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1297 );
1298
1299 editor.move_to_beginning(&MoveToBeginning, window, cx);
1300 assert_eq!(
1301 editor.selections.display_ranges(cx),
1302 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1303 );
1304
1305 editor.change_selections(None, window, cx, |s| {
1306 s.select_display_ranges([
1307 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1308 ]);
1309 });
1310 editor.select_to_beginning(&SelectToBeginning, window, cx);
1311 assert_eq!(
1312 editor.selections.display_ranges(cx),
1313 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1314 );
1315
1316 editor.select_to_end(&SelectToEnd, window, cx);
1317 assert_eq!(
1318 editor.selections.display_ranges(cx),
1319 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1320 );
1321 });
1322}
1323
1324#[gpui::test]
1325fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1326 init_test(cx, |_| {});
1327
1328 let editor = cx.add_window(|window, cx| {
1329 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1330 build_editor(buffer.clone(), window, cx)
1331 });
1332
1333 assert_eq!('🟥'.len_utf8(), 4);
1334 assert_eq!('α'.len_utf8(), 2);
1335
1336 _ = editor.update(cx, |editor, window, cx| {
1337 editor.fold_creases(
1338 vec![
1339 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1341 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1342 ],
1343 true,
1344 window,
1345 cx,
1346 );
1347 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1348
1349 editor.move_right(&MoveRight, window, cx);
1350 assert_eq!(
1351 editor.selections.display_ranges(cx),
1352 &[empty_range(0, "🟥".len())]
1353 );
1354 editor.move_right(&MoveRight, window, cx);
1355 assert_eq!(
1356 editor.selections.display_ranges(cx),
1357 &[empty_range(0, "🟥🟧".len())]
1358 );
1359 editor.move_right(&MoveRight, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[empty_range(0, "🟥🟧⋯".len())]
1363 );
1364
1365 editor.move_down(&MoveDown, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(1, "ab⋯e".len())]
1369 );
1370 editor.move_left(&MoveLeft, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(1, "ab⋯".len())]
1374 );
1375 editor.move_left(&MoveLeft, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "ab".len())]
1379 );
1380 editor.move_left(&MoveLeft, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "a".len())]
1384 );
1385
1386 editor.move_down(&MoveDown, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(2, "α".len())]
1390 );
1391 editor.move_right(&MoveRight, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(2, "αβ".len())]
1395 );
1396 editor.move_right(&MoveRight, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "αβ⋯".len())]
1400 );
1401 editor.move_right(&MoveRight, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "αβ⋯ε".len())]
1405 );
1406
1407 editor.move_up(&MoveUp, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(1, "ab⋯e".len())]
1411 );
1412 editor.move_down(&MoveDown, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(2, "αβ⋯ε".len())]
1416 );
1417 editor.move_up(&MoveUp, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(1, "ab⋯e".len())]
1421 );
1422
1423 editor.move_up(&MoveUp, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(0, "🟥🟧".len())]
1427 );
1428 editor.move_left(&MoveLeft, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(0, "🟥".len())]
1432 );
1433 editor.move_left(&MoveLeft, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(0, "".len())]
1437 );
1438 });
1439}
1440
1441#[gpui::test]
1442fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1443 init_test(cx, |_| {});
1444
1445 let editor = cx.add_window(|window, cx| {
1446 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1447 build_editor(buffer.clone(), window, cx)
1448 });
1449 _ = editor.update(cx, |editor, window, cx| {
1450 editor.change_selections(None, window, cx, |s| {
1451 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1452 });
1453
1454 // moving above start of document should move selection to start of document,
1455 // but the next move down should still be at the original goal_x
1456 editor.move_up(&MoveUp, window, cx);
1457 assert_eq!(
1458 editor.selections.display_ranges(cx),
1459 &[empty_range(0, "".len())]
1460 );
1461
1462 editor.move_down(&MoveDown, window, cx);
1463 assert_eq!(
1464 editor.selections.display_ranges(cx),
1465 &[empty_range(1, "abcd".len())]
1466 );
1467
1468 editor.move_down(&MoveDown, window, cx);
1469 assert_eq!(
1470 editor.selections.display_ranges(cx),
1471 &[empty_range(2, "αβγ".len())]
1472 );
1473
1474 editor.move_down(&MoveDown, window, cx);
1475 assert_eq!(
1476 editor.selections.display_ranges(cx),
1477 &[empty_range(3, "abcd".len())]
1478 );
1479
1480 editor.move_down(&MoveDown, window, cx);
1481 assert_eq!(
1482 editor.selections.display_ranges(cx),
1483 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1484 );
1485
1486 // moving past end of document should not change goal_x
1487 editor.move_down(&MoveDown, window, cx);
1488 assert_eq!(
1489 editor.selections.display_ranges(cx),
1490 &[empty_range(5, "".len())]
1491 );
1492
1493 editor.move_down(&MoveDown, window, cx);
1494 assert_eq!(
1495 editor.selections.display_ranges(cx),
1496 &[empty_range(5, "".len())]
1497 );
1498
1499 editor.move_up(&MoveUp, window, cx);
1500 assert_eq!(
1501 editor.selections.display_ranges(cx),
1502 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1503 );
1504
1505 editor.move_up(&MoveUp, window, cx);
1506 assert_eq!(
1507 editor.selections.display_ranges(cx),
1508 &[empty_range(3, "abcd".len())]
1509 );
1510
1511 editor.move_up(&MoveUp, window, cx);
1512 assert_eq!(
1513 editor.selections.display_ranges(cx),
1514 &[empty_range(2, "αβγ".len())]
1515 );
1516 });
1517}
1518
1519#[gpui::test]
1520fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1521 init_test(cx, |_| {});
1522 let move_to_beg = MoveToBeginningOfLine {
1523 stop_at_soft_wraps: true,
1524 stop_at_indent: true,
1525 };
1526
1527 let delete_to_beg = DeleteToBeginningOfLine {
1528 stop_at_indent: false,
1529 };
1530
1531 let move_to_end = MoveToEndOfLine {
1532 stop_at_soft_wraps: true,
1533 };
1534
1535 let editor = cx.add_window(|window, cx| {
1536 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1537 build_editor(buffer, window, cx)
1538 });
1539 _ = editor.update(cx, |editor, window, cx| {
1540 editor.change_selections(None, window, cx, |s| {
1541 s.select_display_ranges([
1542 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1543 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1544 ]);
1545 });
1546 });
1547
1548 _ = editor.update(cx, |editor, window, cx| {
1549 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1550 assert_eq!(
1551 editor.selections.display_ranges(cx),
1552 &[
1553 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1554 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1555 ]
1556 );
1557 });
1558
1559 _ = editor.update(cx, |editor, window, cx| {
1560 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[
1564 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1565 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1566 ]
1567 );
1568 });
1569
1570 _ = editor.update(cx, |editor, window, cx| {
1571 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[
1575 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1576 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1577 ]
1578 );
1579 });
1580
1581 _ = editor.update(cx, |editor, window, cx| {
1582 editor.move_to_end_of_line(&move_to_end, window, cx);
1583 assert_eq!(
1584 editor.selections.display_ranges(cx),
1585 &[
1586 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1587 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1588 ]
1589 );
1590 });
1591
1592 // Moving to the end of line again is a no-op.
1593 _ = editor.update(cx, |editor, window, cx| {
1594 editor.move_to_end_of_line(&move_to_end, window, cx);
1595 assert_eq!(
1596 editor.selections.display_ranges(cx),
1597 &[
1598 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1599 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1600 ]
1601 );
1602 });
1603
1604 _ = editor.update(cx, |editor, window, cx| {
1605 editor.move_left(&MoveLeft, window, cx);
1606 editor.select_to_beginning_of_line(
1607 &SelectToBeginningOfLine {
1608 stop_at_soft_wraps: true,
1609 stop_at_indent: true,
1610 },
1611 window,
1612 cx,
1613 );
1614 assert_eq!(
1615 editor.selections.display_ranges(cx),
1616 &[
1617 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1618 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1619 ]
1620 );
1621 });
1622
1623 _ = editor.update(cx, |editor, window, cx| {
1624 editor.select_to_beginning_of_line(
1625 &SelectToBeginningOfLine {
1626 stop_at_soft_wraps: true,
1627 stop_at_indent: true,
1628 },
1629 window,
1630 cx,
1631 );
1632 assert_eq!(
1633 editor.selections.display_ranges(cx),
1634 &[
1635 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1636 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1637 ]
1638 );
1639 });
1640
1641 _ = editor.update(cx, |editor, window, cx| {
1642 editor.select_to_beginning_of_line(
1643 &SelectToBeginningOfLine {
1644 stop_at_soft_wraps: true,
1645 stop_at_indent: true,
1646 },
1647 window,
1648 cx,
1649 );
1650 assert_eq!(
1651 editor.selections.display_ranges(cx),
1652 &[
1653 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1654 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1655 ]
1656 );
1657 });
1658
1659 _ = editor.update(cx, |editor, window, cx| {
1660 editor.select_to_end_of_line(
1661 &SelectToEndOfLine {
1662 stop_at_soft_wraps: true,
1663 },
1664 window,
1665 cx,
1666 );
1667 assert_eq!(
1668 editor.selections.display_ranges(cx),
1669 &[
1670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1671 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1672 ]
1673 );
1674 });
1675
1676 _ = editor.update(cx, |editor, window, cx| {
1677 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1678 assert_eq!(editor.display_text(cx), "ab\n de");
1679 assert_eq!(
1680 editor.selections.display_ranges(cx),
1681 &[
1682 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1683 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1684 ]
1685 );
1686 });
1687
1688 _ = editor.update(cx, |editor, window, cx| {
1689 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1690 assert_eq!(editor.display_text(cx), "\n");
1691 assert_eq!(
1692 editor.selections.display_ranges(cx),
1693 &[
1694 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1695 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1696 ]
1697 );
1698 });
1699}
1700
1701#[gpui::test]
1702fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1703 init_test(cx, |_| {});
1704 let move_to_beg = MoveToBeginningOfLine {
1705 stop_at_soft_wraps: false,
1706 stop_at_indent: false,
1707 };
1708
1709 let move_to_end = MoveToEndOfLine {
1710 stop_at_soft_wraps: false,
1711 };
1712
1713 let editor = cx.add_window(|window, cx| {
1714 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1715 build_editor(buffer, window, cx)
1716 });
1717
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.set_wrap_width(Some(140.0.into()), cx);
1720
1721 // We expect the following lines after wrapping
1722 // ```
1723 // thequickbrownfox
1724 // jumpedoverthelazydo
1725 // gs
1726 // ```
1727 // The final `gs` was soft-wrapped onto a new line.
1728 assert_eq!(
1729 "thequickbrownfox\njumpedoverthelaz\nydogs",
1730 editor.display_text(cx),
1731 );
1732
1733 // First, let's assert behavior on the first line, that was not soft-wrapped.
1734 // Start the cursor at the `k` on the first line
1735 editor.change_selections(None, window, cx, |s| {
1736 s.select_display_ranges([
1737 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1738 ]);
1739 });
1740
1741 // Moving to the beginning of the line should put us at the beginning of the line.
1742 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1743 assert_eq!(
1744 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1745 editor.selections.display_ranges(cx)
1746 );
1747
1748 // Moving to the end of the line should put us at the end of the line.
1749 editor.move_to_end_of_line(&move_to_end, window, cx);
1750 assert_eq!(
1751 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1752 editor.selections.display_ranges(cx)
1753 );
1754
1755 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1756 // Start the cursor at the last line (`y` that was wrapped to a new line)
1757 editor.change_selections(None, window, cx, |s| {
1758 s.select_display_ranges([
1759 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1760 ]);
1761 });
1762
1763 // Moving to the beginning of the line should put us at the start of the second line of
1764 // display text, i.e., the `j`.
1765 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1766 assert_eq!(
1767 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1768 editor.selections.display_ranges(cx)
1769 );
1770
1771 // Moving to the beginning of the line again should be a no-op.
1772 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1773 assert_eq!(
1774 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1775 editor.selections.display_ranges(cx)
1776 );
1777
1778 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1779 // next display line.
1780 editor.move_to_end_of_line(&move_to_end, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line again should be a no-op.
1787 editor.move_to_end_of_line(&move_to_end, window, cx);
1788 assert_eq!(
1789 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1790 editor.selections.display_ranges(cx)
1791 );
1792 });
1793}
1794
1795#[gpui::test]
1796fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1797 init_test(cx, |_| {});
1798
1799 let move_to_beg = MoveToBeginningOfLine {
1800 stop_at_soft_wraps: true,
1801 stop_at_indent: true,
1802 };
1803
1804 let select_to_beg = SelectToBeginningOfLine {
1805 stop_at_soft_wraps: true,
1806 stop_at_indent: true,
1807 };
1808
1809 let delete_to_beg = DeleteToBeginningOfLine {
1810 stop_at_indent: true,
1811 };
1812
1813 let move_to_end = MoveToEndOfLine {
1814 stop_at_soft_wraps: false,
1815 };
1816
1817 let editor = cx.add_window(|window, cx| {
1818 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1819 build_editor(buffer, window, cx)
1820 });
1821
1822 _ = editor.update(cx, |editor, window, cx| {
1823 editor.change_selections(None, window, cx, |s| {
1824 s.select_display_ranges([
1825 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1826 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1827 ]);
1828 });
1829
1830 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1831 // and the second cursor at the first non-whitespace character in the line.
1832 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1833 assert_eq!(
1834 editor.selections.display_ranges(cx),
1835 &[
1836 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1837 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1838 ]
1839 );
1840
1841 // Moving to the beginning of the line again should be a no-op for the first cursor,
1842 // and should move the second cursor to the beginning of the line.
1843 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1844 assert_eq!(
1845 editor.selections.display_ranges(cx),
1846 &[
1847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1848 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1849 ]
1850 );
1851
1852 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1853 // and should move the second cursor back to the first non-whitespace character in the line.
1854 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1855 assert_eq!(
1856 editor.selections.display_ranges(cx),
1857 &[
1858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1859 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1860 ]
1861 );
1862
1863 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1864 // and to the first non-whitespace character in the line for the second cursor.
1865 editor.move_to_end_of_line(&move_to_end, window, cx);
1866 editor.move_left(&MoveLeft, window, cx);
1867 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1868 assert_eq!(
1869 editor.selections.display_ranges(cx),
1870 &[
1871 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1872 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1873 ]
1874 );
1875
1876 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1877 // and should select to the beginning of the line for the second cursor.
1878 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1879 assert_eq!(
1880 editor.selections.display_ranges(cx),
1881 &[
1882 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1883 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1884 ]
1885 );
1886
1887 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1888 // and should delete to the first non-whitespace character in the line for the second cursor.
1889 editor.move_to_end_of_line(&move_to_end, window, cx);
1890 editor.move_left(&MoveLeft, window, cx);
1891 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1892 assert_eq!(editor.text(cx), "c\n f");
1893 });
1894}
1895
1896#[gpui::test]
1897fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1898 init_test(cx, |_| {});
1899
1900 let editor = cx.add_window(|window, cx| {
1901 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1902 build_editor(buffer, window, cx)
1903 });
1904 _ = editor.update(cx, |editor, window, cx| {
1905 editor.change_selections(None, window, cx, |s| {
1906 s.select_display_ranges([
1907 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1908 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1909 ])
1910 });
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1924 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1931
1932 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1933 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1934
1935 editor.move_right(&MoveRight, window, cx);
1936 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1937 assert_selection_ranges(
1938 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1939 editor,
1940 cx,
1941 );
1942
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(
1945 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1946 editor,
1947 cx,
1948 );
1949
1950 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1951 assert_selection_ranges(
1952 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1953 editor,
1954 cx,
1955 );
1956 });
1957}
1958
1959#[gpui::test]
1960fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1961 init_test(cx, |_| {});
1962
1963 let editor = cx.add_window(|window, cx| {
1964 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1965 build_editor(buffer, window, cx)
1966 });
1967
1968 _ = editor.update(cx, |editor, window, cx| {
1969 editor.set_wrap_width(Some(140.0.into()), cx);
1970 assert_eq!(
1971 editor.display_text(cx),
1972 "use one::{\n two::three::\n four::five\n};"
1973 );
1974
1975 editor.change_selections(None, window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1978 ]);
1979 });
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_eq!(
1983 editor.selections.display_ranges(cx),
1984 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1985 );
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_eq!(
1989 editor.selections.display_ranges(cx),
1990 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1991 );
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_eq!(
1995 editor.selections.display_ranges(cx),
1996 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1997 );
1998
1999 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2000 assert_eq!(
2001 editor.selections.display_ranges(cx),
2002 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2003 );
2004
2005 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2006 assert_eq!(
2007 editor.selections.display_ranges(cx),
2008 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2009 );
2010
2011 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2012 assert_eq!(
2013 editor.selections.display_ranges(cx),
2014 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023
2024 let line_height = cx.editor(|editor, window, _| {
2025 editor
2026 .style()
2027 .unwrap()
2028 .text
2029 .line_height_in_pixels(window.rem_size())
2030 });
2031 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2032
2033 cx.set_state(
2034 &r#"ˇone
2035 two
2036
2037 three
2038 fourˇ
2039 five
2040
2041 six"#
2042 .unindent(),
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2047 });
2048 cx.assert_editor_state(
2049 &r#"one
2050 two
2051 ˇ
2052 three
2053 four
2054 five
2055 ˇ
2056 six"#
2057 .unindent(),
2058 );
2059
2060 cx.update_editor(|editor, window, cx| {
2061 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2062 });
2063 cx.assert_editor_state(
2064 &r#"one
2065 two
2066
2067 three
2068 four
2069 five
2070 ˇ
2071 sixˇ"#
2072 .unindent(),
2073 );
2074
2075 cx.update_editor(|editor, window, cx| {
2076 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2077 });
2078 cx.assert_editor_state(
2079 &r#"one
2080 two
2081
2082 three
2083 four
2084 five
2085
2086 sixˇ"#
2087 .unindent(),
2088 );
2089
2090 cx.update_editor(|editor, window, cx| {
2091 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2092 });
2093 cx.assert_editor_state(
2094 &r#"one
2095 two
2096
2097 three
2098 four
2099 five
2100 ˇ
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"ˇone
2125 two
2126
2127 three
2128 four
2129 five
2130
2131 six"#
2132 .unindent(),
2133 );
2134}
2135
2136#[gpui::test]
2137async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2138 init_test(cx, |_| {});
2139 let mut cx = EditorTestContext::new(cx).await;
2140 let line_height = cx.editor(|editor, window, _| {
2141 editor
2142 .style()
2143 .unwrap()
2144 .text
2145 .line_height_in_pixels(window.rem_size())
2146 });
2147 let window = cx.window;
2148 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2149
2150 cx.set_state(
2151 r#"ˇone
2152 two
2153 three
2154 four
2155 five
2156 six
2157 seven
2158 eight
2159 nine
2160 ten
2161 "#,
2162 );
2163
2164 cx.update_editor(|editor, window, cx| {
2165 assert_eq!(
2166 editor.snapshot(window, cx).scroll_position(),
2167 gpui::Point::new(0., 0.)
2168 );
2169 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2170 assert_eq!(
2171 editor.snapshot(window, cx).scroll_position(),
2172 gpui::Point::new(0., 3.)
2173 );
2174 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2175 assert_eq!(
2176 editor.snapshot(window, cx).scroll_position(),
2177 gpui::Point::new(0., 6.)
2178 );
2179 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2180 assert_eq!(
2181 editor.snapshot(window, cx).scroll_position(),
2182 gpui::Point::new(0., 3.)
2183 );
2184
2185 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2186 assert_eq!(
2187 editor.snapshot(window, cx).scroll_position(),
2188 gpui::Point::new(0., 1.)
2189 );
2190 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2191 assert_eq!(
2192 editor.snapshot(window, cx).scroll_position(),
2193 gpui::Point::new(0., 3.)
2194 );
2195 });
2196}
2197
2198#[gpui::test]
2199async fn test_autoscroll(cx: &mut TestAppContext) {
2200 init_test(cx, |_| {});
2201 let mut cx = EditorTestContext::new(cx).await;
2202
2203 let line_height = cx.update_editor(|editor, window, cx| {
2204 editor.set_vertical_scroll_margin(2, cx);
2205 editor
2206 .style()
2207 .unwrap()
2208 .text
2209 .line_height_in_pixels(window.rem_size())
2210 });
2211 let window = cx.window;
2212 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2213
2214 cx.set_state(
2215 r#"ˇone
2216 two
2217 three
2218 four
2219 five
2220 six
2221 seven
2222 eight
2223 nine
2224 ten
2225 "#,
2226 );
2227 cx.update_editor(|editor, window, cx| {
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 0.0)
2231 );
2232 });
2233
2234 // Add a cursor below the visible area. Since both cursors cannot fit
2235 // on screen, the editor autoscrolls to reveal the newest cursor, and
2236 // allows the vertical scroll margin below that cursor.
2237 cx.update_editor(|editor, window, cx| {
2238 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2239 selections.select_ranges([
2240 Point::new(0, 0)..Point::new(0, 0),
2241 Point::new(6, 0)..Point::new(6, 0),
2242 ]);
2243 })
2244 });
2245 cx.update_editor(|editor, window, cx| {
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.0)
2249 );
2250 });
2251
2252 // Move down. The editor cursor scrolls down to track the newest cursor.
2253 cx.update_editor(|editor, window, cx| {
2254 editor.move_down(&Default::default(), window, cx);
2255 });
2256 cx.update_editor(|editor, window, cx| {
2257 assert_eq!(
2258 editor.snapshot(window, cx).scroll_position(),
2259 gpui::Point::new(0., 4.0)
2260 );
2261 });
2262
2263 // Add a cursor above the visible area. Since both cursors fit on screen,
2264 // the editor scrolls to show both.
2265 cx.update_editor(|editor, window, cx| {
2266 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2267 selections.select_ranges([
2268 Point::new(1, 0)..Point::new(1, 0),
2269 Point::new(6, 0)..Point::new(6, 0),
2270 ]);
2271 })
2272 });
2273 cx.update_editor(|editor, window, cx| {
2274 assert_eq!(
2275 editor.snapshot(window, cx).scroll_position(),
2276 gpui::Point::new(0., 1.0)
2277 );
2278 });
2279}
2280
2281#[gpui::test]
2282async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2283 init_test(cx, |_| {});
2284 let mut cx = EditorTestContext::new(cx).await;
2285
2286 let line_height = cx.editor(|editor, window, _cx| {
2287 editor
2288 .style()
2289 .unwrap()
2290 .text
2291 .line_height_in_pixels(window.rem_size())
2292 });
2293 let window = cx.window;
2294 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2295 cx.set_state(
2296 &r#"
2297 ˇone
2298 two
2299 threeˇ
2300 four
2301 five
2302 six
2303 seven
2304 eight
2305 nine
2306 ten
2307 "#
2308 .unindent(),
2309 );
2310
2311 cx.update_editor(|editor, window, cx| {
2312 editor.move_page_down(&MovePageDown::default(), window, cx)
2313 });
2314 cx.assert_editor_state(
2315 &r#"
2316 one
2317 two
2318 three
2319 ˇfour
2320 five
2321 sixˇ
2322 seven
2323 eight
2324 nine
2325 ten
2326 "#
2327 .unindent(),
2328 );
2329
2330 cx.update_editor(|editor, window, cx| {
2331 editor.move_page_down(&MovePageDown::default(), window, cx)
2332 });
2333 cx.assert_editor_state(
2334 &r#"
2335 one
2336 two
2337 three
2338 four
2339 five
2340 six
2341 ˇseven
2342 eight
2343 nineˇ
2344 ten
2345 "#
2346 .unindent(),
2347 );
2348
2349 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2350 cx.assert_editor_state(
2351 &r#"
2352 one
2353 two
2354 three
2355 ˇfour
2356 five
2357 sixˇ
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2367 cx.assert_editor_state(
2368 &r#"
2369 ˇone
2370 two
2371 threeˇ
2372 four
2373 five
2374 six
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 // Test select collapsing
2384 cx.update_editor(|editor, window, cx| {
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 editor.move_page_down(&MovePageDown::default(), window, cx);
2388 });
2389 cx.assert_editor_state(
2390 &r#"
2391 one
2392 two
2393 three
2394 four
2395 five
2396 six
2397 seven
2398 eight
2399 nine
2400 ˇten
2401 ˇ"#
2402 .unindent(),
2403 );
2404}
2405
2406#[gpui::test]
2407async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2408 init_test(cx, |_| {});
2409 let mut cx = EditorTestContext::new(cx).await;
2410 cx.set_state("one «two threeˇ» four");
2411 cx.update_editor(|editor, window, cx| {
2412 editor.delete_to_beginning_of_line(
2413 &DeleteToBeginningOfLine {
2414 stop_at_indent: false,
2415 },
2416 window,
2417 cx,
2418 );
2419 assert_eq!(editor.text(cx), " four");
2420 });
2421}
2422
2423#[gpui::test]
2424fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2425 init_test(cx, |_| {});
2426
2427 let editor = cx.add_window(|window, cx| {
2428 let buffer = MultiBuffer::build_simple("one two three four", cx);
2429 build_editor(buffer.clone(), window, cx)
2430 });
2431
2432 _ = editor.update(cx, |editor, window, cx| {
2433 editor.change_selections(None, window, cx, |s| {
2434 s.select_display_ranges([
2435 // an empty selection - the preceding word fragment is deleted
2436 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2437 // characters selected - they are deleted
2438 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2439 ])
2440 });
2441 editor.delete_to_previous_word_start(
2442 &DeleteToPreviousWordStart {
2443 ignore_newlines: false,
2444 },
2445 window,
2446 cx,
2447 );
2448 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2449 });
2450
2451 _ = editor.update(cx, |editor, window, cx| {
2452 editor.change_selections(None, window, cx, |s| {
2453 s.select_display_ranges([
2454 // an empty selection - the following word fragment is deleted
2455 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2456 // characters selected - they are deleted
2457 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2458 ])
2459 });
2460 editor.delete_to_next_word_end(
2461 &DeleteToNextWordEnd {
2462 ignore_newlines: false,
2463 },
2464 window,
2465 cx,
2466 );
2467 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2468 });
2469}
2470
2471#[gpui::test]
2472fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2473 init_test(cx, |_| {});
2474
2475 let editor = cx.add_window(|window, cx| {
2476 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2477 build_editor(buffer.clone(), window, cx)
2478 });
2479 let del_to_prev_word_start = DeleteToPreviousWordStart {
2480 ignore_newlines: false,
2481 };
2482 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2483 ignore_newlines: true,
2484 };
2485
2486 _ = editor.update(cx, |editor, window, cx| {
2487 editor.change_selections(None, window, cx, |s| {
2488 s.select_display_ranges([
2489 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2490 ])
2491 });
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2504 });
2505}
2506
2507#[gpui::test]
2508fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2509 init_test(cx, |_| {});
2510
2511 let editor = cx.add_window(|window, cx| {
2512 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2513 build_editor(buffer.clone(), window, cx)
2514 });
2515 let del_to_next_word_end = DeleteToNextWordEnd {
2516 ignore_newlines: false,
2517 };
2518 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2519 ignore_newlines: true,
2520 };
2521
2522 _ = editor.update(cx, |editor, window, cx| {
2523 editor.change_selections(None, window, cx, |s| {
2524 s.select_display_ranges([
2525 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2526 ])
2527 });
2528 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2529 assert_eq!(
2530 editor.buffer.read(cx).read(cx).text(),
2531 "one\n two\nthree\n four"
2532 );
2533 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2534 assert_eq!(
2535 editor.buffer.read(cx).read(cx).text(),
2536 "\n two\nthree\n four"
2537 );
2538 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2539 assert_eq!(
2540 editor.buffer.read(cx).read(cx).text(),
2541 "two\nthree\n four"
2542 );
2543 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2547 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2549 });
2550}
2551
2552#[gpui::test]
2553fn test_newline(cx: &mut TestAppContext) {
2554 init_test(cx, |_| {});
2555
2556 let editor = cx.add_window(|window, cx| {
2557 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2558 build_editor(buffer.clone(), window, cx)
2559 });
2560
2561 _ = editor.update(cx, |editor, window, cx| {
2562 editor.change_selections(None, window, cx, |s| {
2563 s.select_display_ranges([
2564 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2566 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2567 ])
2568 });
2569
2570 editor.newline(&Newline, window, cx);
2571 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2572 });
2573}
2574
2575#[gpui::test]
2576fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578
2579 let editor = cx.add_window(|window, cx| {
2580 let buffer = MultiBuffer::build_simple(
2581 "
2582 a
2583 b(
2584 X
2585 )
2586 c(
2587 X
2588 )
2589 "
2590 .unindent()
2591 .as_str(),
2592 cx,
2593 );
2594 let mut editor = build_editor(buffer.clone(), window, cx);
2595 editor.change_selections(None, window, cx, |s| {
2596 s.select_ranges([
2597 Point::new(2, 4)..Point::new(2, 5),
2598 Point::new(5, 4)..Point::new(5, 5),
2599 ])
2600 });
2601 editor
2602 });
2603
2604 _ = editor.update(cx, |editor, window, cx| {
2605 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2606 editor.buffer.update(cx, |buffer, cx| {
2607 buffer.edit(
2608 [
2609 (Point::new(1, 2)..Point::new(3, 0), ""),
2610 (Point::new(4, 2)..Point::new(6, 0), ""),
2611 ],
2612 None,
2613 cx,
2614 );
2615 assert_eq!(
2616 buffer.read(cx).text(),
2617 "
2618 a
2619 b()
2620 c()
2621 "
2622 .unindent()
2623 );
2624 });
2625 assert_eq!(
2626 editor.selections.ranges(cx),
2627 &[
2628 Point::new(1, 2)..Point::new(1, 2),
2629 Point::new(2, 2)..Point::new(2, 2),
2630 ],
2631 );
2632
2633 editor.newline(&Newline, window, cx);
2634 assert_eq!(
2635 editor.text(cx),
2636 "
2637 a
2638 b(
2639 )
2640 c(
2641 )
2642 "
2643 .unindent()
2644 );
2645
2646 // The selections are moved after the inserted newlines
2647 assert_eq!(
2648 editor.selections.ranges(cx),
2649 &[
2650 Point::new(2, 0)..Point::new(2, 0),
2651 Point::new(4, 0)..Point::new(4, 0),
2652 ],
2653 );
2654 });
2655}
2656
2657#[gpui::test]
2658async fn test_newline_above(cx: &mut TestAppContext) {
2659 init_test(cx, |settings| {
2660 settings.defaults.tab_size = NonZeroU32::new(4)
2661 });
2662
2663 let language = Arc::new(
2664 Language::new(
2665 LanguageConfig::default(),
2666 Some(tree_sitter_rust::LANGUAGE.into()),
2667 )
2668 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2669 .unwrap(),
2670 );
2671
2672 let mut cx = EditorTestContext::new(cx).await;
2673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2674 cx.set_state(indoc! {"
2675 const a: ˇA = (
2676 (ˇ
2677 «const_functionˇ»(ˇ),
2678 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2679 )ˇ
2680 ˇ);ˇ
2681 "});
2682
2683 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2684 cx.assert_editor_state(indoc! {"
2685 ˇ
2686 const a: A = (
2687 ˇ
2688 (
2689 ˇ
2690 ˇ
2691 const_function(),
2692 ˇ
2693 ˇ
2694 ˇ
2695 ˇ
2696 something_else,
2697 ˇ
2698 )
2699 ˇ
2700 ˇ
2701 );
2702 "});
2703}
2704
2705#[gpui::test]
2706async fn test_newline_below(cx: &mut TestAppContext) {
2707 init_test(cx, |settings| {
2708 settings.defaults.tab_size = NonZeroU32::new(4)
2709 });
2710
2711 let language = Arc::new(
2712 Language::new(
2713 LanguageConfig::default(),
2714 Some(tree_sitter_rust::LANGUAGE.into()),
2715 )
2716 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2717 .unwrap(),
2718 );
2719
2720 let mut cx = EditorTestContext::new(cx).await;
2721 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2722 cx.set_state(indoc! {"
2723 const a: ˇA = (
2724 (ˇ
2725 «const_functionˇ»(ˇ),
2726 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2727 )ˇ
2728 ˇ);ˇ
2729 "});
2730
2731 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2732 cx.assert_editor_state(indoc! {"
2733 const a: A = (
2734 ˇ
2735 (
2736 ˇ
2737 const_function(),
2738 ˇ
2739 ˇ
2740 something_else,
2741 ˇ
2742 ˇ
2743 ˇ
2744 ˇ
2745 )
2746 ˇ
2747 );
2748 ˇ
2749 ˇ
2750 "});
2751}
2752
2753#[gpui::test]
2754async fn test_newline_comments(cx: &mut TestAppContext) {
2755 init_test(cx, |settings| {
2756 settings.defaults.tab_size = NonZeroU32::new(4)
2757 });
2758
2759 let language = Arc::new(Language::new(
2760 LanguageConfig {
2761 line_comments: vec!["// ".into()],
2762 ..LanguageConfig::default()
2763 },
2764 None,
2765 ));
2766 {
2767 let mut cx = EditorTestContext::new(cx).await;
2768 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2769 cx.set_state(indoc! {"
2770 // Fooˇ
2771 "});
2772
2773 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2774 cx.assert_editor_state(indoc! {"
2775 // Foo
2776 // ˇ
2777 "});
2778 // Ensure that we add comment prefix when existing line contains space
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(
2781 indoc! {"
2782 // Foo
2783 //s
2784 // ˇ
2785 "}
2786 .replace("s", " ") // s is used as space placeholder to prevent format on save
2787 .as_str(),
2788 );
2789 // Ensure that we add comment prefix when existing line does not contain space
2790 cx.set_state(indoc! {"
2791 // Foo
2792 //ˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 //
2798 // ˇ
2799 "});
2800 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2801 cx.set_state(indoc! {"
2802 ˇ// Foo
2803 "});
2804 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2805 cx.assert_editor_state(indoc! {"
2806
2807 ˇ// Foo
2808 "});
2809 }
2810 // Ensure that comment continuations can be disabled.
2811 update_test_language_settings(cx, |settings| {
2812 settings.defaults.extend_comment_on_newline = Some(false);
2813 });
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.set_state(indoc! {"
2816 // Fooˇ
2817 "});
2818 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2819 cx.assert_editor_state(indoc! {"
2820 // Foo
2821 ˇ
2822 "});
2823}
2824
2825#[gpui::test]
2826async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2827 init_test(cx, |settings| {
2828 settings.defaults.tab_size = NonZeroU32::new(4)
2829 });
2830
2831 let language = Arc::new(Language::new(
2832 LanguageConfig {
2833 line_comments: vec!["// ".into(), "/// ".into()],
2834 ..LanguageConfig::default()
2835 },
2836 None,
2837 ));
2838 {
2839 let mut cx = EditorTestContext::new(cx).await;
2840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2841 cx.set_state(indoc! {"
2842 //ˇ
2843 "});
2844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 //
2847 // ˇ
2848 "});
2849
2850 cx.set_state(indoc! {"
2851 ///ˇ
2852 "});
2853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 ///
2856 /// ˇ
2857 "});
2858 }
2859}
2860
2861#[gpui::test]
2862async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2863 init_test(cx, |settings| {
2864 settings.defaults.tab_size = NonZeroU32::new(4)
2865 });
2866
2867 let language = Arc::new(
2868 Language::new(
2869 LanguageConfig {
2870 documentation: Some(language::DocumentationConfig {
2871 start: "/**".into(),
2872 end: "*/".into(),
2873 prefix: "* ".into(),
2874 tab_size: NonZeroU32::new(1).unwrap(),
2875 }),
2876
2877 ..LanguageConfig::default()
2878 },
2879 Some(tree_sitter_rust::LANGUAGE.into()),
2880 )
2881 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2882 .unwrap(),
2883 );
2884
2885 {
2886 let mut cx = EditorTestContext::new(cx).await;
2887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2888 cx.set_state(indoc! {"
2889 /**ˇ
2890 "});
2891
2892 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2893 cx.assert_editor_state(indoc! {"
2894 /**
2895 * ˇ
2896 "});
2897 // Ensure that if cursor is before the comment start,
2898 // we do not actually insert a comment prefix.
2899 cx.set_state(indoc! {"
2900 ˇ/**
2901 "});
2902 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2903 cx.assert_editor_state(indoc! {"
2904
2905 ˇ/**
2906 "});
2907 // Ensure that if cursor is between it doesn't add comment prefix.
2908 cx.set_state(indoc! {"
2909 /*ˇ*
2910 "});
2911 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2912 cx.assert_editor_state(indoc! {"
2913 /*
2914 ˇ*
2915 "});
2916 // Ensure that if suffix exists on same line after cursor it adds new line.
2917 cx.set_state(indoc! {"
2918 /**ˇ*/
2919 "});
2920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2921 cx.assert_editor_state(indoc! {"
2922 /**
2923 * ˇ
2924 */
2925 "});
2926 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2927 cx.set_state(indoc! {"
2928 /**ˇ */
2929 "});
2930 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 /**
2933 * ˇ
2934 */
2935 "});
2936 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2937 cx.set_state(indoc! {"
2938 /** ˇ*/
2939 "});
2940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2941 cx.assert_editor_state(
2942 indoc! {"
2943 /**s
2944 * ˇ
2945 */
2946 "}
2947 .replace("s", " ") // s is used as space placeholder to prevent format on save
2948 .as_str(),
2949 );
2950 // Ensure that delimiter space is preserved when newline on already
2951 // spaced delimiter.
2952 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2953 cx.assert_editor_state(
2954 indoc! {"
2955 /**s
2956 *s
2957 * ˇ
2958 */
2959 "}
2960 .replace("s", " ") // s is used as space placeholder to prevent format on save
2961 .as_str(),
2962 );
2963 // Ensure that delimiter space is preserved when space is not
2964 // on existing delimiter.
2965 cx.set_state(indoc! {"
2966 /**
2967 *ˇ
2968 */
2969 "});
2970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 /**
2973 *
2974 * ˇ
2975 */
2976 "});
2977 // Ensure that if suffix exists on same line after cursor it
2978 // doesn't add extra new line if prefix is not on same line.
2979 cx.set_state(indoc! {"
2980 /**
2981 ˇ*/
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986
2987 ˇ*/
2988 "});
2989 // Ensure that it detects suffix after existing prefix.
2990 cx.set_state(indoc! {"
2991 /**ˇ/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 /**
2996 ˇ/
2997 "});
2998 // Ensure that if suffix exists on same line before
2999 // cursor it does not add comment prefix.
3000 cx.set_state(indoc! {"
3001 /** */ˇ
3002 "});
3003 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3004 cx.assert_editor_state(indoc! {"
3005 /** */
3006 ˇ
3007 "});
3008 // Ensure that if suffix exists on same line before
3009 // cursor it does not add comment prefix.
3010 cx.set_state(indoc! {"
3011 /**
3012 *
3013 */ˇ
3014 "});
3015 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3016 cx.assert_editor_state(indoc! {"
3017 /**
3018 *
3019 */
3020 ˇ
3021 "});
3022
3023 // Ensure that inline comment followed by code
3024 // doesn't add comment prefix on newline
3025 cx.set_state(indoc! {"
3026 /** */ textˇ
3027 "});
3028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3029 cx.assert_editor_state(indoc! {"
3030 /** */ text
3031 ˇ
3032 "});
3033
3034 // Ensure that text after comment end tag
3035 // doesn't add comment prefix on newline
3036 cx.set_state(indoc! {"
3037 /**
3038 *
3039 */ˇtext
3040 "});
3041 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3042 cx.assert_editor_state(indoc! {"
3043 /**
3044 *
3045 */
3046 ˇtext
3047 "});
3048
3049 // Ensure if not comment block it doesn't
3050 // add comment prefix on newline
3051 cx.set_state(indoc! {"
3052 * textˇ
3053 "});
3054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 * text
3057 ˇ
3058 "});
3059 }
3060 // Ensure that comment continuations can be disabled.
3061 update_test_language_settings(cx, |settings| {
3062 settings.defaults.extend_comment_on_newline = Some(false);
3063 });
3064 let mut cx = EditorTestContext::new(cx).await;
3065 cx.set_state(indoc! {"
3066 /**ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 ˇ
3072 "});
3073}
3074
3075#[gpui::test]
3076fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3077 init_test(cx, |_| {});
3078
3079 let editor = cx.add_window(|window, cx| {
3080 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3081 let mut editor = build_editor(buffer.clone(), window, cx);
3082 editor.change_selections(None, window, cx, |s| {
3083 s.select_ranges([3..4, 11..12, 19..20])
3084 });
3085 editor
3086 });
3087
3088 _ = editor.update(cx, |editor, window, cx| {
3089 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3090 editor.buffer.update(cx, |buffer, cx| {
3091 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3092 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3093 });
3094 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3095
3096 editor.insert("Z", window, cx);
3097 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3098
3099 // The selections are moved after the inserted characters
3100 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_tab(cx: &mut TestAppContext) {
3106 init_test(cx, |settings| {
3107 settings.defaults.tab_size = NonZeroU32::new(3)
3108 });
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 cx.set_state(indoc! {"
3112 ˇabˇc
3113 ˇ🏀ˇ🏀ˇefg
3114 dˇ
3115 "});
3116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 ˇab ˇc
3119 ˇ🏀 ˇ🏀 ˇefg
3120 d ˇ
3121 "});
3122
3123 cx.set_state(indoc! {"
3124 a
3125 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 a
3130 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3131 "});
3132}
3133
3134#[gpui::test]
3135async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3136 init_test(cx, |_| {});
3137
3138 let mut cx = EditorTestContext::new(cx).await;
3139 let language = Arc::new(
3140 Language::new(
3141 LanguageConfig::default(),
3142 Some(tree_sitter_rust::LANGUAGE.into()),
3143 )
3144 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3145 .unwrap(),
3146 );
3147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3148
3149 // test when all cursors are not at suggested indent
3150 // then simply move to their suggested indent location
3151 cx.set_state(indoc! {"
3152 const a: B = (
3153 c(
3154 ˇ
3155 ˇ )
3156 );
3157 "});
3158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3159 cx.assert_editor_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ)
3164 );
3165 "});
3166
3167 // test cursor already at suggested indent not moving when
3168 // other cursors are yet to reach their suggested indents
3169 cx.set_state(indoc! {"
3170 ˇ
3171 const a: B = (
3172 c(
3173 d(
3174 ˇ
3175 )
3176 ˇ
3177 ˇ )
3178 );
3179 "});
3180 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 ˇ
3183 const a: B = (
3184 c(
3185 d(
3186 ˇ
3187 )
3188 ˇ
3189 ˇ)
3190 );
3191 "});
3192 // test when all cursors are at suggested indent then tab is inserted
3193 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195 ˇ
3196 const a: B = (
3197 c(
3198 d(
3199 ˇ
3200 )
3201 ˇ
3202 ˇ)
3203 );
3204 "});
3205
3206 // test when current indent is less than suggested indent,
3207 // we adjust line to match suggested indent and move cursor to it
3208 //
3209 // when no other cursor is at word boundary, all of them should move
3210 cx.set_state(indoc! {"
3211 const a: B = (
3212 c(
3213 d(
3214 ˇ
3215 ˇ )
3216 ˇ )
3217 );
3218 "});
3219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 const a: B = (
3222 c(
3223 d(
3224 ˇ
3225 ˇ)
3226 ˇ)
3227 );
3228 "});
3229
3230 // test when current indent is less than suggested indent,
3231 // we adjust line to match suggested indent and move cursor to it
3232 //
3233 // when some other cursor is at word boundary, it should not move
3234 cx.set_state(indoc! {"
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 ˇ )
3240 ˇ)
3241 );
3242 "});
3243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 const a: B = (
3246 c(
3247 d(
3248 ˇ
3249 ˇ)
3250 ˇ)
3251 );
3252 "});
3253
3254 // test when current indent is more than suggested indent,
3255 // we just move cursor to current indent instead of suggested indent
3256 //
3257 // when no other cursor is at word boundary, all of them should move
3258 cx.set_state(indoc! {"
3259 const a: B = (
3260 c(
3261 d(
3262 ˇ
3263 ˇ )
3264 ˇ )
3265 );
3266 "});
3267 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 const a: B = (
3270 c(
3271 d(
3272 ˇ
3273 ˇ)
3274 ˇ)
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 const a: B = (
3280 c(
3281 d(
3282 ˇ
3283 ˇ)
3284 ˇ)
3285 );
3286 "});
3287
3288 // test when current indent is more than suggested indent,
3289 // we just move cursor to current indent instead of suggested indent
3290 //
3291 // when some other cursor is at word boundary, it doesn't move
3292 cx.set_state(indoc! {"
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
3297 ˇ )
3298 ˇ)
3299 );
3300 "});
3301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 const a: B = (
3304 c(
3305 d(
3306 ˇ
3307 ˇ)
3308 ˇ)
3309 );
3310 "});
3311
3312 // handle auto-indent when there are multiple cursors on the same line
3313 cx.set_state(indoc! {"
3314 const a: B = (
3315 c(
3316 ˇ ˇ
3317 ˇ )
3318 );
3319 "});
3320 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3321 cx.assert_editor_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ
3325 ˇ)
3326 );
3327 "});
3328}
3329
3330#[gpui::test]
3331async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3332 init_test(cx, |settings| {
3333 settings.defaults.tab_size = NonZeroU32::new(3)
3334 });
3335
3336 let mut cx = EditorTestContext::new(cx).await;
3337 cx.set_state(indoc! {"
3338 ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t ˇ
3342 \t \t\t \t \t\t \t\t \t \t ˇ
3343 "});
3344
3345 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3346 cx.assert_editor_state(indoc! {"
3347 ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t ˇ
3351 \t \t\t \t \t\t \t\t \t \t ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(
3362 Language::new(
3363 LanguageConfig::default(),
3364 Some(tree_sitter_rust::LANGUAGE.into()),
3365 )
3366 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3367 .unwrap(),
3368 );
3369
3370 let mut cx = EditorTestContext::new(cx).await;
3371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3372 cx.set_state(indoc! {"
3373 fn a() {
3374 if b {
3375 \t ˇc
3376 }
3377 }
3378 "});
3379
3380 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3381 cx.assert_editor_state(indoc! {"
3382 fn a() {
3383 if b {
3384 ˇc
3385 }
3386 }
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_indent_outdent(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4);
3394 });
3395
3396 let mut cx = EditorTestContext::new(cx).await;
3397
3398 cx.set_state(indoc! {"
3399 «oneˇ» «twoˇ»
3400 three
3401 four
3402 "});
3403 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3404 cx.assert_editor_state(indoc! {"
3405 «oneˇ» «twoˇ»
3406 three
3407 four
3408 "});
3409
3410 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 «oneˇ» «twoˇ»
3413 three
3414 four
3415 "});
3416
3417 // select across line ending
3418 cx.set_state(indoc! {"
3419 one two
3420 t«hree
3421 ˇ» four
3422 "});
3423 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 one two
3426 t«hree
3427 ˇ» four
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 one two
3433 t«hree
3434 ˇ» four
3435 "});
3436
3437 // Ensure that indenting/outdenting works when the cursor is at column 0.
3438 cx.set_state(indoc! {"
3439 one two
3440 ˇthree
3441 four
3442 "});
3443 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 one two
3446 ˇthree
3447 four
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 one two
3452 ˇ three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 one two
3458 ˇthree
3459 four
3460 "});
3461}
3462
3463#[gpui::test]
3464async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.hard_tabs = Some(true);
3467 });
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470
3471 // select two ranges on one line
3472 cx.set_state(indoc! {"
3473 «oneˇ» «twoˇ»
3474 three
3475 four
3476 "});
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 \t«oneˇ» «twoˇ»
3480 three
3481 four
3482 "});
3483 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3484 cx.assert_editor_state(indoc! {"
3485 \t\t«oneˇ» «twoˇ»
3486 three
3487 four
3488 "});
3489 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 \t«oneˇ» «twoˇ»
3492 three
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 «oneˇ» «twoˇ»
3498 three
3499 four
3500 "});
3501
3502 // select across a line ending
3503 cx.set_state(indoc! {"
3504 one two
3505 t«hree
3506 ˇ»four
3507 "});
3508 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 one two
3511 \tt«hree
3512 ˇ»four
3513 "});
3514 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3515 cx.assert_editor_state(indoc! {"
3516 one two
3517 \t\tt«hree
3518 ˇ»four
3519 "});
3520 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 \tt«hree
3524 ˇ»four
3525 "});
3526 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3527 cx.assert_editor_state(indoc! {"
3528 one two
3529 t«hree
3530 ˇ»four
3531 "});
3532
3533 // Ensure that indenting/outdenting works when the cursor is at column 0.
3534 cx.set_state(indoc! {"
3535 one two
3536 ˇthree
3537 four
3538 "});
3539 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3540 cx.assert_editor_state(indoc! {"
3541 one two
3542 ˇthree
3543 four
3544 "});
3545 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 one two
3548 \tˇthree
3549 four
3550 "});
3551 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3552 cx.assert_editor_state(indoc! {"
3553 one two
3554 ˇthree
3555 four
3556 "});
3557}
3558
3559#[gpui::test]
3560fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3561 init_test(cx, |settings| {
3562 settings.languages.extend([
3563 (
3564 "TOML".into(),
3565 LanguageSettingsContent {
3566 tab_size: NonZeroU32::new(2),
3567 ..Default::default()
3568 },
3569 ),
3570 (
3571 "Rust".into(),
3572 LanguageSettingsContent {
3573 tab_size: NonZeroU32::new(4),
3574 ..Default::default()
3575 },
3576 ),
3577 ]);
3578 });
3579
3580 let toml_language = Arc::new(Language::new(
3581 LanguageConfig {
3582 name: "TOML".into(),
3583 ..Default::default()
3584 },
3585 None,
3586 ));
3587 let rust_language = Arc::new(Language::new(
3588 LanguageConfig {
3589 name: "Rust".into(),
3590 ..Default::default()
3591 },
3592 None,
3593 ));
3594
3595 let toml_buffer =
3596 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3597 let rust_buffer =
3598 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3599 let multibuffer = cx.new(|cx| {
3600 let mut multibuffer = MultiBuffer::new(ReadWrite);
3601 multibuffer.push_excerpts(
3602 toml_buffer.clone(),
3603 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3604 cx,
3605 );
3606 multibuffer.push_excerpts(
3607 rust_buffer.clone(),
3608 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3609 cx,
3610 );
3611 multibuffer
3612 });
3613
3614 cx.add_window(|window, cx| {
3615 let mut editor = build_editor(multibuffer, window, cx);
3616
3617 assert_eq!(
3618 editor.text(cx),
3619 indoc! {"
3620 a = 1
3621 b = 2
3622
3623 const c: usize = 3;
3624 "}
3625 );
3626
3627 select_ranges(
3628 &mut editor,
3629 indoc! {"
3630 «aˇ» = 1
3631 b = 2
3632
3633 «const c:ˇ» usize = 3;
3634 "},
3635 window,
3636 cx,
3637 );
3638
3639 editor.tab(&Tab, window, cx);
3640 assert_text_with_selections(
3641 &mut editor,
3642 indoc! {"
3643 «aˇ» = 1
3644 b = 2
3645
3646 «const c:ˇ» usize = 3;
3647 "},
3648 cx,
3649 );
3650 editor.backtab(&Backtab, window, cx);
3651 assert_text_with_selections(
3652 &mut editor,
3653 indoc! {"
3654 «aˇ» = 1
3655 b = 2
3656
3657 «const c:ˇ» usize = 3;
3658 "},
3659 cx,
3660 );
3661
3662 editor
3663 });
3664}
3665
3666#[gpui::test]
3667async fn test_backspace(cx: &mut TestAppContext) {
3668 init_test(cx, |_| {});
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671
3672 // Basic backspace
3673 cx.set_state(indoc! {"
3674 onˇe two three
3675 fou«rˇ» five six
3676 seven «ˇeight nine
3677 »ten
3678 "});
3679 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3680 cx.assert_editor_state(indoc! {"
3681 oˇe two three
3682 fouˇ five six
3683 seven ˇten
3684 "});
3685
3686 // Test backspace inside and around indents
3687 cx.set_state(indoc! {"
3688 zero
3689 ˇone
3690 ˇtwo
3691 ˇ ˇ ˇ three
3692 ˇ ˇ four
3693 "});
3694 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3695 cx.assert_editor_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ threeˇ four
3700 "});
3701}
3702
3703#[gpui::test]
3704async fn test_delete(cx: &mut TestAppContext) {
3705 init_test(cx, |_| {});
3706
3707 let mut cx = EditorTestContext::new(cx).await;
3708 cx.set_state(indoc! {"
3709 onˇe two three
3710 fou«rˇ» five six
3711 seven «ˇeight nine
3712 »ten
3713 "});
3714 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 onˇ two three
3717 fouˇ five six
3718 seven ˇten
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_delete_line(cx: &mut TestAppContext) {
3724 init_test(cx, |_| {});
3725
3726 let editor = cx.add_window(|window, cx| {
3727 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3728 build_editor(buffer, window, cx)
3729 });
3730 _ = editor.update(cx, |editor, window, cx| {
3731 editor.change_selections(None, window, cx, |s| {
3732 s.select_display_ranges([
3733 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3736 ])
3737 });
3738 editor.delete_line(&DeleteLine, window, cx);
3739 assert_eq!(editor.display_text(cx), "ghi");
3740 assert_eq!(
3741 editor.selections.display_ranges(cx),
3742 vec![
3743 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3744 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3745 ]
3746 );
3747 });
3748
3749 let editor = cx.add_window(|window, cx| {
3750 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3751 build_editor(buffer, window, cx)
3752 });
3753 _ = editor.update(cx, |editor, window, cx| {
3754 editor.change_selections(None, window, cx, |s| {
3755 s.select_display_ranges([
3756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3757 ])
3758 });
3759 editor.delete_line(&DeleteLine, window, cx);
3760 assert_eq!(editor.display_text(cx), "ghi\n");
3761 assert_eq!(
3762 editor.selections.display_ranges(cx),
3763 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3764 );
3765 });
3766}
3767
3768#[gpui::test]
3769fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3770 init_test(cx, |_| {});
3771
3772 cx.add_window(|window, cx| {
3773 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3774 let mut editor = build_editor(buffer.clone(), window, cx);
3775 let buffer = buffer.read(cx).as_singleton().unwrap();
3776
3777 assert_eq!(
3778 editor.selections.ranges::<Point>(cx),
3779 &[Point::new(0, 0)..Point::new(0, 0)]
3780 );
3781
3782 // When on single line, replace newline at end by space
3783 editor.join_lines(&JoinLines, window, cx);
3784 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 3)..Point::new(0, 3)]
3788 );
3789
3790 // When multiple lines are selected, remove newlines that are spanned by the selection
3791 editor.change_selections(None, window, cx, |s| {
3792 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3793 });
3794 editor.join_lines(&JoinLines, window, cx);
3795 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3796 assert_eq!(
3797 editor.selections.ranges::<Point>(cx),
3798 &[Point::new(0, 11)..Point::new(0, 11)]
3799 );
3800
3801 // Undo should be transactional
3802 editor.undo(&Undo, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 5)..Point::new(2, 2)]
3807 );
3808
3809 // When joining an empty line don't insert a space
3810 editor.change_selections(None, window, cx, |s| {
3811 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3812 });
3813 editor.join_lines(&JoinLines, window, cx);
3814 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3815 assert_eq!(
3816 editor.selections.ranges::<Point>(cx),
3817 [Point::new(2, 3)..Point::new(2, 3)]
3818 );
3819
3820 // We can remove trailing newlines
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We don't blow up on the last line
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // reset to test indentation
3837 editor.buffer.update(cx, |buffer, cx| {
3838 buffer.edit(
3839 [
3840 (Point::new(1, 0)..Point::new(1, 2), " "),
3841 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3842 ],
3843 None,
3844 cx,
3845 )
3846 });
3847
3848 // We remove any leading spaces
3849 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3850 editor.change_selections(None, window, cx, |s| {
3851 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3852 });
3853 editor.join_lines(&JoinLines, window, cx);
3854 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3855
3856 // We don't insert a space for a line containing only spaces
3857 editor.join_lines(&JoinLines, window, cx);
3858 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3859
3860 // We ignore any leading tabs
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3863
3864 editor
3865 });
3866}
3867
3868#[gpui::test]
3869fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3870 init_test(cx, |_| {});
3871
3872 cx.add_window(|window, cx| {
3873 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3874 let mut editor = build_editor(buffer.clone(), window, cx);
3875 let buffer = buffer.read(cx).as_singleton().unwrap();
3876
3877 editor.change_selections(None, window, cx, |s| {
3878 s.select_ranges([
3879 Point::new(0, 2)..Point::new(1, 1),
3880 Point::new(1, 2)..Point::new(1, 2),
3881 Point::new(3, 1)..Point::new(3, 2),
3882 ])
3883 });
3884
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 [
3891 Point::new(0, 7)..Point::new(0, 7),
3892 Point::new(1, 3)..Point::new(1, 3)
3893 ]
3894 );
3895 editor
3896 });
3897}
3898
3899#[gpui::test]
3900async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3901 init_test(cx, |_| {});
3902
3903 let mut cx = EditorTestContext::new(cx).await;
3904
3905 let diff_base = r#"
3906 Line 0
3907 Line 1
3908 Line 2
3909 Line 3
3910 "#
3911 .unindent();
3912
3913 cx.set_state(
3914 &r#"
3915 ˇLine 0
3916 Line 1
3917 Line 2
3918 Line 3
3919 "#
3920 .unindent(),
3921 );
3922
3923 cx.set_head_text(&diff_base);
3924 executor.run_until_parked();
3925
3926 // Join lines
3927 cx.update_editor(|editor, window, cx| {
3928 editor.join_lines(&JoinLines, window, cx);
3929 });
3930 executor.run_until_parked();
3931
3932 cx.assert_editor_state(
3933 &r#"
3934 Line 0ˇ Line 1
3935 Line 2
3936 Line 3
3937 "#
3938 .unindent(),
3939 );
3940 // Join again
3941 cx.update_editor(|editor, window, cx| {
3942 editor.join_lines(&JoinLines, window, cx);
3943 });
3944 executor.run_until_parked();
3945
3946 cx.assert_editor_state(
3947 &r#"
3948 Line 0 Line 1ˇ Line 2
3949 Line 3
3950 "#
3951 .unindent(),
3952 );
3953}
3954
3955#[gpui::test]
3956async fn test_custom_newlines_cause_no_false_positive_diffs(
3957 executor: BackgroundExecutor,
3958 cx: &mut TestAppContext,
3959) {
3960 init_test(cx, |_| {});
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3963 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3964 executor.run_until_parked();
3965
3966 cx.update_editor(|editor, window, cx| {
3967 let snapshot = editor.snapshot(window, cx);
3968 assert_eq!(
3969 snapshot
3970 .buffer_snapshot
3971 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3972 .collect::<Vec<_>>(),
3973 Vec::new(),
3974 "Should not have any diffs for files with custom newlines"
3975 );
3976 });
3977}
3978
3979#[gpui::test]
3980async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 let mut cx = EditorTestContext::new(cx).await;
3984
3985 // Test sort_lines_case_insensitive()
3986 cx.set_state(indoc! {"
3987 «z
3988 y
3989 x
3990 Z
3991 Y
3992 Xˇ»
3993 "});
3994 cx.update_editor(|e, window, cx| {
3995 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3996 });
3997 cx.assert_editor_state(indoc! {"
3998 «x
3999 X
4000 y
4001 Y
4002 z
4003 Zˇ»
4004 "});
4005
4006 // Test reverse_lines()
4007 cx.set_state(indoc! {"
4008 «5
4009 4
4010 3
4011 2
4012 1ˇ»
4013 "});
4014 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4015 cx.assert_editor_state(indoc! {"
4016 «1
4017 2
4018 3
4019 4
4020 5ˇ»
4021 "});
4022
4023 // Skip testing shuffle_line()
4024
4025 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4026 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4027
4028 // Don't manipulate when cursor is on single line, but expand the selection
4029 cx.set_state(indoc! {"
4030 ddˇdd
4031 ccc
4032 bb
4033 a
4034 "});
4035 cx.update_editor(|e, window, cx| {
4036 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4037 });
4038 cx.assert_editor_state(indoc! {"
4039 «ddddˇ»
4040 ccc
4041 bb
4042 a
4043 "});
4044
4045 // Basic manipulate case
4046 // Start selection moves to column 0
4047 // End of selection shrinks to fit shorter line
4048 cx.set_state(indoc! {"
4049 dd«d
4050 ccc
4051 bb
4052 aaaaaˇ»
4053 "});
4054 cx.update_editor(|e, window, cx| {
4055 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4056 });
4057 cx.assert_editor_state(indoc! {"
4058 «aaaaa
4059 bb
4060 ccc
4061 dddˇ»
4062 "});
4063
4064 // Manipulate case with newlines
4065 cx.set_state(indoc! {"
4066 dd«d
4067 ccc
4068
4069 bb
4070 aaaaa
4071
4072 ˇ»
4073 "});
4074 cx.update_editor(|e, window, cx| {
4075 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4076 });
4077 cx.assert_editor_state(indoc! {"
4078 «
4079
4080 aaaaa
4081 bb
4082 ccc
4083 dddˇ»
4084
4085 "});
4086
4087 // Adding new line
4088 cx.set_state(indoc! {"
4089 aa«a
4090 bbˇ»b
4091 "});
4092 cx.update_editor(|e, window, cx| {
4093 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4094 });
4095 cx.assert_editor_state(indoc! {"
4096 «aaa
4097 bbb
4098 added_lineˇ»
4099 "});
4100
4101 // Removing line
4102 cx.set_state(indoc! {"
4103 aa«a
4104 bbbˇ»
4105 "});
4106 cx.update_editor(|e, window, cx| {
4107 e.manipulate_lines(window, cx, |lines| {
4108 lines.pop();
4109 })
4110 });
4111 cx.assert_editor_state(indoc! {"
4112 «aaaˇ»
4113 "});
4114
4115 // Removing all lines
4116 cx.set_state(indoc! {"
4117 aa«a
4118 bbbˇ»
4119 "});
4120 cx.update_editor(|e, window, cx| {
4121 e.manipulate_lines(window, cx, |lines| {
4122 lines.drain(..);
4123 })
4124 });
4125 cx.assert_editor_state(indoc! {"
4126 ˇ
4127 "});
4128}
4129
4130#[gpui::test]
4131async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4132 init_test(cx, |_| {});
4133
4134 let mut cx = EditorTestContext::new(cx).await;
4135
4136 // Consider continuous selection as single selection
4137 cx.set_state(indoc! {"
4138 Aaa«aa
4139 cˇ»c«c
4140 bb
4141 aaaˇ»aa
4142 "});
4143 cx.update_editor(|e, window, cx| {
4144 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4145 });
4146 cx.assert_editor_state(indoc! {"
4147 «Aaaaa
4148 ccc
4149 bb
4150 aaaaaˇ»
4151 "});
4152
4153 cx.set_state(indoc! {"
4154 Aaa«aa
4155 cˇ»c«c
4156 bb
4157 aaaˇ»aa
4158 "});
4159 cx.update_editor(|e, window, cx| {
4160 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4161 });
4162 cx.assert_editor_state(indoc! {"
4163 «Aaaaa
4164 ccc
4165 bbˇ»
4166 "});
4167
4168 // Consider non continuous selection as distinct dedup operations
4169 cx.set_state(indoc! {"
4170 «aaaaa
4171 bb
4172 aaaaa
4173 aaaaaˇ»
4174
4175 aaa«aaˇ»
4176 "});
4177 cx.update_editor(|e, window, cx| {
4178 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4179 });
4180 cx.assert_editor_state(indoc! {"
4181 «aaaaa
4182 bbˇ»
4183
4184 «aaaaaˇ»
4185 "});
4186}
4187
4188#[gpui::test]
4189async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4190 init_test(cx, |_| {});
4191
4192 let mut cx = EditorTestContext::new(cx).await;
4193
4194 cx.set_state(indoc! {"
4195 «Aaa
4196 aAa
4197 Aaaˇ»
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «Aaa
4204 aAaˇ»
4205 "});
4206
4207 cx.set_state(indoc! {"
4208 «Aaa
4209 aAa
4210 aaAˇ»
4211 "});
4212 cx.update_editor(|e, window, cx| {
4213 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4214 });
4215 cx.assert_editor_state(indoc! {"
4216 «Aaaˇ»
4217 "});
4218}
4219
4220#[gpui::test]
4221async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4222 init_test(cx, |_| {});
4223
4224 let mut cx = EditorTestContext::new(cx).await;
4225
4226 // Manipulate with multiple selections on a single line
4227 cx.set_state(indoc! {"
4228 dd«dd
4229 cˇ»c«c
4230 bb
4231 aaaˇ»aa
4232 "});
4233 cx.update_editor(|e, window, cx| {
4234 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4235 });
4236 cx.assert_editor_state(indoc! {"
4237 «aaaaa
4238 bb
4239 ccc
4240 ddddˇ»
4241 "});
4242
4243 // Manipulate with multiple disjoin selections
4244 cx.set_state(indoc! {"
4245 5«
4246 4
4247 3
4248 2
4249 1ˇ»
4250
4251 dd«dd
4252 ccc
4253 bb
4254 aaaˇ»aa
4255 "});
4256 cx.update_editor(|e, window, cx| {
4257 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 «1
4261 2
4262 3
4263 4
4264 5ˇ»
4265
4266 «aaaaa
4267 bb
4268 ccc
4269 ddddˇ»
4270 "});
4271
4272 // Adding lines on each selection
4273 cx.set_state(indoc! {"
4274 2«
4275 1ˇ»
4276
4277 bb«bb
4278 aaaˇ»aa
4279 "});
4280 cx.update_editor(|e, window, cx| {
4281 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4282 });
4283 cx.assert_editor_state(indoc! {"
4284 «2
4285 1
4286 added lineˇ»
4287
4288 «bbbb
4289 aaaaa
4290 added lineˇ»
4291 "});
4292
4293 // Removing lines on each selection
4294 cx.set_state(indoc! {"
4295 2«
4296 1ˇ»
4297
4298 bb«bb
4299 aaaˇ»aa
4300 "});
4301 cx.update_editor(|e, window, cx| {
4302 e.manipulate_lines(window, cx, |lines| {
4303 lines.pop();
4304 })
4305 });
4306 cx.assert_editor_state(indoc! {"
4307 «2ˇ»
4308
4309 «bbbbˇ»
4310 "});
4311}
4312
4313#[gpui::test]
4314async fn test_toggle_case(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let mut cx = EditorTestContext::new(cx).await;
4318
4319 // If all lower case -> upper case
4320 cx.set_state(indoc! {"
4321 «hello worldˇ»
4322 "});
4323 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4324 cx.assert_editor_state(indoc! {"
4325 «HELLO WORLDˇ»
4326 "});
4327
4328 // If all upper case -> lower case
4329 cx.set_state(indoc! {"
4330 «HELLO WORLDˇ»
4331 "});
4332 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4333 cx.assert_editor_state(indoc! {"
4334 «hello worldˇ»
4335 "});
4336
4337 // If any upper case characters are identified -> lower case
4338 // This matches JetBrains IDEs
4339 cx.set_state(indoc! {"
4340 «hEllo worldˇ»
4341 "});
4342 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4343 cx.assert_editor_state(indoc! {"
4344 «hello worldˇ»
4345 "});
4346}
4347
4348#[gpui::test]
4349async fn test_manipulate_text(cx: &mut TestAppContext) {
4350 init_test(cx, |_| {});
4351
4352 let mut cx = EditorTestContext::new(cx).await;
4353
4354 // Test convert_to_upper_case()
4355 cx.set_state(indoc! {"
4356 «hello worldˇ»
4357 "});
4358 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4359 cx.assert_editor_state(indoc! {"
4360 «HELLO WORLDˇ»
4361 "});
4362
4363 // Test convert_to_lower_case()
4364 cx.set_state(indoc! {"
4365 «HELLO WORLDˇ»
4366 "});
4367 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4368 cx.assert_editor_state(indoc! {"
4369 «hello worldˇ»
4370 "});
4371
4372 // Test multiple line, single selection case
4373 cx.set_state(indoc! {"
4374 «The quick brown
4375 fox jumps over
4376 the lazy dogˇ»
4377 "});
4378 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4379 cx.assert_editor_state(indoc! {"
4380 «The Quick Brown
4381 Fox Jumps Over
4382 The Lazy Dogˇ»
4383 "});
4384
4385 // Test multiple line, single selection case
4386 cx.set_state(indoc! {"
4387 «The quick brown
4388 fox jumps over
4389 the lazy dogˇ»
4390 "});
4391 cx.update_editor(|e, window, cx| {
4392 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4393 });
4394 cx.assert_editor_state(indoc! {"
4395 «TheQuickBrown
4396 FoxJumpsOver
4397 TheLazyDogˇ»
4398 "});
4399
4400 // From here on out, test more complex cases of manipulate_text()
4401
4402 // Test no selection case - should affect words cursors are in
4403 // Cursor at beginning, middle, and end of word
4404 cx.set_state(indoc! {"
4405 ˇhello big beauˇtiful worldˇ
4406 "});
4407 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4408 cx.assert_editor_state(indoc! {"
4409 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4410 "});
4411
4412 // Test multiple selections on a single line and across multiple lines
4413 cx.set_state(indoc! {"
4414 «Theˇ» quick «brown
4415 foxˇ» jumps «overˇ»
4416 the «lazyˇ» dog
4417 "});
4418 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4419 cx.assert_editor_state(indoc! {"
4420 «THEˇ» quick «BROWN
4421 FOXˇ» jumps «OVERˇ»
4422 the «LAZYˇ» dog
4423 "});
4424
4425 // Test case where text length grows
4426 cx.set_state(indoc! {"
4427 «tschüߡ»
4428 "});
4429 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4430 cx.assert_editor_state(indoc! {"
4431 «TSCHÜSSˇ»
4432 "});
4433
4434 // Test to make sure we don't crash when text shrinks
4435 cx.set_state(indoc! {"
4436 aaa_bbbˇ
4437 "});
4438 cx.update_editor(|e, window, cx| {
4439 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4440 });
4441 cx.assert_editor_state(indoc! {"
4442 «aaaBbbˇ»
4443 "});
4444
4445 // Test to make sure we all aware of the fact that each word can grow and shrink
4446 // Final selections should be aware of this fact
4447 cx.set_state(indoc! {"
4448 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4449 "});
4450 cx.update_editor(|e, window, cx| {
4451 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4452 });
4453 cx.assert_editor_state(indoc! {"
4454 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4455 "});
4456
4457 cx.set_state(indoc! {"
4458 «hElLo, WoRld!ˇ»
4459 "});
4460 cx.update_editor(|e, window, cx| {
4461 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4462 });
4463 cx.assert_editor_state(indoc! {"
4464 «HeLlO, wOrLD!ˇ»
4465 "});
4466}
4467
4468#[gpui::test]
4469fn test_duplicate_line(cx: &mut TestAppContext) {
4470 init_test(cx, |_| {});
4471
4472 let editor = cx.add_window(|window, cx| {
4473 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4474 build_editor(buffer, window, cx)
4475 });
4476 _ = editor.update(cx, |editor, window, cx| {
4477 editor.change_selections(None, window, cx, |s| {
4478 s.select_display_ranges([
4479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4480 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4481 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4482 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4483 ])
4484 });
4485 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4486 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4487 assert_eq!(
4488 editor.selections.display_ranges(cx),
4489 vec![
4490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4491 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4492 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4493 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4494 ]
4495 );
4496 });
4497
4498 let editor = cx.add_window(|window, cx| {
4499 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4500 build_editor(buffer, window, cx)
4501 });
4502 _ = editor.update(cx, |editor, window, cx| {
4503 editor.change_selections(None, window, cx, |s| {
4504 s.select_display_ranges([
4505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4506 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4507 ])
4508 });
4509 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4510 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4511 assert_eq!(
4512 editor.selections.display_ranges(cx),
4513 vec![
4514 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4515 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4516 ]
4517 );
4518 });
4519
4520 // With `move_upwards` the selections stay in place, except for
4521 // the lines inserted above them
4522 let editor = cx.add_window(|window, cx| {
4523 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4524 build_editor(buffer, window, cx)
4525 });
4526 _ = editor.update(cx, |editor, window, cx| {
4527 editor.change_selections(None, window, cx, |s| {
4528 s.select_display_ranges([
4529 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4530 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4531 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4532 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4533 ])
4534 });
4535 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4536 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4537 assert_eq!(
4538 editor.selections.display_ranges(cx),
4539 vec![
4540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4541 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4542 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4543 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4544 ]
4545 );
4546 });
4547
4548 let editor = cx.add_window(|window, cx| {
4549 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4550 build_editor(buffer, window, cx)
4551 });
4552 _ = editor.update(cx, |editor, window, cx| {
4553 editor.change_selections(None, window, cx, |s| {
4554 s.select_display_ranges([
4555 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4556 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4557 ])
4558 });
4559 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4560 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4561 assert_eq!(
4562 editor.selections.display_ranges(cx),
4563 vec![
4564 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4566 ]
4567 );
4568 });
4569
4570 let editor = cx.add_window(|window, cx| {
4571 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4572 build_editor(buffer, window, cx)
4573 });
4574 _ = editor.update(cx, |editor, window, cx| {
4575 editor.change_selections(None, window, cx, |s| {
4576 s.select_display_ranges([
4577 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4578 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4579 ])
4580 });
4581 editor.duplicate_selection(&DuplicateSelection, window, cx);
4582 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4583 assert_eq!(
4584 editor.selections.display_ranges(cx),
4585 vec![
4586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4587 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4588 ]
4589 );
4590 });
4591}
4592
4593#[gpui::test]
4594fn test_move_line_up_down(cx: &mut TestAppContext) {
4595 init_test(cx, |_| {});
4596
4597 let editor = cx.add_window(|window, cx| {
4598 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4599 build_editor(buffer, window, cx)
4600 });
4601 _ = editor.update(cx, |editor, window, cx| {
4602 editor.fold_creases(
4603 vec![
4604 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4606 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4607 ],
4608 true,
4609 window,
4610 cx,
4611 );
4612 editor.change_selections(None, window, cx, |s| {
4613 s.select_display_ranges([
4614 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4615 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4616 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4617 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4618 ])
4619 });
4620 assert_eq!(
4621 editor.display_text(cx),
4622 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4623 );
4624
4625 editor.move_line_up(&MoveLineUp, window, cx);
4626 assert_eq!(
4627 editor.display_text(cx),
4628 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4629 );
4630 assert_eq!(
4631 editor.selections.display_ranges(cx),
4632 vec![
4633 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4634 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4635 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4636 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4637 ]
4638 );
4639 });
4640
4641 _ = editor.update(cx, |editor, window, cx| {
4642 editor.move_line_down(&MoveLineDown, window, cx);
4643 assert_eq!(
4644 editor.display_text(cx),
4645 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4646 );
4647 assert_eq!(
4648 editor.selections.display_ranges(cx),
4649 vec![
4650 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4651 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4652 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4653 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4654 ]
4655 );
4656 });
4657
4658 _ = editor.update(cx, |editor, window, cx| {
4659 editor.move_line_down(&MoveLineDown, window, cx);
4660 assert_eq!(
4661 editor.display_text(cx),
4662 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4663 );
4664 assert_eq!(
4665 editor.selections.display_ranges(cx),
4666 vec![
4667 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4668 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4669 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4670 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4671 ]
4672 );
4673 });
4674
4675 _ = editor.update(cx, |editor, window, cx| {
4676 editor.move_line_up(&MoveLineUp, window, cx);
4677 assert_eq!(
4678 editor.display_text(cx),
4679 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4680 );
4681 assert_eq!(
4682 editor.selections.display_ranges(cx),
4683 vec![
4684 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4685 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4686 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4687 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4688 ]
4689 );
4690 });
4691}
4692
4693#[gpui::test]
4694fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4695 init_test(cx, |_| {});
4696
4697 let editor = cx.add_window(|window, cx| {
4698 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4699 build_editor(buffer, window, cx)
4700 });
4701 _ = editor.update(cx, |editor, window, cx| {
4702 let snapshot = editor.buffer.read(cx).snapshot(cx);
4703 editor.insert_blocks(
4704 [BlockProperties {
4705 style: BlockStyle::Fixed,
4706 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4707 height: Some(1),
4708 render: Arc::new(|_| div().into_any()),
4709 priority: 0,
4710 render_in_minimap: true,
4711 }],
4712 Some(Autoscroll::fit()),
4713 cx,
4714 );
4715 editor.change_selections(None, window, cx, |s| {
4716 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4717 });
4718 editor.move_line_down(&MoveLineDown, window, cx);
4719 });
4720}
4721
4722#[gpui::test]
4723async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727 cx.set_state(
4728 &"
4729 ˇzero
4730 one
4731 two
4732 three
4733 four
4734 five
4735 "
4736 .unindent(),
4737 );
4738
4739 // Create a four-line block that replaces three lines of text.
4740 cx.update_editor(|editor, window, cx| {
4741 let snapshot = editor.snapshot(window, cx);
4742 let snapshot = &snapshot.buffer_snapshot;
4743 let placement = BlockPlacement::Replace(
4744 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4745 );
4746 editor.insert_blocks(
4747 [BlockProperties {
4748 placement,
4749 height: Some(4),
4750 style: BlockStyle::Sticky,
4751 render: Arc::new(|_| gpui::div().into_any_element()),
4752 priority: 0,
4753 render_in_minimap: true,
4754 }],
4755 None,
4756 cx,
4757 );
4758 });
4759
4760 // Move down so that the cursor touches the block.
4761 cx.update_editor(|editor, window, cx| {
4762 editor.move_down(&Default::default(), window, cx);
4763 });
4764 cx.assert_editor_state(
4765 &"
4766 zero
4767 «one
4768 two
4769 threeˇ»
4770 four
4771 five
4772 "
4773 .unindent(),
4774 );
4775
4776 // Move down past the block.
4777 cx.update_editor(|editor, window, cx| {
4778 editor.move_down(&Default::default(), window, cx);
4779 });
4780 cx.assert_editor_state(
4781 &"
4782 zero
4783 one
4784 two
4785 three
4786 ˇfour
4787 five
4788 "
4789 .unindent(),
4790 );
4791}
4792
4793#[gpui::test]
4794fn test_transpose(cx: &mut TestAppContext) {
4795 init_test(cx, |_| {});
4796
4797 _ = cx.add_window(|window, cx| {
4798 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4799 editor.set_style(EditorStyle::default(), window, cx);
4800 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4801 editor.transpose(&Default::default(), window, cx);
4802 assert_eq!(editor.text(cx), "bac");
4803 assert_eq!(editor.selections.ranges(cx), [2..2]);
4804
4805 editor.transpose(&Default::default(), window, cx);
4806 assert_eq!(editor.text(cx), "bca");
4807 assert_eq!(editor.selections.ranges(cx), [3..3]);
4808
4809 editor.transpose(&Default::default(), window, cx);
4810 assert_eq!(editor.text(cx), "bac");
4811 assert_eq!(editor.selections.ranges(cx), [3..3]);
4812
4813 editor
4814 });
4815
4816 _ = cx.add_window(|window, cx| {
4817 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4818 editor.set_style(EditorStyle::default(), window, cx);
4819 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4820 editor.transpose(&Default::default(), window, cx);
4821 assert_eq!(editor.text(cx), "acb\nde");
4822 assert_eq!(editor.selections.ranges(cx), [3..3]);
4823
4824 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4825 editor.transpose(&Default::default(), window, cx);
4826 assert_eq!(editor.text(cx), "acbd\ne");
4827 assert_eq!(editor.selections.ranges(cx), [5..5]);
4828
4829 editor.transpose(&Default::default(), window, cx);
4830 assert_eq!(editor.text(cx), "acbde\n");
4831 assert_eq!(editor.selections.ranges(cx), [6..6]);
4832
4833 editor.transpose(&Default::default(), window, cx);
4834 assert_eq!(editor.text(cx), "acbd\ne");
4835 assert_eq!(editor.selections.ranges(cx), [6..6]);
4836
4837 editor
4838 });
4839
4840 _ = cx.add_window(|window, cx| {
4841 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4842 editor.set_style(EditorStyle::default(), window, cx);
4843 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4844 editor.transpose(&Default::default(), window, cx);
4845 assert_eq!(editor.text(cx), "bacd\ne");
4846 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4847
4848 editor.transpose(&Default::default(), window, cx);
4849 assert_eq!(editor.text(cx), "bcade\n");
4850 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4851
4852 editor.transpose(&Default::default(), window, cx);
4853 assert_eq!(editor.text(cx), "bcda\ne");
4854 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4855
4856 editor.transpose(&Default::default(), window, cx);
4857 assert_eq!(editor.text(cx), "bcade\n");
4858 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4859
4860 editor.transpose(&Default::default(), window, cx);
4861 assert_eq!(editor.text(cx), "bcaed\n");
4862 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4863
4864 editor
4865 });
4866
4867 _ = cx.add_window(|window, cx| {
4868 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4869 editor.set_style(EditorStyle::default(), window, cx);
4870 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4871 editor.transpose(&Default::default(), window, cx);
4872 assert_eq!(editor.text(cx), "🏀🍐✋");
4873 assert_eq!(editor.selections.ranges(cx), [8..8]);
4874
4875 editor.transpose(&Default::default(), window, cx);
4876 assert_eq!(editor.text(cx), "🏀✋🍐");
4877 assert_eq!(editor.selections.ranges(cx), [11..11]);
4878
4879 editor.transpose(&Default::default(), window, cx);
4880 assert_eq!(editor.text(cx), "🏀🍐✋");
4881 assert_eq!(editor.selections.ranges(cx), [11..11]);
4882
4883 editor
4884 });
4885}
4886
4887#[gpui::test]
4888async fn test_rewrap(cx: &mut TestAppContext) {
4889 init_test(cx, |settings| {
4890 settings.languages.extend([
4891 (
4892 "Markdown".into(),
4893 LanguageSettingsContent {
4894 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4895 ..Default::default()
4896 },
4897 ),
4898 (
4899 "Plain Text".into(),
4900 LanguageSettingsContent {
4901 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4902 ..Default::default()
4903 },
4904 ),
4905 ])
4906 });
4907
4908 let mut cx = EditorTestContext::new(cx).await;
4909
4910 let language_with_c_comments = Arc::new(Language::new(
4911 LanguageConfig {
4912 line_comments: vec!["// ".into()],
4913 ..LanguageConfig::default()
4914 },
4915 None,
4916 ));
4917 let language_with_pound_comments = Arc::new(Language::new(
4918 LanguageConfig {
4919 line_comments: vec!["# ".into()],
4920 ..LanguageConfig::default()
4921 },
4922 None,
4923 ));
4924 let markdown_language = Arc::new(Language::new(
4925 LanguageConfig {
4926 name: "Markdown".into(),
4927 ..LanguageConfig::default()
4928 },
4929 None,
4930 ));
4931 let language_with_doc_comments = Arc::new(Language::new(
4932 LanguageConfig {
4933 line_comments: vec!["// ".into(), "/// ".into()],
4934 ..LanguageConfig::default()
4935 },
4936 Some(tree_sitter_rust::LANGUAGE.into()),
4937 ));
4938
4939 let plaintext_language = Arc::new(Language::new(
4940 LanguageConfig {
4941 name: "Plain Text".into(),
4942 ..LanguageConfig::default()
4943 },
4944 None,
4945 ));
4946
4947 assert_rewrap(
4948 indoc! {"
4949 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4950 "},
4951 indoc! {"
4952 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4953 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4954 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4955 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4956 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4957 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4958 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4959 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4960 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4961 // porttitor id. Aliquam id accumsan eros.
4962 "},
4963 language_with_c_comments.clone(),
4964 &mut cx,
4965 );
4966
4967 // Test that rewrapping works inside of a selection
4968 assert_rewrap(
4969 indoc! {"
4970 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4971 "},
4972 indoc! {"
4973 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4974 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4975 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4976 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4977 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4978 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4979 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4980 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4981 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4982 // porttitor id. Aliquam id accumsan eros.ˇ»
4983 "},
4984 language_with_c_comments.clone(),
4985 &mut cx,
4986 );
4987
4988 // Test that cursors that expand to the same region are collapsed.
4989 assert_rewrap(
4990 indoc! {"
4991 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4992 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4993 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4994 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4995 "},
4996 indoc! {"
4997 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4998 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4999 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5000 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5001 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5002 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5003 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5004 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5005 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5006 // porttitor id. Aliquam id accumsan eros.
5007 "},
5008 language_with_c_comments.clone(),
5009 &mut cx,
5010 );
5011
5012 // Test that non-contiguous selections are treated separately.
5013 assert_rewrap(
5014 indoc! {"
5015 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5016 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5017 //
5018 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5019 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5020 "},
5021 indoc! {"
5022 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5023 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5024 // auctor, eu lacinia sapien scelerisque.
5025 //
5026 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5027 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5028 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5029 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5030 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5031 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5032 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5033 "},
5034 language_with_c_comments.clone(),
5035 &mut cx,
5036 );
5037
5038 // Test that different comment prefixes are supported.
5039 assert_rewrap(
5040 indoc! {"
5041 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5042 "},
5043 indoc! {"
5044 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5045 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5046 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5047 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5048 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5049 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5050 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5051 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5052 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5053 # accumsan eros.
5054 "},
5055 language_with_pound_comments.clone(),
5056 &mut cx,
5057 );
5058
5059 // Test that rewrapping is ignored outside of comments in most languages.
5060 assert_rewrap(
5061 indoc! {"
5062 /// Adds two numbers.
5063 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5064 fn add(a: u32, b: u32) -> u32 {
5065 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5066 }
5067 "},
5068 indoc! {"
5069 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5070 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5071 fn add(a: u32, b: u32) -> u32 {
5072 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5073 }
5074 "},
5075 language_with_doc_comments.clone(),
5076 &mut cx,
5077 );
5078
5079 // Test that rewrapping works in Markdown and Plain Text languages.
5080 assert_rewrap(
5081 indoc! {"
5082 # Hello
5083
5084 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5085 "},
5086 indoc! {"
5087 # Hello
5088
5089 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5090 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5091 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5092 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5093 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5094 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5095 Integer sit amet scelerisque nisi.
5096 "},
5097 markdown_language,
5098 &mut cx,
5099 );
5100
5101 assert_rewrap(
5102 indoc! {"
5103 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5104 "},
5105 indoc! {"
5106 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5107 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5108 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5109 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5110 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5111 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5112 Integer sit amet scelerisque nisi.
5113 "},
5114 plaintext_language.clone(),
5115 &mut cx,
5116 );
5117
5118 // Test rewrapping unaligned comments in a selection.
5119 assert_rewrap(
5120 indoc! {"
5121 fn foo() {
5122 if true {
5123 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5124 // Praesent semper egestas tellus id dignissim.ˇ»
5125 do_something();
5126 } else {
5127 //
5128 }
5129 }
5130 "},
5131 indoc! {"
5132 fn foo() {
5133 if true {
5134 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5135 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5136 // egestas tellus id dignissim.ˇ»
5137 do_something();
5138 } else {
5139 //
5140 }
5141 }
5142 "},
5143 language_with_doc_comments.clone(),
5144 &mut cx,
5145 );
5146
5147 assert_rewrap(
5148 indoc! {"
5149 fn foo() {
5150 if true {
5151 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5152 // Praesent semper egestas tellus id dignissim.»
5153 do_something();
5154 } else {
5155 //
5156 }
5157
5158 }
5159 "},
5160 indoc! {"
5161 fn foo() {
5162 if true {
5163 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5164 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5165 // egestas tellus id dignissim.»
5166 do_something();
5167 } else {
5168 //
5169 }
5170
5171 }
5172 "},
5173 language_with_doc_comments.clone(),
5174 &mut cx,
5175 );
5176
5177 assert_rewrap(
5178 indoc! {"
5179 «ˇone one one one one one one one one one one one one one one one one one one one one one one one one
5180
5181 two»
5182
5183 three
5184
5185 «ˇ\t
5186
5187 four four four four four four four four four four four four four four four four four four four four»
5188
5189 «ˇfive five five five five five five five five five five five five five five five five five five five
5190 \t»
5191 six six six six six six six six six six six six six six six six six six six six six six six six six
5192 "},
5193 indoc! {"
5194 «ˇone one one one one one one one one one one one one one one one one one one one
5195 one one one one one
5196
5197 two»
5198
5199 three
5200
5201 «ˇ\t
5202
5203 four four four four four four four four four four four four four four four four
5204 four four four four»
5205
5206 «ˇfive five five five five five five five five five five five five five five five
5207 five five five five
5208 \t»
5209 six six six six six six six six six six six six six six six six six six six six six six six six six
5210 "},
5211 plaintext_language.clone(),
5212 &mut cx,
5213 );
5214
5215 assert_rewrap(
5216 indoc! {"
5217 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5218 //ˇ
5219 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5220 //ˇ short short short
5221 int main(void) {
5222 return 17;
5223 }
5224 "},
5225 indoc! {"
5226 //ˇ long long long long long long long long long long long long long long long
5227 // long long long long long long long long long long long long long
5228 //ˇ
5229 //ˇ long long long long long long long long long long long long long long long
5230 //ˇ long long long long long long long long long long long long long short short
5231 // short
5232 int main(void) {
5233 return 17;
5234 }
5235 "},
5236 language_with_c_comments,
5237 &mut cx,
5238 );
5239
5240 #[track_caller]
5241 fn assert_rewrap(
5242 unwrapped_text: &str,
5243 wrapped_text: &str,
5244 language: Arc<Language>,
5245 cx: &mut EditorTestContext,
5246 ) {
5247 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5248 cx.set_state(unwrapped_text);
5249 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5250 cx.assert_editor_state(wrapped_text);
5251 }
5252}
5253
5254#[gpui::test]
5255async fn test_hard_wrap(cx: &mut TestAppContext) {
5256 init_test(cx, |_| {});
5257 let mut cx = EditorTestContext::new(cx).await;
5258
5259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5260 cx.update_editor(|editor, _, cx| {
5261 editor.set_hard_wrap(Some(14), cx);
5262 });
5263
5264 cx.set_state(indoc!(
5265 "
5266 one two three ˇ
5267 "
5268 ));
5269 cx.simulate_input("four");
5270 cx.run_until_parked();
5271
5272 cx.assert_editor_state(indoc!(
5273 "
5274 one two three
5275 fourˇ
5276 "
5277 ));
5278
5279 cx.update_editor(|editor, window, cx| {
5280 editor.newline(&Default::default(), window, cx);
5281 });
5282 cx.run_until_parked();
5283 cx.assert_editor_state(indoc!(
5284 "
5285 one two three
5286 four
5287 ˇ
5288 "
5289 ));
5290
5291 cx.simulate_input("five");
5292 cx.run_until_parked();
5293 cx.assert_editor_state(indoc!(
5294 "
5295 one two three
5296 four
5297 fiveˇ
5298 "
5299 ));
5300
5301 cx.update_editor(|editor, window, cx| {
5302 editor.newline(&Default::default(), window, cx);
5303 });
5304 cx.run_until_parked();
5305 cx.simulate_input("# ");
5306 cx.run_until_parked();
5307 cx.assert_editor_state(indoc!(
5308 "
5309 one two three
5310 four
5311 five
5312 # ˇ
5313 "
5314 ));
5315
5316 cx.update_editor(|editor, window, cx| {
5317 editor.newline(&Default::default(), window, cx);
5318 });
5319 cx.run_until_parked();
5320 cx.assert_editor_state(indoc!(
5321 "
5322 one two three
5323 four
5324 five
5325 #\x20
5326 #ˇ
5327 "
5328 ));
5329
5330 cx.simulate_input(" 6");
5331 cx.run_until_parked();
5332 cx.assert_editor_state(indoc!(
5333 "
5334 one two three
5335 four
5336 five
5337 #
5338 # 6ˇ
5339 "
5340 ));
5341}
5342
5343#[gpui::test]
5344async fn test_clipboard(cx: &mut TestAppContext) {
5345 init_test(cx, |_| {});
5346
5347 let mut cx = EditorTestContext::new(cx).await;
5348
5349 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5350 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5351 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5352
5353 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5354 cx.set_state("two ˇfour ˇsix ˇ");
5355 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5356 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5357
5358 // Paste again but with only two cursors. Since the number of cursors doesn't
5359 // match the number of slices in the clipboard, the entire clipboard text
5360 // is pasted at each cursor.
5361 cx.set_state("ˇtwo one✅ four three six five ˇ");
5362 cx.update_editor(|e, window, cx| {
5363 e.handle_input("( ", window, cx);
5364 e.paste(&Paste, window, cx);
5365 e.handle_input(") ", window, cx);
5366 });
5367 cx.assert_editor_state(
5368 &([
5369 "( one✅ ",
5370 "three ",
5371 "five ) ˇtwo one✅ four three six five ( one✅ ",
5372 "three ",
5373 "five ) ˇ",
5374 ]
5375 .join("\n")),
5376 );
5377
5378 // Cut with three selections, one of which is full-line.
5379 cx.set_state(indoc! {"
5380 1«2ˇ»3
5381 4ˇ567
5382 «8ˇ»9"});
5383 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5384 cx.assert_editor_state(indoc! {"
5385 1ˇ3
5386 ˇ9"});
5387
5388 // Paste with three selections, noticing how the copied selection that was full-line
5389 // gets inserted before the second cursor.
5390 cx.set_state(indoc! {"
5391 1ˇ3
5392 9ˇ
5393 «oˇ»ne"});
5394 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5395 cx.assert_editor_state(indoc! {"
5396 12ˇ3
5397 4567
5398 9ˇ
5399 8ˇne"});
5400
5401 // Copy with a single cursor only, which writes the whole line into the clipboard.
5402 cx.set_state(indoc! {"
5403 The quick brown
5404 fox juˇmps over
5405 the lazy dog"});
5406 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5407 assert_eq!(
5408 cx.read_from_clipboard()
5409 .and_then(|item| item.text().as_deref().map(str::to_string)),
5410 Some("fox jumps over\n".to_string())
5411 );
5412
5413 // Paste with three selections, noticing how the copied full-line selection is inserted
5414 // before the empty selections but replaces the selection that is non-empty.
5415 cx.set_state(indoc! {"
5416 Tˇhe quick brown
5417 «foˇ»x jumps over
5418 tˇhe lazy dog"});
5419 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5420 cx.assert_editor_state(indoc! {"
5421 fox jumps over
5422 Tˇhe quick brown
5423 fox jumps over
5424 ˇx jumps over
5425 fox jumps over
5426 tˇhe lazy dog"});
5427}
5428
5429#[gpui::test]
5430async fn test_copy_trim(cx: &mut TestAppContext) {
5431 init_test(cx, |_| {});
5432
5433 let mut cx = EditorTestContext::new(cx).await;
5434 cx.set_state(
5435 r#" «for selection in selections.iter() {
5436 let mut start = selection.start;
5437 let mut end = selection.end;
5438 let is_entire_line = selection.is_empty();
5439 if is_entire_line {
5440 start = Point::new(start.row, 0);ˇ»
5441 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5442 }
5443 "#,
5444 );
5445 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5446 assert_eq!(
5447 cx.read_from_clipboard()
5448 .and_then(|item| item.text().as_deref().map(str::to_string)),
5449 Some(
5450 "for selection in selections.iter() {
5451 let mut start = selection.start;
5452 let mut end = selection.end;
5453 let is_entire_line = selection.is_empty();
5454 if is_entire_line {
5455 start = Point::new(start.row, 0);"
5456 .to_string()
5457 ),
5458 "Regular copying preserves all indentation selected",
5459 );
5460 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5461 assert_eq!(
5462 cx.read_from_clipboard()
5463 .and_then(|item| item.text().as_deref().map(str::to_string)),
5464 Some(
5465 "for selection in selections.iter() {
5466let mut start = selection.start;
5467let mut end = selection.end;
5468let is_entire_line = selection.is_empty();
5469if is_entire_line {
5470 start = Point::new(start.row, 0);"
5471 .to_string()
5472 ),
5473 "Copying with stripping should strip all leading whitespaces"
5474 );
5475
5476 cx.set_state(
5477 r#" « for selection in selections.iter() {
5478 let mut start = selection.start;
5479 let mut end = selection.end;
5480 let is_entire_line = selection.is_empty();
5481 if is_entire_line {
5482 start = Point::new(start.row, 0);ˇ»
5483 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5484 }
5485 "#,
5486 );
5487 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5488 assert_eq!(
5489 cx.read_from_clipboard()
5490 .and_then(|item| item.text().as_deref().map(str::to_string)),
5491 Some(
5492 " for selection in selections.iter() {
5493 let mut start = selection.start;
5494 let mut end = selection.end;
5495 let is_entire_line = selection.is_empty();
5496 if is_entire_line {
5497 start = Point::new(start.row, 0);"
5498 .to_string()
5499 ),
5500 "Regular copying preserves all indentation selected",
5501 );
5502 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5503 assert_eq!(
5504 cx.read_from_clipboard()
5505 .and_then(|item| item.text().as_deref().map(str::to_string)),
5506 Some(
5507 "for selection in selections.iter() {
5508let mut start = selection.start;
5509let mut end = selection.end;
5510let is_entire_line = selection.is_empty();
5511if is_entire_line {
5512 start = Point::new(start.row, 0);"
5513 .to_string()
5514 ),
5515 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5516 );
5517
5518 cx.set_state(
5519 r#" «ˇ for selection in selections.iter() {
5520 let mut start = selection.start;
5521 let mut end = selection.end;
5522 let is_entire_line = selection.is_empty();
5523 if is_entire_line {
5524 start = Point::new(start.row, 0);»
5525 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5526 }
5527 "#,
5528 );
5529 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5530 assert_eq!(
5531 cx.read_from_clipboard()
5532 .and_then(|item| item.text().as_deref().map(str::to_string)),
5533 Some(
5534 " for selection in selections.iter() {
5535 let mut start = selection.start;
5536 let mut end = selection.end;
5537 let is_entire_line = selection.is_empty();
5538 if is_entire_line {
5539 start = Point::new(start.row, 0);"
5540 .to_string()
5541 ),
5542 "Regular copying for reverse selection works the same",
5543 );
5544 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5545 assert_eq!(
5546 cx.read_from_clipboard()
5547 .and_then(|item| item.text().as_deref().map(str::to_string)),
5548 Some(
5549 "for selection in selections.iter() {
5550let mut start = selection.start;
5551let mut end = selection.end;
5552let is_entire_line = selection.is_empty();
5553if is_entire_line {
5554 start = Point::new(start.row, 0);"
5555 .to_string()
5556 ),
5557 "Copying with stripping for reverse selection works the same"
5558 );
5559
5560 cx.set_state(
5561 r#" for selection «in selections.iter() {
5562 let mut start = selection.start;
5563 let mut end = selection.end;
5564 let is_entire_line = selection.is_empty();
5565 if is_entire_line {
5566 start = Point::new(start.row, 0);ˇ»
5567 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5568 }
5569 "#,
5570 );
5571 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5572 assert_eq!(
5573 cx.read_from_clipboard()
5574 .and_then(|item| item.text().as_deref().map(str::to_string)),
5575 Some(
5576 "in selections.iter() {
5577 let mut start = selection.start;
5578 let mut end = selection.end;
5579 let is_entire_line = selection.is_empty();
5580 if is_entire_line {
5581 start = Point::new(start.row, 0);"
5582 .to_string()
5583 ),
5584 "When selecting past the indent, the copying works as usual",
5585 );
5586 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5587 assert_eq!(
5588 cx.read_from_clipboard()
5589 .and_then(|item| item.text().as_deref().map(str::to_string)),
5590 Some(
5591 "in selections.iter() {
5592 let mut start = selection.start;
5593 let mut end = selection.end;
5594 let is_entire_line = selection.is_empty();
5595 if is_entire_line {
5596 start = Point::new(start.row, 0);"
5597 .to_string()
5598 ),
5599 "When selecting past the indent, nothing is trimmed"
5600 );
5601
5602 cx.set_state(
5603 r#" «for selection in selections.iter() {
5604 let mut start = selection.start;
5605
5606 let mut end = selection.end;
5607 let is_entire_line = selection.is_empty();
5608 if is_entire_line {
5609 start = Point::new(start.row, 0);
5610ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5611 }
5612 "#,
5613 );
5614 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5615 assert_eq!(
5616 cx.read_from_clipboard()
5617 .and_then(|item| item.text().as_deref().map(str::to_string)),
5618 Some(
5619 "for selection in selections.iter() {
5620let mut start = selection.start;
5621
5622let mut end = selection.end;
5623let is_entire_line = selection.is_empty();
5624if is_entire_line {
5625 start = Point::new(start.row, 0);
5626"
5627 .to_string()
5628 ),
5629 "Copying with stripping should ignore empty lines"
5630 );
5631}
5632
5633#[gpui::test]
5634async fn test_paste_multiline(cx: &mut TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let mut cx = EditorTestContext::new(cx).await;
5638 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5639
5640 // Cut an indented block, without the leading whitespace.
5641 cx.set_state(indoc! {"
5642 const a: B = (
5643 c(),
5644 «d(
5645 e,
5646 f
5647 )ˇ»
5648 );
5649 "});
5650 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5651 cx.assert_editor_state(indoc! {"
5652 const a: B = (
5653 c(),
5654 ˇ
5655 );
5656 "});
5657
5658 // Paste it at the same position.
5659 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5660 cx.assert_editor_state(indoc! {"
5661 const a: B = (
5662 c(),
5663 d(
5664 e,
5665 f
5666 )ˇ
5667 );
5668 "});
5669
5670 // Paste it at a line with a lower indent level.
5671 cx.set_state(indoc! {"
5672 ˇ
5673 const a: B = (
5674 c(),
5675 );
5676 "});
5677 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5678 cx.assert_editor_state(indoc! {"
5679 d(
5680 e,
5681 f
5682 )ˇ
5683 const a: B = (
5684 c(),
5685 );
5686 "});
5687
5688 // Cut an indented block, with the leading whitespace.
5689 cx.set_state(indoc! {"
5690 const a: B = (
5691 c(),
5692 « d(
5693 e,
5694 f
5695 )
5696 ˇ»);
5697 "});
5698 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5699 cx.assert_editor_state(indoc! {"
5700 const a: B = (
5701 c(),
5702 ˇ);
5703 "});
5704
5705 // Paste it at the same position.
5706 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5707 cx.assert_editor_state(indoc! {"
5708 const a: B = (
5709 c(),
5710 d(
5711 e,
5712 f
5713 )
5714 ˇ);
5715 "});
5716
5717 // Paste it at a line with a higher indent level.
5718 cx.set_state(indoc! {"
5719 const a: B = (
5720 c(),
5721 d(
5722 e,
5723 fˇ
5724 )
5725 );
5726 "});
5727 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5728 cx.assert_editor_state(indoc! {"
5729 const a: B = (
5730 c(),
5731 d(
5732 e,
5733 f d(
5734 e,
5735 f
5736 )
5737 ˇ
5738 )
5739 );
5740 "});
5741
5742 // Copy an indented block, starting mid-line
5743 cx.set_state(indoc! {"
5744 const a: B = (
5745 c(),
5746 somethin«g(
5747 e,
5748 f
5749 )ˇ»
5750 );
5751 "});
5752 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5753
5754 // Paste it on a line with a lower indent level
5755 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5756 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5757 cx.assert_editor_state(indoc! {"
5758 const a: B = (
5759 c(),
5760 something(
5761 e,
5762 f
5763 )
5764 );
5765 g(
5766 e,
5767 f
5768 )ˇ"});
5769}
5770
5771#[gpui::test]
5772async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5773 init_test(cx, |_| {});
5774
5775 cx.write_to_clipboard(ClipboardItem::new_string(
5776 " d(\n e\n );\n".into(),
5777 ));
5778
5779 let mut cx = EditorTestContext::new(cx).await;
5780 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5781
5782 cx.set_state(indoc! {"
5783 fn a() {
5784 b();
5785 if c() {
5786 ˇ
5787 }
5788 }
5789 "});
5790
5791 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5792 cx.assert_editor_state(indoc! {"
5793 fn a() {
5794 b();
5795 if c() {
5796 d(
5797 e
5798 );
5799 ˇ
5800 }
5801 }
5802 "});
5803
5804 cx.set_state(indoc! {"
5805 fn a() {
5806 b();
5807 ˇ
5808 }
5809 "});
5810
5811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5812 cx.assert_editor_state(indoc! {"
5813 fn a() {
5814 b();
5815 d(
5816 e
5817 );
5818 ˇ
5819 }
5820 "});
5821}
5822
5823#[gpui::test]
5824fn test_select_all(cx: &mut TestAppContext) {
5825 init_test(cx, |_| {});
5826
5827 let editor = cx.add_window(|window, cx| {
5828 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5829 build_editor(buffer, window, cx)
5830 });
5831 _ = editor.update(cx, |editor, window, cx| {
5832 editor.select_all(&SelectAll, window, cx);
5833 assert_eq!(
5834 editor.selections.display_ranges(cx),
5835 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5836 );
5837 });
5838}
5839
5840#[gpui::test]
5841fn test_select_line(cx: &mut TestAppContext) {
5842 init_test(cx, |_| {});
5843
5844 let editor = cx.add_window(|window, cx| {
5845 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5846 build_editor(buffer, window, cx)
5847 });
5848 _ = editor.update(cx, |editor, window, cx| {
5849 editor.change_selections(None, window, cx, |s| {
5850 s.select_display_ranges([
5851 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5852 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5853 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5854 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5855 ])
5856 });
5857 editor.select_line(&SelectLine, window, cx);
5858 assert_eq!(
5859 editor.selections.display_ranges(cx),
5860 vec![
5861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5863 ]
5864 );
5865 });
5866
5867 _ = editor.update(cx, |editor, window, cx| {
5868 editor.select_line(&SelectLine, window, cx);
5869 assert_eq!(
5870 editor.selections.display_ranges(cx),
5871 vec![
5872 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5873 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5874 ]
5875 );
5876 });
5877
5878 _ = editor.update(cx, |editor, window, cx| {
5879 editor.select_line(&SelectLine, window, cx);
5880 assert_eq!(
5881 editor.selections.display_ranges(cx),
5882 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5883 );
5884 });
5885}
5886
5887#[gpui::test]
5888async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890 let mut cx = EditorTestContext::new(cx).await;
5891
5892 #[track_caller]
5893 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5894 cx.set_state(initial_state);
5895 cx.update_editor(|e, window, cx| {
5896 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5897 });
5898 cx.assert_editor_state(expected_state);
5899 }
5900
5901 // Selection starts and ends at the middle of lines, left-to-right
5902 test(
5903 &mut cx,
5904 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5905 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5906 );
5907 // Same thing, right-to-left
5908 test(
5909 &mut cx,
5910 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5911 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5912 );
5913
5914 // Whole buffer, left-to-right, last line *doesn't* end with newline
5915 test(
5916 &mut cx,
5917 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5918 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5919 );
5920 // Same thing, right-to-left
5921 test(
5922 &mut cx,
5923 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5924 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5925 );
5926
5927 // Whole buffer, left-to-right, last line ends with newline
5928 test(
5929 &mut cx,
5930 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5931 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5932 );
5933 // Same thing, right-to-left
5934 test(
5935 &mut cx,
5936 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5937 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5938 );
5939
5940 // Starts at the end of a line, ends at the start of another
5941 test(
5942 &mut cx,
5943 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5944 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5945 );
5946}
5947
5948#[gpui::test]
5949async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5950 init_test(cx, |_| {});
5951
5952 let editor = cx.add_window(|window, cx| {
5953 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5954 build_editor(buffer, window, cx)
5955 });
5956
5957 // setup
5958 _ = editor.update(cx, |editor, window, cx| {
5959 editor.fold_creases(
5960 vec![
5961 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5962 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5963 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5964 ],
5965 true,
5966 window,
5967 cx,
5968 );
5969 assert_eq!(
5970 editor.display_text(cx),
5971 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5972 );
5973 });
5974
5975 _ = editor.update(cx, |editor, window, cx| {
5976 editor.change_selections(None, window, cx, |s| {
5977 s.select_display_ranges([
5978 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5979 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5980 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5981 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5982 ])
5983 });
5984 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5985 assert_eq!(
5986 editor.display_text(cx),
5987 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5988 );
5989 });
5990 EditorTestContext::for_editor(editor, cx)
5991 .await
5992 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5993
5994 _ = editor.update(cx, |editor, window, cx| {
5995 editor.change_selections(None, window, cx, |s| {
5996 s.select_display_ranges([
5997 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5998 ])
5999 });
6000 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6001 assert_eq!(
6002 editor.display_text(cx),
6003 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6004 );
6005 assert_eq!(
6006 editor.selections.display_ranges(cx),
6007 [
6008 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6009 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6010 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6011 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6012 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6013 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6014 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6015 ]
6016 );
6017 });
6018 EditorTestContext::for_editor(editor, cx)
6019 .await
6020 .assert_editor_state(
6021 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6022 );
6023}
6024
6025#[gpui::test]
6026async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6027 init_test(cx, |_| {});
6028
6029 let mut cx = EditorTestContext::new(cx).await;
6030
6031 cx.set_state(indoc!(
6032 r#"abc
6033 defˇghi
6034
6035 jk
6036 nlmo
6037 "#
6038 ));
6039
6040 cx.update_editor(|editor, window, cx| {
6041 editor.add_selection_above(&Default::default(), window, cx);
6042 });
6043
6044 cx.assert_editor_state(indoc!(
6045 r#"abcˇ
6046 defˇghi
6047
6048 jk
6049 nlmo
6050 "#
6051 ));
6052
6053 cx.update_editor(|editor, window, cx| {
6054 editor.add_selection_above(&Default::default(), window, cx);
6055 });
6056
6057 cx.assert_editor_state(indoc!(
6058 r#"abcˇ
6059 defˇghi
6060
6061 jk
6062 nlmo
6063 "#
6064 ));
6065
6066 cx.update_editor(|editor, window, cx| {
6067 editor.add_selection_below(&Default::default(), window, cx);
6068 });
6069
6070 cx.assert_editor_state(indoc!(
6071 r#"abc
6072 defˇghi
6073
6074 jk
6075 nlmo
6076 "#
6077 ));
6078
6079 cx.update_editor(|editor, window, cx| {
6080 editor.undo_selection(&Default::default(), window, cx);
6081 });
6082
6083 cx.assert_editor_state(indoc!(
6084 r#"abcˇ
6085 defˇghi
6086
6087 jk
6088 nlmo
6089 "#
6090 ));
6091
6092 cx.update_editor(|editor, window, cx| {
6093 editor.redo_selection(&Default::default(), window, cx);
6094 });
6095
6096 cx.assert_editor_state(indoc!(
6097 r#"abc
6098 defˇghi
6099
6100 jk
6101 nlmo
6102 "#
6103 ));
6104
6105 cx.update_editor(|editor, window, cx| {
6106 editor.add_selection_below(&Default::default(), window, cx);
6107 });
6108
6109 cx.assert_editor_state(indoc!(
6110 r#"abc
6111 defˇghi
6112 ˇ
6113 jk
6114 nlmo
6115 "#
6116 ));
6117
6118 cx.update_editor(|editor, window, cx| {
6119 editor.add_selection_below(&Default::default(), window, cx);
6120 });
6121
6122 cx.assert_editor_state(indoc!(
6123 r#"abc
6124 defˇghi
6125 ˇ
6126 jkˇ
6127 nlmo
6128 "#
6129 ));
6130
6131 cx.update_editor(|editor, window, cx| {
6132 editor.add_selection_below(&Default::default(), window, cx);
6133 });
6134
6135 cx.assert_editor_state(indoc!(
6136 r#"abc
6137 defˇghi
6138 ˇ
6139 jkˇ
6140 nlmˇo
6141 "#
6142 ));
6143
6144 cx.update_editor(|editor, window, cx| {
6145 editor.add_selection_below(&Default::default(), window, cx);
6146 });
6147
6148 cx.assert_editor_state(indoc!(
6149 r#"abc
6150 defˇghi
6151 ˇ
6152 jkˇ
6153 nlmˇo
6154 ˇ"#
6155 ));
6156
6157 // change selections
6158 cx.set_state(indoc!(
6159 r#"abc
6160 def«ˇg»hi
6161
6162 jk
6163 nlmo
6164 "#
6165 ));
6166
6167 cx.update_editor(|editor, window, cx| {
6168 editor.add_selection_below(&Default::default(), window, cx);
6169 });
6170
6171 cx.assert_editor_state(indoc!(
6172 r#"abc
6173 def«ˇg»hi
6174
6175 jk
6176 nlm«ˇo»
6177 "#
6178 ));
6179
6180 cx.update_editor(|editor, window, cx| {
6181 editor.add_selection_below(&Default::default(), window, cx);
6182 });
6183
6184 cx.assert_editor_state(indoc!(
6185 r#"abc
6186 def«ˇg»hi
6187
6188 jk
6189 nlm«ˇo»
6190 "#
6191 ));
6192
6193 cx.update_editor(|editor, window, cx| {
6194 editor.add_selection_above(&Default::default(), window, cx);
6195 });
6196
6197 cx.assert_editor_state(indoc!(
6198 r#"abc
6199 def«ˇg»hi
6200
6201 jk
6202 nlmo
6203 "#
6204 ));
6205
6206 cx.update_editor(|editor, window, cx| {
6207 editor.add_selection_above(&Default::default(), window, cx);
6208 });
6209
6210 cx.assert_editor_state(indoc!(
6211 r#"abc
6212 def«ˇg»hi
6213
6214 jk
6215 nlmo
6216 "#
6217 ));
6218
6219 // Change selections again
6220 cx.set_state(indoc!(
6221 r#"a«bc
6222 defgˇ»hi
6223
6224 jk
6225 nlmo
6226 "#
6227 ));
6228
6229 cx.update_editor(|editor, window, cx| {
6230 editor.add_selection_below(&Default::default(), window, cx);
6231 });
6232
6233 cx.assert_editor_state(indoc!(
6234 r#"a«bcˇ»
6235 d«efgˇ»hi
6236
6237 j«kˇ»
6238 nlmo
6239 "#
6240 ));
6241
6242 cx.update_editor(|editor, window, cx| {
6243 editor.add_selection_below(&Default::default(), window, cx);
6244 });
6245 cx.assert_editor_state(indoc!(
6246 r#"a«bcˇ»
6247 d«efgˇ»hi
6248
6249 j«kˇ»
6250 n«lmoˇ»
6251 "#
6252 ));
6253 cx.update_editor(|editor, window, cx| {
6254 editor.add_selection_above(&Default::default(), window, cx);
6255 });
6256
6257 cx.assert_editor_state(indoc!(
6258 r#"a«bcˇ»
6259 d«efgˇ»hi
6260
6261 j«kˇ»
6262 nlmo
6263 "#
6264 ));
6265
6266 // Change selections again
6267 cx.set_state(indoc!(
6268 r#"abc
6269 d«ˇefghi
6270
6271 jk
6272 nlm»o
6273 "#
6274 ));
6275
6276 cx.update_editor(|editor, window, cx| {
6277 editor.add_selection_above(&Default::default(), window, cx);
6278 });
6279
6280 cx.assert_editor_state(indoc!(
6281 r#"a«ˇbc»
6282 d«ˇef»ghi
6283
6284 j«ˇk»
6285 n«ˇlm»o
6286 "#
6287 ));
6288
6289 cx.update_editor(|editor, window, cx| {
6290 editor.add_selection_below(&Default::default(), window, cx);
6291 });
6292
6293 cx.assert_editor_state(indoc!(
6294 r#"abc
6295 d«ˇef»ghi
6296
6297 j«ˇk»
6298 n«ˇlm»o
6299 "#
6300 ));
6301}
6302
6303#[gpui::test]
6304async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306 let mut cx = EditorTestContext::new(cx).await;
6307
6308 cx.set_state(indoc!(
6309 r#"line onˇe
6310 liˇne two
6311 line three
6312 line four"#
6313 ));
6314
6315 cx.update_editor(|editor, window, cx| {
6316 editor.add_selection_below(&Default::default(), window, cx);
6317 });
6318
6319 // test multiple cursors expand in the same direction
6320 cx.assert_editor_state(indoc!(
6321 r#"line onˇe
6322 liˇne twˇo
6323 liˇne three
6324 line four"#
6325 ));
6326
6327 cx.update_editor(|editor, window, cx| {
6328 editor.add_selection_below(&Default::default(), window, cx);
6329 });
6330
6331 cx.update_editor(|editor, window, cx| {
6332 editor.add_selection_below(&Default::default(), window, cx);
6333 });
6334
6335 // test multiple cursors expand below overflow
6336 cx.assert_editor_state(indoc!(
6337 r#"line onˇe
6338 liˇne twˇo
6339 liˇne thˇree
6340 liˇne foˇur"#
6341 ));
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.add_selection_above(&Default::default(), window, cx);
6345 });
6346
6347 // test multiple cursors retrieves back correctly
6348 cx.assert_editor_state(indoc!(
6349 r#"line onˇe
6350 liˇne twˇo
6351 liˇne thˇree
6352 line four"#
6353 ));
6354
6355 cx.update_editor(|editor, window, cx| {
6356 editor.add_selection_above(&Default::default(), window, cx);
6357 });
6358
6359 cx.update_editor(|editor, window, cx| {
6360 editor.add_selection_above(&Default::default(), window, cx);
6361 });
6362
6363 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6364 cx.assert_editor_state(indoc!(
6365 r#"liˇne onˇe
6366 liˇne two
6367 line three
6368 line four"#
6369 ));
6370
6371 cx.update_editor(|editor, window, cx| {
6372 editor.undo_selection(&Default::default(), window, cx);
6373 });
6374
6375 // test undo
6376 cx.assert_editor_state(indoc!(
6377 r#"line onˇe
6378 liˇne twˇo
6379 line three
6380 line four"#
6381 ));
6382
6383 cx.update_editor(|editor, window, cx| {
6384 editor.redo_selection(&Default::default(), window, cx);
6385 });
6386
6387 // test redo
6388 cx.assert_editor_state(indoc!(
6389 r#"liˇne onˇe
6390 liˇne two
6391 line three
6392 line four"#
6393 ));
6394
6395 cx.set_state(indoc!(
6396 r#"abcd
6397 ef«ghˇ»
6398 ijkl
6399 «mˇ»nop"#
6400 ));
6401
6402 cx.update_editor(|editor, window, cx| {
6403 editor.add_selection_above(&Default::default(), window, cx);
6404 });
6405
6406 // test multiple selections expand in the same direction
6407 cx.assert_editor_state(indoc!(
6408 r#"ab«cdˇ»
6409 ef«ghˇ»
6410 «iˇ»jkl
6411 «mˇ»nop"#
6412 ));
6413
6414 cx.update_editor(|editor, window, cx| {
6415 editor.add_selection_above(&Default::default(), window, cx);
6416 });
6417
6418 // test multiple selection upward overflow
6419 cx.assert_editor_state(indoc!(
6420 r#"ab«cdˇ»
6421 «eˇ»f«ghˇ»
6422 «iˇ»jkl
6423 «mˇ»nop"#
6424 ));
6425
6426 cx.update_editor(|editor, window, cx| {
6427 editor.add_selection_below(&Default::default(), window, cx);
6428 });
6429
6430 // test multiple selection retrieves back correctly
6431 cx.assert_editor_state(indoc!(
6432 r#"abcd
6433 ef«ghˇ»
6434 «iˇ»jkl
6435 «mˇ»nop"#
6436 ));
6437
6438 cx.update_editor(|editor, window, cx| {
6439 editor.add_selection_below(&Default::default(), window, cx);
6440 });
6441
6442 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6443 cx.assert_editor_state(indoc!(
6444 r#"abcd
6445 ef«ghˇ»
6446 ij«klˇ»
6447 «mˇ»nop"#
6448 ));
6449
6450 cx.update_editor(|editor, window, cx| {
6451 editor.undo_selection(&Default::default(), window, cx);
6452 });
6453
6454 // test undo
6455 cx.assert_editor_state(indoc!(
6456 r#"abcd
6457 ef«ghˇ»
6458 «iˇ»jkl
6459 «mˇ»nop"#
6460 ));
6461
6462 cx.update_editor(|editor, window, cx| {
6463 editor.redo_selection(&Default::default(), window, cx);
6464 });
6465
6466 // test redo
6467 cx.assert_editor_state(indoc!(
6468 r#"abcd
6469 ef«ghˇ»
6470 ij«klˇ»
6471 «mˇ»nop"#
6472 ));
6473}
6474
6475#[gpui::test]
6476async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6477 init_test(cx, |_| {});
6478 let mut cx = EditorTestContext::new(cx).await;
6479
6480 cx.set_state(indoc!(
6481 r#"line onˇe
6482 liˇne two
6483 line three
6484 line four"#
6485 ));
6486
6487 cx.update_editor(|editor, window, cx| {
6488 editor.add_selection_below(&Default::default(), window, cx);
6489 editor.add_selection_below(&Default::default(), window, cx);
6490 editor.add_selection_below(&Default::default(), window, cx);
6491 });
6492
6493 // initial state with two multi cursor groups
6494 cx.assert_editor_state(indoc!(
6495 r#"line onˇe
6496 liˇne twˇo
6497 liˇne thˇree
6498 liˇne foˇur"#
6499 ));
6500
6501 // add single cursor in middle - simulate opt click
6502 cx.update_editor(|editor, window, cx| {
6503 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6504 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6505 editor.end_selection(window, cx);
6506 });
6507
6508 cx.assert_editor_state(indoc!(
6509 r#"line onˇe
6510 liˇne twˇo
6511 liˇneˇ thˇree
6512 liˇne foˇur"#
6513 ));
6514
6515 cx.update_editor(|editor, window, cx| {
6516 editor.add_selection_above(&Default::default(), window, cx);
6517 });
6518
6519 // test new added selection expands above and existing selection shrinks
6520 cx.assert_editor_state(indoc!(
6521 r#"line onˇe
6522 liˇneˇ twˇo
6523 liˇneˇ thˇree
6524 line four"#
6525 ));
6526
6527 cx.update_editor(|editor, window, cx| {
6528 editor.add_selection_above(&Default::default(), window, cx);
6529 });
6530
6531 // test new added selection expands above and existing selection shrinks
6532 cx.assert_editor_state(indoc!(
6533 r#"lineˇ onˇe
6534 liˇneˇ twˇo
6535 lineˇ three
6536 line four"#
6537 ));
6538
6539 // intial state with two selection groups
6540 cx.set_state(indoc!(
6541 r#"abcd
6542 ef«ghˇ»
6543 ijkl
6544 «mˇ»nop"#
6545 ));
6546
6547 cx.update_editor(|editor, window, cx| {
6548 editor.add_selection_above(&Default::default(), window, cx);
6549 editor.add_selection_above(&Default::default(), window, cx);
6550 });
6551
6552 cx.assert_editor_state(indoc!(
6553 r#"ab«cdˇ»
6554 «eˇ»f«ghˇ»
6555 «iˇ»jkl
6556 «mˇ»nop"#
6557 ));
6558
6559 // add single selection in middle - simulate opt drag
6560 cx.update_editor(|editor, window, cx| {
6561 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6562 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6563 editor.update_selection(
6564 DisplayPoint::new(DisplayRow(2), 4),
6565 0,
6566 gpui::Point::<f32>::default(),
6567 window,
6568 cx,
6569 );
6570 editor.end_selection(window, cx);
6571 });
6572
6573 cx.assert_editor_state(indoc!(
6574 r#"ab«cdˇ»
6575 «eˇ»f«ghˇ»
6576 «iˇ»jk«lˇ»
6577 «mˇ»nop"#
6578 ));
6579
6580 cx.update_editor(|editor, window, cx| {
6581 editor.add_selection_below(&Default::default(), window, cx);
6582 });
6583
6584 // test new added selection expands below, others shrinks from above
6585 cx.assert_editor_state(indoc!(
6586 r#"abcd
6587 ef«ghˇ»
6588 «iˇ»jk«lˇ»
6589 «mˇ»no«pˇ»"#
6590 ));
6591}
6592
6593#[gpui::test]
6594async fn test_select_next(cx: &mut TestAppContext) {
6595 init_test(cx, |_| {});
6596
6597 let mut cx = EditorTestContext::new(cx).await;
6598 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6599
6600 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6601 .unwrap();
6602 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6603
6604 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6605 .unwrap();
6606 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6607
6608 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6609 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6610
6611 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6612 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6613
6614 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6615 .unwrap();
6616 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6617
6618 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6619 .unwrap();
6620 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6621
6622 // Test selection direction should be preserved
6623 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6624
6625 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6626 .unwrap();
6627 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6628}
6629
6630#[gpui::test]
6631async fn test_select_all_matches(cx: &mut TestAppContext) {
6632 init_test(cx, |_| {});
6633
6634 let mut cx = EditorTestContext::new(cx).await;
6635
6636 // Test caret-only selections
6637 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6638 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6639 .unwrap();
6640 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6641
6642 // Test left-to-right selections
6643 cx.set_state("abc\n«abcˇ»\nabc");
6644 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6645 .unwrap();
6646 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6647
6648 // Test right-to-left selections
6649 cx.set_state("abc\n«ˇabc»\nabc");
6650 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6651 .unwrap();
6652 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6653
6654 // Test selecting whitespace with caret selection
6655 cx.set_state("abc\nˇ abc\nabc");
6656 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6657 .unwrap();
6658 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6659
6660 // Test selecting whitespace with left-to-right selection
6661 cx.set_state("abc\n«ˇ »abc\nabc");
6662 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6663 .unwrap();
6664 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6665
6666 // Test no matches with right-to-left selection
6667 cx.set_state("abc\n« ˇ»abc\nabc");
6668 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6669 .unwrap();
6670 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6671}
6672
6673#[gpui::test]
6674async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6675 init_test(cx, |_| {});
6676
6677 let mut cx = EditorTestContext::new(cx).await;
6678
6679 let large_body_1 = "\nd".repeat(200);
6680 let large_body_2 = "\ne".repeat(200);
6681
6682 cx.set_state(&format!(
6683 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6684 ));
6685 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6686 let scroll_position = editor.scroll_position(cx);
6687 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6688 scroll_position
6689 });
6690
6691 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6692 .unwrap();
6693 cx.assert_editor_state(&format!(
6694 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6695 ));
6696 let scroll_position_after_selection =
6697 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6698 assert_eq!(
6699 initial_scroll_position, scroll_position_after_selection,
6700 "Scroll position should not change after selecting all matches"
6701 );
6702}
6703
6704#[gpui::test]
6705async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6706 init_test(cx, |_| {});
6707
6708 let mut cx = EditorLspTestContext::new_rust(
6709 lsp::ServerCapabilities {
6710 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6711 ..Default::default()
6712 },
6713 cx,
6714 )
6715 .await;
6716
6717 cx.set_state(indoc! {"
6718 line 1
6719 line 2
6720 linˇe 3
6721 line 4
6722 line 5
6723 "});
6724
6725 // Make an edit
6726 cx.update_editor(|editor, window, cx| {
6727 editor.handle_input("X", window, cx);
6728 });
6729
6730 // Move cursor to a different position
6731 cx.update_editor(|editor, window, cx| {
6732 editor.change_selections(None, window, cx, |s| {
6733 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6734 });
6735 });
6736
6737 cx.assert_editor_state(indoc! {"
6738 line 1
6739 line 2
6740 linXe 3
6741 line 4
6742 liˇne 5
6743 "});
6744
6745 cx.lsp
6746 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6747 Ok(Some(vec![lsp::TextEdit::new(
6748 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6749 "PREFIX ".to_string(),
6750 )]))
6751 });
6752
6753 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6754 .unwrap()
6755 .await
6756 .unwrap();
6757
6758 cx.assert_editor_state(indoc! {"
6759 PREFIX line 1
6760 line 2
6761 linXe 3
6762 line 4
6763 liˇne 5
6764 "});
6765
6766 // Undo formatting
6767 cx.update_editor(|editor, window, cx| {
6768 editor.undo(&Default::default(), window, cx);
6769 });
6770
6771 // Verify cursor moved back to position after edit
6772 cx.assert_editor_state(indoc! {"
6773 line 1
6774 line 2
6775 linXˇe 3
6776 line 4
6777 line 5
6778 "});
6779}
6780
6781#[gpui::test]
6782async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6783 init_test(cx, |_| {});
6784
6785 let mut cx = EditorTestContext::new(cx).await;
6786
6787 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6788 cx.update_editor(|editor, window, cx| {
6789 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6790 });
6791
6792 cx.set_state(indoc! {"
6793 line 1
6794 line 2
6795 linˇe 3
6796 line 4
6797 line 5
6798 line 6
6799 line 7
6800 line 8
6801 line 9
6802 line 10
6803 "});
6804
6805 let snapshot = cx.buffer_snapshot();
6806 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6807
6808 cx.update(|_, cx| {
6809 provider.update(cx, |provider, _| {
6810 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6811 id: None,
6812 edits: vec![(edit_position..edit_position, "X".into())],
6813 edit_preview: None,
6814 }))
6815 })
6816 });
6817
6818 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6819 cx.update_editor(|editor, window, cx| {
6820 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6821 });
6822
6823 cx.assert_editor_state(indoc! {"
6824 line 1
6825 line 2
6826 lineXˇ 3
6827 line 4
6828 line 5
6829 line 6
6830 line 7
6831 line 8
6832 line 9
6833 line 10
6834 "});
6835
6836 cx.update_editor(|editor, window, cx| {
6837 editor.change_selections(None, window, cx, |s| {
6838 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6839 });
6840 });
6841
6842 cx.assert_editor_state(indoc! {"
6843 line 1
6844 line 2
6845 lineX 3
6846 line 4
6847 line 5
6848 line 6
6849 line 7
6850 line 8
6851 line 9
6852 liˇne 10
6853 "});
6854
6855 cx.update_editor(|editor, window, cx| {
6856 editor.undo(&Default::default(), window, cx);
6857 });
6858
6859 cx.assert_editor_state(indoc! {"
6860 line 1
6861 line 2
6862 lineˇ 3
6863 line 4
6864 line 5
6865 line 6
6866 line 7
6867 line 8
6868 line 9
6869 line 10
6870 "});
6871}
6872
6873#[gpui::test]
6874async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6875 init_test(cx, |_| {});
6876
6877 let mut cx = EditorTestContext::new(cx).await;
6878 cx.set_state(
6879 r#"let foo = 2;
6880lˇet foo = 2;
6881let fooˇ = 2;
6882let foo = 2;
6883let foo = ˇ2;"#,
6884 );
6885
6886 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6887 .unwrap();
6888 cx.assert_editor_state(
6889 r#"let foo = 2;
6890«letˇ» foo = 2;
6891let «fooˇ» = 2;
6892let foo = 2;
6893let foo = «2ˇ»;"#,
6894 );
6895
6896 // noop for multiple selections with different contents
6897 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6898 .unwrap();
6899 cx.assert_editor_state(
6900 r#"let foo = 2;
6901«letˇ» foo = 2;
6902let «fooˇ» = 2;
6903let foo = 2;
6904let foo = «2ˇ»;"#,
6905 );
6906
6907 // Test last selection direction should be preserved
6908 cx.set_state(
6909 r#"let foo = 2;
6910let foo = 2;
6911let «fooˇ» = 2;
6912let «ˇfoo» = 2;
6913let foo = 2;"#,
6914 );
6915
6916 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6917 .unwrap();
6918 cx.assert_editor_state(
6919 r#"let foo = 2;
6920let foo = 2;
6921let «fooˇ» = 2;
6922let «ˇfoo» = 2;
6923let «ˇfoo» = 2;"#,
6924 );
6925}
6926
6927#[gpui::test]
6928async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6929 init_test(cx, |_| {});
6930
6931 let mut cx =
6932 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6933
6934 cx.assert_editor_state(indoc! {"
6935 ˇbbb
6936 ccc
6937
6938 bbb
6939 ccc
6940 "});
6941 cx.dispatch_action(SelectPrevious::default());
6942 cx.assert_editor_state(indoc! {"
6943 «bbbˇ»
6944 ccc
6945
6946 bbb
6947 ccc
6948 "});
6949 cx.dispatch_action(SelectPrevious::default());
6950 cx.assert_editor_state(indoc! {"
6951 «bbbˇ»
6952 ccc
6953
6954 «bbbˇ»
6955 ccc
6956 "});
6957}
6958
6959#[gpui::test]
6960async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6961 init_test(cx, |_| {});
6962
6963 let mut cx = EditorTestContext::new(cx).await;
6964 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6965
6966 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6967 .unwrap();
6968 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6969
6970 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6971 .unwrap();
6972 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6973
6974 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6975 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6976
6977 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6978 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6979
6980 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6981 .unwrap();
6982 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6983
6984 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6985 .unwrap();
6986 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6987}
6988
6989#[gpui::test]
6990async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6991 init_test(cx, |_| {});
6992
6993 let mut cx = EditorTestContext::new(cx).await;
6994 cx.set_state("aˇ");
6995
6996 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6997 .unwrap();
6998 cx.assert_editor_state("«aˇ»");
6999 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7000 .unwrap();
7001 cx.assert_editor_state("«aˇ»");
7002}
7003
7004#[gpui::test]
7005async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7006 init_test(cx, |_| {});
7007
7008 let mut cx = EditorTestContext::new(cx).await;
7009 cx.set_state(
7010 r#"let foo = 2;
7011lˇet foo = 2;
7012let fooˇ = 2;
7013let foo = 2;
7014let foo = ˇ2;"#,
7015 );
7016
7017 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7018 .unwrap();
7019 cx.assert_editor_state(
7020 r#"let foo = 2;
7021«letˇ» foo = 2;
7022let «fooˇ» = 2;
7023let foo = 2;
7024let foo = «2ˇ»;"#,
7025 );
7026
7027 // noop for multiple selections with different contents
7028 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7029 .unwrap();
7030 cx.assert_editor_state(
7031 r#"let foo = 2;
7032«letˇ» foo = 2;
7033let «fooˇ» = 2;
7034let foo = 2;
7035let foo = «2ˇ»;"#,
7036 );
7037}
7038
7039#[gpui::test]
7040async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7041 init_test(cx, |_| {});
7042
7043 let mut cx = EditorTestContext::new(cx).await;
7044 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7045
7046 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7047 .unwrap();
7048 // selection direction is preserved
7049 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7050
7051 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7052 .unwrap();
7053 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7054
7055 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7056 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7057
7058 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7059 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7060
7061 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7062 .unwrap();
7063 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7064
7065 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7066 .unwrap();
7067 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7068}
7069
7070#[gpui::test]
7071async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7072 init_test(cx, |_| {});
7073
7074 let language = Arc::new(Language::new(
7075 LanguageConfig::default(),
7076 Some(tree_sitter_rust::LANGUAGE.into()),
7077 ));
7078
7079 let text = r#"
7080 use mod1::mod2::{mod3, mod4};
7081
7082 fn fn_1(param1: bool, param2: &str) {
7083 let var1 = "text";
7084 }
7085 "#
7086 .unindent();
7087
7088 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7089 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7090 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7091
7092 editor
7093 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7094 .await;
7095
7096 editor.update_in(cx, |editor, window, cx| {
7097 editor.change_selections(None, window, cx, |s| {
7098 s.select_display_ranges([
7099 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7100 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7101 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7102 ]);
7103 });
7104 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7105 });
7106 editor.update(cx, |editor, cx| {
7107 assert_text_with_selections(
7108 editor,
7109 indoc! {r#"
7110 use mod1::mod2::{mod3, «mod4ˇ»};
7111
7112 fn fn_1«ˇ(param1: bool, param2: &str)» {
7113 let var1 = "«ˇtext»";
7114 }
7115 "#},
7116 cx,
7117 );
7118 });
7119
7120 editor.update_in(cx, |editor, window, cx| {
7121 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7122 });
7123 editor.update(cx, |editor, cx| {
7124 assert_text_with_selections(
7125 editor,
7126 indoc! {r#"
7127 use mod1::mod2::«{mod3, mod4}ˇ»;
7128
7129 «ˇfn fn_1(param1: bool, param2: &str) {
7130 let var1 = "text";
7131 }»
7132 "#},
7133 cx,
7134 );
7135 });
7136
7137 editor.update_in(cx, |editor, window, cx| {
7138 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7139 });
7140 assert_eq!(
7141 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7142 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7143 );
7144
7145 // Trying to expand the selected syntax node one more time has no effect.
7146 editor.update_in(cx, |editor, window, cx| {
7147 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7148 });
7149 assert_eq!(
7150 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7151 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7152 );
7153
7154 editor.update_in(cx, |editor, window, cx| {
7155 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7156 });
7157 editor.update(cx, |editor, cx| {
7158 assert_text_with_selections(
7159 editor,
7160 indoc! {r#"
7161 use mod1::mod2::«{mod3, mod4}ˇ»;
7162
7163 «ˇfn fn_1(param1: bool, param2: &str) {
7164 let var1 = "text";
7165 }»
7166 "#},
7167 cx,
7168 );
7169 });
7170
7171 editor.update_in(cx, |editor, window, cx| {
7172 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7173 });
7174 editor.update(cx, |editor, cx| {
7175 assert_text_with_selections(
7176 editor,
7177 indoc! {r#"
7178 use mod1::mod2::{mod3, «mod4ˇ»};
7179
7180 fn fn_1«ˇ(param1: bool, param2: &str)» {
7181 let var1 = "«ˇtext»";
7182 }
7183 "#},
7184 cx,
7185 );
7186 });
7187
7188 editor.update_in(cx, |editor, window, cx| {
7189 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7190 });
7191 editor.update(cx, |editor, cx| {
7192 assert_text_with_selections(
7193 editor,
7194 indoc! {r#"
7195 use mod1::mod2::{mod3, mo«ˇ»d4};
7196
7197 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7198 let var1 = "te«ˇ»xt";
7199 }
7200 "#},
7201 cx,
7202 );
7203 });
7204
7205 // Trying to shrink the selected syntax node one more time has no effect.
7206 editor.update_in(cx, |editor, window, cx| {
7207 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7208 });
7209 editor.update_in(cx, |editor, _, cx| {
7210 assert_text_with_selections(
7211 editor,
7212 indoc! {r#"
7213 use mod1::mod2::{mod3, mo«ˇ»d4};
7214
7215 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7216 let var1 = "te«ˇ»xt";
7217 }
7218 "#},
7219 cx,
7220 );
7221 });
7222
7223 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7224 // a fold.
7225 editor.update_in(cx, |editor, window, cx| {
7226 editor.fold_creases(
7227 vec![
7228 Crease::simple(
7229 Point::new(0, 21)..Point::new(0, 24),
7230 FoldPlaceholder::test(),
7231 ),
7232 Crease::simple(
7233 Point::new(3, 20)..Point::new(3, 22),
7234 FoldPlaceholder::test(),
7235 ),
7236 ],
7237 true,
7238 window,
7239 cx,
7240 );
7241 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7242 });
7243 editor.update(cx, |editor, cx| {
7244 assert_text_with_selections(
7245 editor,
7246 indoc! {r#"
7247 use mod1::mod2::«{mod3, mod4}ˇ»;
7248
7249 fn fn_1«ˇ(param1: bool, param2: &str)» {
7250 let var1 = "«ˇtext»";
7251 }
7252 "#},
7253 cx,
7254 );
7255 });
7256}
7257
7258#[gpui::test]
7259async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7260 init_test(cx, |_| {});
7261
7262 let language = Arc::new(Language::new(
7263 LanguageConfig::default(),
7264 Some(tree_sitter_rust::LANGUAGE.into()),
7265 ));
7266
7267 let text = "let a = 2;";
7268
7269 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7270 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7271 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7272
7273 editor
7274 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7275 .await;
7276
7277 // Test case 1: Cursor at end of word
7278 editor.update_in(cx, |editor, window, cx| {
7279 editor.change_selections(None, window, cx, |s| {
7280 s.select_display_ranges([
7281 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7282 ]);
7283 });
7284 });
7285 editor.update(cx, |editor, cx| {
7286 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7287 });
7288 editor.update_in(cx, |editor, window, cx| {
7289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7290 });
7291 editor.update(cx, |editor, cx| {
7292 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7293 });
7294 editor.update_in(cx, |editor, window, cx| {
7295 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7296 });
7297 editor.update(cx, |editor, cx| {
7298 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7299 });
7300
7301 // Test case 2: Cursor at end of statement
7302 editor.update_in(cx, |editor, window, cx| {
7303 editor.change_selections(None, window, cx, |s| {
7304 s.select_display_ranges([
7305 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7306 ]);
7307 });
7308 });
7309 editor.update(cx, |editor, cx| {
7310 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7311 });
7312 editor.update_in(cx, |editor, window, cx| {
7313 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7314 });
7315 editor.update(cx, |editor, cx| {
7316 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7317 });
7318}
7319
7320#[gpui::test]
7321async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7322 init_test(cx, |_| {});
7323
7324 let language = Arc::new(Language::new(
7325 LanguageConfig::default(),
7326 Some(tree_sitter_rust::LANGUAGE.into()),
7327 ));
7328
7329 let text = r#"
7330 use mod1::mod2::{mod3, mod4};
7331
7332 fn fn_1(param1: bool, param2: &str) {
7333 let var1 = "hello world";
7334 }
7335 "#
7336 .unindent();
7337
7338 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7339 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7340 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7341
7342 editor
7343 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7344 .await;
7345
7346 // Test 1: Cursor on a letter of a string word
7347 editor.update_in(cx, |editor, window, cx| {
7348 editor.change_selections(None, window, cx, |s| {
7349 s.select_display_ranges([
7350 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7351 ]);
7352 });
7353 });
7354 editor.update_in(cx, |editor, window, cx| {
7355 assert_text_with_selections(
7356 editor,
7357 indoc! {r#"
7358 use mod1::mod2::{mod3, mod4};
7359
7360 fn fn_1(param1: bool, param2: &str) {
7361 let var1 = "hˇello world";
7362 }
7363 "#},
7364 cx,
7365 );
7366 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7367 assert_text_with_selections(
7368 editor,
7369 indoc! {r#"
7370 use mod1::mod2::{mod3, mod4};
7371
7372 fn fn_1(param1: bool, param2: &str) {
7373 let var1 = "«ˇhello» world";
7374 }
7375 "#},
7376 cx,
7377 );
7378 });
7379
7380 // Test 2: Partial selection within a word
7381 editor.update_in(cx, |editor, window, cx| {
7382 editor.change_selections(None, window, cx, |s| {
7383 s.select_display_ranges([
7384 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7385 ]);
7386 });
7387 });
7388 editor.update_in(cx, |editor, window, cx| {
7389 assert_text_with_selections(
7390 editor,
7391 indoc! {r#"
7392 use mod1::mod2::{mod3, mod4};
7393
7394 fn fn_1(param1: bool, param2: &str) {
7395 let var1 = "h«elˇ»lo world";
7396 }
7397 "#},
7398 cx,
7399 );
7400 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7401 assert_text_with_selections(
7402 editor,
7403 indoc! {r#"
7404 use mod1::mod2::{mod3, mod4};
7405
7406 fn fn_1(param1: bool, param2: &str) {
7407 let var1 = "«ˇhello» world";
7408 }
7409 "#},
7410 cx,
7411 );
7412 });
7413
7414 // Test 3: Complete word already selected
7415 editor.update_in(cx, |editor, window, cx| {
7416 editor.change_selections(None, window, cx, |s| {
7417 s.select_display_ranges([
7418 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7419 ]);
7420 });
7421 });
7422 editor.update_in(cx, |editor, window, cx| {
7423 assert_text_with_selections(
7424 editor,
7425 indoc! {r#"
7426 use mod1::mod2::{mod3, mod4};
7427
7428 fn fn_1(param1: bool, param2: &str) {
7429 let var1 = "«helloˇ» world";
7430 }
7431 "#},
7432 cx,
7433 );
7434 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7435 assert_text_with_selections(
7436 editor,
7437 indoc! {r#"
7438 use mod1::mod2::{mod3, mod4};
7439
7440 fn fn_1(param1: bool, param2: &str) {
7441 let var1 = "«hello worldˇ»";
7442 }
7443 "#},
7444 cx,
7445 );
7446 });
7447
7448 // Test 4: Selection spanning across words
7449 editor.update_in(cx, |editor, window, cx| {
7450 editor.change_selections(None, window, cx, |s| {
7451 s.select_display_ranges([
7452 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7453 ]);
7454 });
7455 });
7456 editor.update_in(cx, |editor, window, cx| {
7457 assert_text_with_selections(
7458 editor,
7459 indoc! {r#"
7460 use mod1::mod2::{mod3, mod4};
7461
7462 fn fn_1(param1: bool, param2: &str) {
7463 let var1 = "hel«lo woˇ»rld";
7464 }
7465 "#},
7466 cx,
7467 );
7468 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7469 assert_text_with_selections(
7470 editor,
7471 indoc! {r#"
7472 use mod1::mod2::{mod3, mod4};
7473
7474 fn fn_1(param1: bool, param2: &str) {
7475 let var1 = "«ˇhello world»";
7476 }
7477 "#},
7478 cx,
7479 );
7480 });
7481
7482 // Test 5: Expansion beyond string
7483 editor.update_in(cx, |editor, window, cx| {
7484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7486 assert_text_with_selections(
7487 editor,
7488 indoc! {r#"
7489 use mod1::mod2::{mod3, mod4};
7490
7491 fn fn_1(param1: bool, param2: &str) {
7492 «ˇlet var1 = "hello world";»
7493 }
7494 "#},
7495 cx,
7496 );
7497 });
7498}
7499
7500#[gpui::test]
7501async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7502 init_test(cx, |_| {});
7503
7504 let base_text = r#"
7505 impl A {
7506 // this is an uncommitted comment
7507
7508 fn b() {
7509 c();
7510 }
7511
7512 // this is another uncommitted comment
7513
7514 fn d() {
7515 // e
7516 // f
7517 }
7518 }
7519
7520 fn g() {
7521 // h
7522 }
7523 "#
7524 .unindent();
7525
7526 let text = r#"
7527 ˇimpl A {
7528
7529 fn b() {
7530 c();
7531 }
7532
7533 fn d() {
7534 // e
7535 // f
7536 }
7537 }
7538
7539 fn g() {
7540 // h
7541 }
7542 "#
7543 .unindent();
7544
7545 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7546 cx.set_state(&text);
7547 cx.set_head_text(&base_text);
7548 cx.update_editor(|editor, window, cx| {
7549 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7550 });
7551
7552 cx.assert_state_with_diff(
7553 "
7554 ˇimpl A {
7555 - // this is an uncommitted comment
7556
7557 fn b() {
7558 c();
7559 }
7560
7561 - // this is another uncommitted comment
7562 -
7563 fn d() {
7564 // e
7565 // f
7566 }
7567 }
7568
7569 fn g() {
7570 // h
7571 }
7572 "
7573 .unindent(),
7574 );
7575
7576 let expected_display_text = "
7577 impl A {
7578 // this is an uncommitted comment
7579
7580 fn b() {
7581 ⋯
7582 }
7583
7584 // this is another uncommitted comment
7585
7586 fn d() {
7587 ⋯
7588 }
7589 }
7590
7591 fn g() {
7592 ⋯
7593 }
7594 "
7595 .unindent();
7596
7597 cx.update_editor(|editor, window, cx| {
7598 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7599 assert_eq!(editor.display_text(cx), expected_display_text);
7600 });
7601}
7602
7603#[gpui::test]
7604async fn test_autoindent(cx: &mut TestAppContext) {
7605 init_test(cx, |_| {});
7606
7607 let language = Arc::new(
7608 Language::new(
7609 LanguageConfig {
7610 brackets: BracketPairConfig {
7611 pairs: vec![
7612 BracketPair {
7613 start: "{".to_string(),
7614 end: "}".to_string(),
7615 close: false,
7616 surround: false,
7617 newline: true,
7618 },
7619 BracketPair {
7620 start: "(".to_string(),
7621 end: ")".to_string(),
7622 close: false,
7623 surround: false,
7624 newline: true,
7625 },
7626 ],
7627 ..Default::default()
7628 },
7629 ..Default::default()
7630 },
7631 Some(tree_sitter_rust::LANGUAGE.into()),
7632 )
7633 .with_indents_query(
7634 r#"
7635 (_ "(" ")" @end) @indent
7636 (_ "{" "}" @end) @indent
7637 "#,
7638 )
7639 .unwrap(),
7640 );
7641
7642 let text = "fn a() {}";
7643
7644 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7645 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7646 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7647 editor
7648 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7649 .await;
7650
7651 editor.update_in(cx, |editor, window, cx| {
7652 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7653 editor.newline(&Newline, window, cx);
7654 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7655 assert_eq!(
7656 editor.selections.ranges(cx),
7657 &[
7658 Point::new(1, 4)..Point::new(1, 4),
7659 Point::new(3, 4)..Point::new(3, 4),
7660 Point::new(5, 0)..Point::new(5, 0)
7661 ]
7662 );
7663 });
7664}
7665
7666#[gpui::test]
7667async fn test_autoindent_selections(cx: &mut TestAppContext) {
7668 init_test(cx, |_| {});
7669
7670 {
7671 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7672 cx.set_state(indoc! {"
7673 impl A {
7674
7675 fn b() {}
7676
7677 «fn c() {
7678
7679 }ˇ»
7680 }
7681 "});
7682
7683 cx.update_editor(|editor, window, cx| {
7684 editor.autoindent(&Default::default(), window, cx);
7685 });
7686
7687 cx.assert_editor_state(indoc! {"
7688 impl A {
7689
7690 fn b() {}
7691
7692 «fn c() {
7693
7694 }ˇ»
7695 }
7696 "});
7697 }
7698
7699 {
7700 let mut cx = EditorTestContext::new_multibuffer(
7701 cx,
7702 [indoc! { "
7703 impl A {
7704 «
7705 // a
7706 fn b(){}
7707 »
7708 «
7709 }
7710 fn c(){}
7711 »
7712 "}],
7713 );
7714
7715 let buffer = cx.update_editor(|editor, _, cx| {
7716 let buffer = editor.buffer().update(cx, |buffer, _| {
7717 buffer.all_buffers().iter().next().unwrap().clone()
7718 });
7719 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7720 buffer
7721 });
7722
7723 cx.run_until_parked();
7724 cx.update_editor(|editor, window, cx| {
7725 editor.select_all(&Default::default(), window, cx);
7726 editor.autoindent(&Default::default(), window, cx)
7727 });
7728 cx.run_until_parked();
7729
7730 cx.update(|_, cx| {
7731 assert_eq!(
7732 buffer.read(cx).text(),
7733 indoc! { "
7734 impl A {
7735
7736 // a
7737 fn b(){}
7738
7739
7740 }
7741 fn c(){}
7742
7743 " }
7744 )
7745 });
7746 }
7747}
7748
7749#[gpui::test]
7750async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7751 init_test(cx, |_| {});
7752
7753 let mut cx = EditorTestContext::new(cx).await;
7754
7755 let language = Arc::new(Language::new(
7756 LanguageConfig {
7757 brackets: BracketPairConfig {
7758 pairs: vec![
7759 BracketPair {
7760 start: "{".to_string(),
7761 end: "}".to_string(),
7762 close: true,
7763 surround: true,
7764 newline: true,
7765 },
7766 BracketPair {
7767 start: "(".to_string(),
7768 end: ")".to_string(),
7769 close: true,
7770 surround: true,
7771 newline: true,
7772 },
7773 BracketPair {
7774 start: "/*".to_string(),
7775 end: " */".to_string(),
7776 close: true,
7777 surround: true,
7778 newline: true,
7779 },
7780 BracketPair {
7781 start: "[".to_string(),
7782 end: "]".to_string(),
7783 close: false,
7784 surround: false,
7785 newline: true,
7786 },
7787 BracketPair {
7788 start: "\"".to_string(),
7789 end: "\"".to_string(),
7790 close: true,
7791 surround: true,
7792 newline: false,
7793 },
7794 BracketPair {
7795 start: "<".to_string(),
7796 end: ">".to_string(),
7797 close: false,
7798 surround: true,
7799 newline: true,
7800 },
7801 ],
7802 ..Default::default()
7803 },
7804 autoclose_before: "})]".to_string(),
7805 ..Default::default()
7806 },
7807 Some(tree_sitter_rust::LANGUAGE.into()),
7808 ));
7809
7810 cx.language_registry().add(language.clone());
7811 cx.update_buffer(|buffer, cx| {
7812 buffer.set_language(Some(language), cx);
7813 });
7814
7815 cx.set_state(
7816 &r#"
7817 🏀ˇ
7818 εˇ
7819 ❤️ˇ
7820 "#
7821 .unindent(),
7822 );
7823
7824 // autoclose multiple nested brackets at multiple cursors
7825 cx.update_editor(|editor, window, cx| {
7826 editor.handle_input("{", window, cx);
7827 editor.handle_input("{", window, cx);
7828 editor.handle_input("{", window, cx);
7829 });
7830 cx.assert_editor_state(
7831 &"
7832 🏀{{{ˇ}}}
7833 ε{{{ˇ}}}
7834 ❤️{{{ˇ}}}
7835 "
7836 .unindent(),
7837 );
7838
7839 // insert a different closing bracket
7840 cx.update_editor(|editor, window, cx| {
7841 editor.handle_input(")", window, cx);
7842 });
7843 cx.assert_editor_state(
7844 &"
7845 🏀{{{)ˇ}}}
7846 ε{{{)ˇ}}}
7847 ❤️{{{)ˇ}}}
7848 "
7849 .unindent(),
7850 );
7851
7852 // skip over the auto-closed brackets when typing a closing bracket
7853 cx.update_editor(|editor, window, cx| {
7854 editor.move_right(&MoveRight, window, cx);
7855 editor.handle_input("}", window, cx);
7856 editor.handle_input("}", window, cx);
7857 editor.handle_input("}", window, cx);
7858 });
7859 cx.assert_editor_state(
7860 &"
7861 🏀{{{)}}}}ˇ
7862 ε{{{)}}}}ˇ
7863 ❤️{{{)}}}}ˇ
7864 "
7865 .unindent(),
7866 );
7867
7868 // autoclose multi-character pairs
7869 cx.set_state(
7870 &"
7871 ˇ
7872 ˇ
7873 "
7874 .unindent(),
7875 );
7876 cx.update_editor(|editor, window, cx| {
7877 editor.handle_input("/", window, cx);
7878 editor.handle_input("*", window, cx);
7879 });
7880 cx.assert_editor_state(
7881 &"
7882 /*ˇ */
7883 /*ˇ */
7884 "
7885 .unindent(),
7886 );
7887
7888 // one cursor autocloses a multi-character pair, one cursor
7889 // does not autoclose.
7890 cx.set_state(
7891 &"
7892 /ˇ
7893 ˇ
7894 "
7895 .unindent(),
7896 );
7897 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7898 cx.assert_editor_state(
7899 &"
7900 /*ˇ */
7901 *ˇ
7902 "
7903 .unindent(),
7904 );
7905
7906 // Don't autoclose if the next character isn't whitespace and isn't
7907 // listed in the language's "autoclose_before" section.
7908 cx.set_state("ˇa b");
7909 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7910 cx.assert_editor_state("{ˇa b");
7911
7912 // Don't autoclose if `close` is false for the bracket pair
7913 cx.set_state("ˇ");
7914 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7915 cx.assert_editor_state("[ˇ");
7916
7917 // Surround with brackets if text is selected
7918 cx.set_state("«aˇ» b");
7919 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7920 cx.assert_editor_state("{«aˇ»} b");
7921
7922 // Autoclose when not immediately after a word character
7923 cx.set_state("a ˇ");
7924 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7925 cx.assert_editor_state("a \"ˇ\"");
7926
7927 // Autoclose pair where the start and end characters are the same
7928 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7929 cx.assert_editor_state("a \"\"ˇ");
7930
7931 // Don't autoclose when immediately after a word character
7932 cx.set_state("aˇ");
7933 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7934 cx.assert_editor_state("a\"ˇ");
7935
7936 // Do autoclose when after a non-word character
7937 cx.set_state("{ˇ");
7938 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7939 cx.assert_editor_state("{\"ˇ\"");
7940
7941 // Non identical pairs autoclose regardless of preceding character
7942 cx.set_state("aˇ");
7943 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7944 cx.assert_editor_state("a{ˇ}");
7945
7946 // Don't autoclose pair if autoclose is disabled
7947 cx.set_state("ˇ");
7948 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7949 cx.assert_editor_state("<ˇ");
7950
7951 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7952 cx.set_state("«aˇ» b");
7953 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7954 cx.assert_editor_state("<«aˇ»> b");
7955}
7956
7957#[gpui::test]
7958async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7959 init_test(cx, |settings| {
7960 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7961 });
7962
7963 let mut cx = EditorTestContext::new(cx).await;
7964
7965 let language = Arc::new(Language::new(
7966 LanguageConfig {
7967 brackets: BracketPairConfig {
7968 pairs: vec![
7969 BracketPair {
7970 start: "{".to_string(),
7971 end: "}".to_string(),
7972 close: true,
7973 surround: true,
7974 newline: true,
7975 },
7976 BracketPair {
7977 start: "(".to_string(),
7978 end: ")".to_string(),
7979 close: true,
7980 surround: true,
7981 newline: true,
7982 },
7983 BracketPair {
7984 start: "[".to_string(),
7985 end: "]".to_string(),
7986 close: false,
7987 surround: false,
7988 newline: true,
7989 },
7990 ],
7991 ..Default::default()
7992 },
7993 autoclose_before: "})]".to_string(),
7994 ..Default::default()
7995 },
7996 Some(tree_sitter_rust::LANGUAGE.into()),
7997 ));
7998
7999 cx.language_registry().add(language.clone());
8000 cx.update_buffer(|buffer, cx| {
8001 buffer.set_language(Some(language), cx);
8002 });
8003
8004 cx.set_state(
8005 &"
8006 ˇ
8007 ˇ
8008 ˇ
8009 "
8010 .unindent(),
8011 );
8012
8013 // ensure only matching closing brackets are skipped over
8014 cx.update_editor(|editor, window, cx| {
8015 editor.handle_input("}", window, cx);
8016 editor.move_left(&MoveLeft, window, cx);
8017 editor.handle_input(")", window, cx);
8018 editor.move_left(&MoveLeft, window, cx);
8019 });
8020 cx.assert_editor_state(
8021 &"
8022 ˇ)}
8023 ˇ)}
8024 ˇ)}
8025 "
8026 .unindent(),
8027 );
8028
8029 // skip-over closing brackets at multiple cursors
8030 cx.update_editor(|editor, window, cx| {
8031 editor.handle_input(")", window, cx);
8032 editor.handle_input("}", window, cx);
8033 });
8034 cx.assert_editor_state(
8035 &"
8036 )}ˇ
8037 )}ˇ
8038 )}ˇ
8039 "
8040 .unindent(),
8041 );
8042
8043 // ignore non-close brackets
8044 cx.update_editor(|editor, window, cx| {
8045 editor.handle_input("]", window, cx);
8046 editor.move_left(&MoveLeft, window, cx);
8047 editor.handle_input("]", window, cx);
8048 });
8049 cx.assert_editor_state(
8050 &"
8051 )}]ˇ]
8052 )}]ˇ]
8053 )}]ˇ]
8054 "
8055 .unindent(),
8056 );
8057}
8058
8059#[gpui::test]
8060async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8061 init_test(cx, |_| {});
8062
8063 let mut cx = EditorTestContext::new(cx).await;
8064
8065 let html_language = Arc::new(
8066 Language::new(
8067 LanguageConfig {
8068 name: "HTML".into(),
8069 brackets: BracketPairConfig {
8070 pairs: vec![
8071 BracketPair {
8072 start: "<".into(),
8073 end: ">".into(),
8074 close: true,
8075 ..Default::default()
8076 },
8077 BracketPair {
8078 start: "{".into(),
8079 end: "}".into(),
8080 close: true,
8081 ..Default::default()
8082 },
8083 BracketPair {
8084 start: "(".into(),
8085 end: ")".into(),
8086 close: true,
8087 ..Default::default()
8088 },
8089 ],
8090 ..Default::default()
8091 },
8092 autoclose_before: "})]>".into(),
8093 ..Default::default()
8094 },
8095 Some(tree_sitter_html::LANGUAGE.into()),
8096 )
8097 .with_injection_query(
8098 r#"
8099 (script_element
8100 (raw_text) @injection.content
8101 (#set! injection.language "javascript"))
8102 "#,
8103 )
8104 .unwrap(),
8105 );
8106
8107 let javascript_language = Arc::new(Language::new(
8108 LanguageConfig {
8109 name: "JavaScript".into(),
8110 brackets: BracketPairConfig {
8111 pairs: vec![
8112 BracketPair {
8113 start: "/*".into(),
8114 end: " */".into(),
8115 close: true,
8116 ..Default::default()
8117 },
8118 BracketPair {
8119 start: "{".into(),
8120 end: "}".into(),
8121 close: true,
8122 ..Default::default()
8123 },
8124 BracketPair {
8125 start: "(".into(),
8126 end: ")".into(),
8127 close: true,
8128 ..Default::default()
8129 },
8130 ],
8131 ..Default::default()
8132 },
8133 autoclose_before: "})]>".into(),
8134 ..Default::default()
8135 },
8136 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8137 ));
8138
8139 cx.language_registry().add(html_language.clone());
8140 cx.language_registry().add(javascript_language.clone());
8141
8142 cx.update_buffer(|buffer, cx| {
8143 buffer.set_language(Some(html_language), cx);
8144 });
8145
8146 cx.set_state(
8147 &r#"
8148 <body>ˇ
8149 <script>
8150 var x = 1;ˇ
8151 </script>
8152 </body>ˇ
8153 "#
8154 .unindent(),
8155 );
8156
8157 // Precondition: different languages are active at different locations.
8158 cx.update_editor(|editor, window, cx| {
8159 let snapshot = editor.snapshot(window, cx);
8160 let cursors = editor.selections.ranges::<usize>(cx);
8161 let languages = cursors
8162 .iter()
8163 .map(|c| snapshot.language_at(c.start).unwrap().name())
8164 .collect::<Vec<_>>();
8165 assert_eq!(
8166 languages,
8167 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8168 );
8169 });
8170
8171 // Angle brackets autoclose in HTML, but not JavaScript.
8172 cx.update_editor(|editor, window, cx| {
8173 editor.handle_input("<", window, cx);
8174 editor.handle_input("a", window, cx);
8175 });
8176 cx.assert_editor_state(
8177 &r#"
8178 <body><aˇ>
8179 <script>
8180 var x = 1;<aˇ
8181 </script>
8182 </body><aˇ>
8183 "#
8184 .unindent(),
8185 );
8186
8187 // Curly braces and parens autoclose in both HTML and JavaScript.
8188 cx.update_editor(|editor, window, cx| {
8189 editor.handle_input(" b=", window, cx);
8190 editor.handle_input("{", window, cx);
8191 editor.handle_input("c", window, cx);
8192 editor.handle_input("(", window, cx);
8193 });
8194 cx.assert_editor_state(
8195 &r#"
8196 <body><a b={c(ˇ)}>
8197 <script>
8198 var x = 1;<a b={c(ˇ)}
8199 </script>
8200 </body><a b={c(ˇ)}>
8201 "#
8202 .unindent(),
8203 );
8204
8205 // Brackets that were already autoclosed are skipped.
8206 cx.update_editor(|editor, window, cx| {
8207 editor.handle_input(")", window, cx);
8208 editor.handle_input("d", window, cx);
8209 editor.handle_input("}", window, cx);
8210 });
8211 cx.assert_editor_state(
8212 &r#"
8213 <body><a b={c()d}ˇ>
8214 <script>
8215 var x = 1;<a b={c()d}ˇ
8216 </script>
8217 </body><a b={c()d}ˇ>
8218 "#
8219 .unindent(),
8220 );
8221 cx.update_editor(|editor, window, cx| {
8222 editor.handle_input(">", window, cx);
8223 });
8224 cx.assert_editor_state(
8225 &r#"
8226 <body><a b={c()d}>ˇ
8227 <script>
8228 var x = 1;<a b={c()d}>ˇ
8229 </script>
8230 </body><a b={c()d}>ˇ
8231 "#
8232 .unindent(),
8233 );
8234
8235 // Reset
8236 cx.set_state(
8237 &r#"
8238 <body>ˇ
8239 <script>
8240 var x = 1;ˇ
8241 </script>
8242 </body>ˇ
8243 "#
8244 .unindent(),
8245 );
8246
8247 cx.update_editor(|editor, window, cx| {
8248 editor.handle_input("<", window, cx);
8249 });
8250 cx.assert_editor_state(
8251 &r#"
8252 <body><ˇ>
8253 <script>
8254 var x = 1;<ˇ
8255 </script>
8256 </body><ˇ>
8257 "#
8258 .unindent(),
8259 );
8260
8261 // When backspacing, the closing angle brackets are removed.
8262 cx.update_editor(|editor, window, cx| {
8263 editor.backspace(&Backspace, window, cx);
8264 });
8265 cx.assert_editor_state(
8266 &r#"
8267 <body>ˇ
8268 <script>
8269 var x = 1;ˇ
8270 </script>
8271 </body>ˇ
8272 "#
8273 .unindent(),
8274 );
8275
8276 // Block comments autoclose in JavaScript, but not HTML.
8277 cx.update_editor(|editor, window, cx| {
8278 editor.handle_input("/", window, cx);
8279 editor.handle_input("*", window, cx);
8280 });
8281 cx.assert_editor_state(
8282 &r#"
8283 <body>/*ˇ
8284 <script>
8285 var x = 1;/*ˇ */
8286 </script>
8287 </body>/*ˇ
8288 "#
8289 .unindent(),
8290 );
8291}
8292
8293#[gpui::test]
8294async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8295 init_test(cx, |_| {});
8296
8297 let mut cx = EditorTestContext::new(cx).await;
8298
8299 let rust_language = Arc::new(
8300 Language::new(
8301 LanguageConfig {
8302 name: "Rust".into(),
8303 brackets: serde_json::from_value(json!([
8304 { "start": "{", "end": "}", "close": true, "newline": true },
8305 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8306 ]))
8307 .unwrap(),
8308 autoclose_before: "})]>".into(),
8309 ..Default::default()
8310 },
8311 Some(tree_sitter_rust::LANGUAGE.into()),
8312 )
8313 .with_override_query("(string_literal) @string")
8314 .unwrap(),
8315 );
8316
8317 cx.language_registry().add(rust_language.clone());
8318 cx.update_buffer(|buffer, cx| {
8319 buffer.set_language(Some(rust_language), cx);
8320 });
8321
8322 cx.set_state(
8323 &r#"
8324 let x = ˇ
8325 "#
8326 .unindent(),
8327 );
8328
8329 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8330 cx.update_editor(|editor, window, cx| {
8331 editor.handle_input("\"", window, cx);
8332 });
8333 cx.assert_editor_state(
8334 &r#"
8335 let x = "ˇ"
8336 "#
8337 .unindent(),
8338 );
8339
8340 // Inserting another quotation mark. The cursor moves across the existing
8341 // automatically-inserted quotation mark.
8342 cx.update_editor(|editor, window, cx| {
8343 editor.handle_input("\"", window, cx);
8344 });
8345 cx.assert_editor_state(
8346 &r#"
8347 let x = ""ˇ
8348 "#
8349 .unindent(),
8350 );
8351
8352 // Reset
8353 cx.set_state(
8354 &r#"
8355 let x = ˇ
8356 "#
8357 .unindent(),
8358 );
8359
8360 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8361 cx.update_editor(|editor, window, cx| {
8362 editor.handle_input("\"", window, cx);
8363 editor.handle_input(" ", window, cx);
8364 editor.move_left(&Default::default(), window, cx);
8365 editor.handle_input("\\", window, cx);
8366 editor.handle_input("\"", window, cx);
8367 });
8368 cx.assert_editor_state(
8369 &r#"
8370 let x = "\"ˇ "
8371 "#
8372 .unindent(),
8373 );
8374
8375 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8376 // mark. Nothing is inserted.
8377 cx.update_editor(|editor, window, cx| {
8378 editor.move_right(&Default::default(), window, cx);
8379 editor.handle_input("\"", window, cx);
8380 });
8381 cx.assert_editor_state(
8382 &r#"
8383 let x = "\" "ˇ
8384 "#
8385 .unindent(),
8386 );
8387}
8388
8389#[gpui::test]
8390async fn test_surround_with_pair(cx: &mut TestAppContext) {
8391 init_test(cx, |_| {});
8392
8393 let language = Arc::new(Language::new(
8394 LanguageConfig {
8395 brackets: BracketPairConfig {
8396 pairs: vec![
8397 BracketPair {
8398 start: "{".to_string(),
8399 end: "}".to_string(),
8400 close: true,
8401 surround: true,
8402 newline: true,
8403 },
8404 BracketPair {
8405 start: "/* ".to_string(),
8406 end: "*/".to_string(),
8407 close: true,
8408 surround: true,
8409 ..Default::default()
8410 },
8411 ],
8412 ..Default::default()
8413 },
8414 ..Default::default()
8415 },
8416 Some(tree_sitter_rust::LANGUAGE.into()),
8417 ));
8418
8419 let text = r#"
8420 a
8421 b
8422 c
8423 "#
8424 .unindent();
8425
8426 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8428 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8429 editor
8430 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8431 .await;
8432
8433 editor.update_in(cx, |editor, window, cx| {
8434 editor.change_selections(None, window, cx, |s| {
8435 s.select_display_ranges([
8436 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8437 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8438 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8439 ])
8440 });
8441
8442 editor.handle_input("{", window, cx);
8443 editor.handle_input("{", window, cx);
8444 editor.handle_input("{", window, cx);
8445 assert_eq!(
8446 editor.text(cx),
8447 "
8448 {{{a}}}
8449 {{{b}}}
8450 {{{c}}}
8451 "
8452 .unindent()
8453 );
8454 assert_eq!(
8455 editor.selections.display_ranges(cx),
8456 [
8457 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8458 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8459 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8460 ]
8461 );
8462
8463 editor.undo(&Undo, window, cx);
8464 editor.undo(&Undo, window, cx);
8465 editor.undo(&Undo, window, cx);
8466 assert_eq!(
8467 editor.text(cx),
8468 "
8469 a
8470 b
8471 c
8472 "
8473 .unindent()
8474 );
8475 assert_eq!(
8476 editor.selections.display_ranges(cx),
8477 [
8478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8479 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8480 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8481 ]
8482 );
8483
8484 // Ensure inserting the first character of a multi-byte bracket pair
8485 // doesn't surround the selections with the bracket.
8486 editor.handle_input("/", window, cx);
8487 assert_eq!(
8488 editor.text(cx),
8489 "
8490 /
8491 /
8492 /
8493 "
8494 .unindent()
8495 );
8496 assert_eq!(
8497 editor.selections.display_ranges(cx),
8498 [
8499 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8500 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8502 ]
8503 );
8504
8505 editor.undo(&Undo, window, cx);
8506 assert_eq!(
8507 editor.text(cx),
8508 "
8509 a
8510 b
8511 c
8512 "
8513 .unindent()
8514 );
8515 assert_eq!(
8516 editor.selections.display_ranges(cx),
8517 [
8518 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8519 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8520 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8521 ]
8522 );
8523
8524 // Ensure inserting the last character of a multi-byte bracket pair
8525 // doesn't surround the selections with the bracket.
8526 editor.handle_input("*", window, cx);
8527 assert_eq!(
8528 editor.text(cx),
8529 "
8530 *
8531 *
8532 *
8533 "
8534 .unindent()
8535 );
8536 assert_eq!(
8537 editor.selections.display_ranges(cx),
8538 [
8539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8540 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8541 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8542 ]
8543 );
8544 });
8545}
8546
8547#[gpui::test]
8548async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8549 init_test(cx, |_| {});
8550
8551 let language = Arc::new(Language::new(
8552 LanguageConfig {
8553 brackets: BracketPairConfig {
8554 pairs: vec![BracketPair {
8555 start: "{".to_string(),
8556 end: "}".to_string(),
8557 close: true,
8558 surround: true,
8559 newline: true,
8560 }],
8561 ..Default::default()
8562 },
8563 autoclose_before: "}".to_string(),
8564 ..Default::default()
8565 },
8566 Some(tree_sitter_rust::LANGUAGE.into()),
8567 ));
8568
8569 let text = r#"
8570 a
8571 b
8572 c
8573 "#
8574 .unindent();
8575
8576 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8577 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8578 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8579 editor
8580 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8581 .await;
8582
8583 editor.update_in(cx, |editor, window, cx| {
8584 editor.change_selections(None, window, cx, |s| {
8585 s.select_ranges([
8586 Point::new(0, 1)..Point::new(0, 1),
8587 Point::new(1, 1)..Point::new(1, 1),
8588 Point::new(2, 1)..Point::new(2, 1),
8589 ])
8590 });
8591
8592 editor.handle_input("{", window, cx);
8593 editor.handle_input("{", window, cx);
8594 editor.handle_input("_", window, cx);
8595 assert_eq!(
8596 editor.text(cx),
8597 "
8598 a{{_}}
8599 b{{_}}
8600 c{{_}}
8601 "
8602 .unindent()
8603 );
8604 assert_eq!(
8605 editor.selections.ranges::<Point>(cx),
8606 [
8607 Point::new(0, 4)..Point::new(0, 4),
8608 Point::new(1, 4)..Point::new(1, 4),
8609 Point::new(2, 4)..Point::new(2, 4)
8610 ]
8611 );
8612
8613 editor.backspace(&Default::default(), window, cx);
8614 editor.backspace(&Default::default(), window, cx);
8615 assert_eq!(
8616 editor.text(cx),
8617 "
8618 a{}
8619 b{}
8620 c{}
8621 "
8622 .unindent()
8623 );
8624 assert_eq!(
8625 editor.selections.ranges::<Point>(cx),
8626 [
8627 Point::new(0, 2)..Point::new(0, 2),
8628 Point::new(1, 2)..Point::new(1, 2),
8629 Point::new(2, 2)..Point::new(2, 2)
8630 ]
8631 );
8632
8633 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8634 assert_eq!(
8635 editor.text(cx),
8636 "
8637 a
8638 b
8639 c
8640 "
8641 .unindent()
8642 );
8643 assert_eq!(
8644 editor.selections.ranges::<Point>(cx),
8645 [
8646 Point::new(0, 1)..Point::new(0, 1),
8647 Point::new(1, 1)..Point::new(1, 1),
8648 Point::new(2, 1)..Point::new(2, 1)
8649 ]
8650 );
8651 });
8652}
8653
8654#[gpui::test]
8655async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8656 init_test(cx, |settings| {
8657 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8658 });
8659
8660 let mut cx = EditorTestContext::new(cx).await;
8661
8662 let language = Arc::new(Language::new(
8663 LanguageConfig {
8664 brackets: BracketPairConfig {
8665 pairs: vec![
8666 BracketPair {
8667 start: "{".to_string(),
8668 end: "}".to_string(),
8669 close: true,
8670 surround: true,
8671 newline: true,
8672 },
8673 BracketPair {
8674 start: "(".to_string(),
8675 end: ")".to_string(),
8676 close: true,
8677 surround: true,
8678 newline: true,
8679 },
8680 BracketPair {
8681 start: "[".to_string(),
8682 end: "]".to_string(),
8683 close: false,
8684 surround: true,
8685 newline: true,
8686 },
8687 ],
8688 ..Default::default()
8689 },
8690 autoclose_before: "})]".to_string(),
8691 ..Default::default()
8692 },
8693 Some(tree_sitter_rust::LANGUAGE.into()),
8694 ));
8695
8696 cx.language_registry().add(language.clone());
8697 cx.update_buffer(|buffer, cx| {
8698 buffer.set_language(Some(language), cx);
8699 });
8700
8701 cx.set_state(
8702 &"
8703 {(ˇ)}
8704 [[ˇ]]
8705 {(ˇ)}
8706 "
8707 .unindent(),
8708 );
8709
8710 cx.update_editor(|editor, window, cx| {
8711 editor.backspace(&Default::default(), window, cx);
8712 editor.backspace(&Default::default(), window, cx);
8713 });
8714
8715 cx.assert_editor_state(
8716 &"
8717 ˇ
8718 ˇ]]
8719 ˇ
8720 "
8721 .unindent(),
8722 );
8723
8724 cx.update_editor(|editor, window, cx| {
8725 editor.handle_input("{", window, cx);
8726 editor.handle_input("{", window, cx);
8727 editor.move_right(&MoveRight, window, cx);
8728 editor.move_right(&MoveRight, window, cx);
8729 editor.move_left(&MoveLeft, window, cx);
8730 editor.move_left(&MoveLeft, window, cx);
8731 editor.backspace(&Default::default(), window, cx);
8732 });
8733
8734 cx.assert_editor_state(
8735 &"
8736 {ˇ}
8737 {ˇ}]]
8738 {ˇ}
8739 "
8740 .unindent(),
8741 );
8742
8743 cx.update_editor(|editor, window, cx| {
8744 editor.backspace(&Default::default(), window, cx);
8745 });
8746
8747 cx.assert_editor_state(
8748 &"
8749 ˇ
8750 ˇ]]
8751 ˇ
8752 "
8753 .unindent(),
8754 );
8755}
8756
8757#[gpui::test]
8758async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8759 init_test(cx, |_| {});
8760
8761 let language = Arc::new(Language::new(
8762 LanguageConfig::default(),
8763 Some(tree_sitter_rust::LANGUAGE.into()),
8764 ));
8765
8766 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8767 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8768 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8769 editor
8770 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8771 .await;
8772
8773 editor.update_in(cx, |editor, window, cx| {
8774 editor.set_auto_replace_emoji_shortcode(true);
8775
8776 editor.handle_input("Hello ", window, cx);
8777 editor.handle_input(":wave", window, cx);
8778 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8779
8780 editor.handle_input(":", window, cx);
8781 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8782
8783 editor.handle_input(" :smile", window, cx);
8784 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8785
8786 editor.handle_input(":", window, cx);
8787 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8788
8789 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8790 editor.handle_input(":wave", window, cx);
8791 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8792
8793 editor.handle_input(":", window, cx);
8794 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8795
8796 editor.handle_input(":1", window, cx);
8797 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8798
8799 editor.handle_input(":", window, cx);
8800 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8801
8802 // Ensure shortcode does not get replaced when it is part of a word
8803 editor.handle_input(" Test:wave", window, cx);
8804 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8805
8806 editor.handle_input(":", window, cx);
8807 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8808
8809 editor.set_auto_replace_emoji_shortcode(false);
8810
8811 // Ensure shortcode does not get replaced when auto replace is off
8812 editor.handle_input(" :wave", window, cx);
8813 assert_eq!(
8814 editor.text(cx),
8815 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8816 );
8817
8818 editor.handle_input(":", window, cx);
8819 assert_eq!(
8820 editor.text(cx),
8821 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8822 );
8823 });
8824}
8825
8826#[gpui::test]
8827async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8828 init_test(cx, |_| {});
8829
8830 let (text, insertion_ranges) = marked_text_ranges(
8831 indoc! {"
8832 ˇ
8833 "},
8834 false,
8835 );
8836
8837 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8838 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8839
8840 _ = editor.update_in(cx, |editor, window, cx| {
8841 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8842
8843 editor
8844 .insert_snippet(&insertion_ranges, snippet, window, cx)
8845 .unwrap();
8846
8847 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8848 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8849 assert_eq!(editor.text(cx), expected_text);
8850 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8851 }
8852
8853 assert(
8854 editor,
8855 cx,
8856 indoc! {"
8857 type «» =•
8858 "},
8859 );
8860
8861 assert!(editor.context_menu_visible(), "There should be a matches");
8862 });
8863}
8864
8865#[gpui::test]
8866async fn test_snippets(cx: &mut TestAppContext) {
8867 init_test(cx, |_| {});
8868
8869 let mut cx = EditorTestContext::new(cx).await;
8870
8871 cx.set_state(indoc! {"
8872 a.ˇ b
8873 a.ˇ b
8874 a.ˇ b
8875 "});
8876
8877 cx.update_editor(|editor, window, cx| {
8878 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8879 let insertion_ranges = editor
8880 .selections
8881 .all(cx)
8882 .iter()
8883 .map(|s| s.range().clone())
8884 .collect::<Vec<_>>();
8885 editor
8886 .insert_snippet(&insertion_ranges, snippet, window, cx)
8887 .unwrap();
8888 });
8889
8890 cx.assert_editor_state(indoc! {"
8891 a.f(«oneˇ», two, «threeˇ») b
8892 a.f(«oneˇ», two, «threeˇ») b
8893 a.f(«oneˇ», two, «threeˇ») b
8894 "});
8895
8896 // Can't move earlier than the first tab stop
8897 cx.update_editor(|editor, window, cx| {
8898 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8899 });
8900 cx.assert_editor_state(indoc! {"
8901 a.f(«oneˇ», two, «threeˇ») b
8902 a.f(«oneˇ», two, «threeˇ») b
8903 a.f(«oneˇ», two, «threeˇ») b
8904 "});
8905
8906 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8907 cx.assert_editor_state(indoc! {"
8908 a.f(one, «twoˇ», three) b
8909 a.f(one, «twoˇ», three) b
8910 a.f(one, «twoˇ», three) b
8911 "});
8912
8913 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8914 cx.assert_editor_state(indoc! {"
8915 a.f(«oneˇ», two, «threeˇ») b
8916 a.f(«oneˇ», two, «threeˇ») b
8917 a.f(«oneˇ», two, «threeˇ») b
8918 "});
8919
8920 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8921 cx.assert_editor_state(indoc! {"
8922 a.f(one, «twoˇ», three) b
8923 a.f(one, «twoˇ», three) b
8924 a.f(one, «twoˇ», three) b
8925 "});
8926 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8927 cx.assert_editor_state(indoc! {"
8928 a.f(one, two, three)ˇ b
8929 a.f(one, two, three)ˇ b
8930 a.f(one, two, three)ˇ b
8931 "});
8932
8933 // As soon as the last tab stop is reached, snippet state is gone
8934 cx.update_editor(|editor, window, cx| {
8935 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8936 });
8937 cx.assert_editor_state(indoc! {"
8938 a.f(one, two, three)ˇ b
8939 a.f(one, two, three)ˇ b
8940 a.f(one, two, three)ˇ b
8941 "});
8942}
8943
8944#[gpui::test]
8945async fn test_snippet_indentation(cx: &mut TestAppContext) {
8946 init_test(cx, |_| {});
8947
8948 let mut cx = EditorTestContext::new(cx).await;
8949
8950 cx.update_editor(|editor, window, cx| {
8951 let snippet = Snippet::parse(indoc! {"
8952 /*
8953 * Multiline comment with leading indentation
8954 *
8955 * $1
8956 */
8957 $0"})
8958 .unwrap();
8959 let insertion_ranges = editor
8960 .selections
8961 .all(cx)
8962 .iter()
8963 .map(|s| s.range().clone())
8964 .collect::<Vec<_>>();
8965 editor
8966 .insert_snippet(&insertion_ranges, snippet, window, cx)
8967 .unwrap();
8968 });
8969
8970 cx.assert_editor_state(indoc! {"
8971 /*
8972 * Multiline comment with leading indentation
8973 *
8974 * ˇ
8975 */
8976 "});
8977
8978 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8979 cx.assert_editor_state(indoc! {"
8980 /*
8981 * Multiline comment with leading indentation
8982 *
8983 *•
8984 */
8985 ˇ"});
8986}
8987
8988#[gpui::test]
8989async fn test_document_format_during_save(cx: &mut TestAppContext) {
8990 init_test(cx, |_| {});
8991
8992 let fs = FakeFs::new(cx.executor());
8993 fs.insert_file(path!("/file.rs"), Default::default()).await;
8994
8995 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8996
8997 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8998 language_registry.add(rust_lang());
8999 let mut fake_servers = language_registry.register_fake_lsp(
9000 "Rust",
9001 FakeLspAdapter {
9002 capabilities: lsp::ServerCapabilities {
9003 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9004 ..Default::default()
9005 },
9006 ..Default::default()
9007 },
9008 );
9009
9010 let buffer = project
9011 .update(cx, |project, cx| {
9012 project.open_local_buffer(path!("/file.rs"), cx)
9013 })
9014 .await
9015 .unwrap();
9016
9017 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9018 let (editor, cx) = cx.add_window_view(|window, cx| {
9019 build_editor_with_project(project.clone(), buffer, window, cx)
9020 });
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.set_text("one\ntwo\nthree\n", window, cx)
9023 });
9024 assert!(cx.read(|cx| editor.is_dirty(cx)));
9025
9026 cx.executor().start_waiting();
9027 let fake_server = fake_servers.next().await.unwrap();
9028
9029 {
9030 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9031 move |params, _| async move {
9032 assert_eq!(
9033 params.text_document.uri,
9034 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9035 );
9036 assert_eq!(params.options.tab_size, 4);
9037 Ok(Some(vec![lsp::TextEdit::new(
9038 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9039 ", ".to_string(),
9040 )]))
9041 },
9042 );
9043 let save = editor
9044 .update_in(cx, |editor, window, cx| {
9045 editor.save(
9046 SaveOptions {
9047 format: true,
9048 autosave: false,
9049 },
9050 project.clone(),
9051 window,
9052 cx,
9053 )
9054 })
9055 .unwrap();
9056 cx.executor().start_waiting();
9057 save.await;
9058
9059 assert_eq!(
9060 editor.update(cx, |editor, cx| editor.text(cx)),
9061 "one, two\nthree\n"
9062 );
9063 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9064 }
9065
9066 {
9067 editor.update_in(cx, |editor, window, cx| {
9068 editor.set_text("one\ntwo\nthree\n", window, cx)
9069 });
9070 assert!(cx.read(|cx| editor.is_dirty(cx)));
9071
9072 // Ensure we can still save even if formatting hangs.
9073 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9074 move |params, _| async move {
9075 assert_eq!(
9076 params.text_document.uri,
9077 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9078 );
9079 futures::future::pending::<()>().await;
9080 unreachable!()
9081 },
9082 );
9083 let save = editor
9084 .update_in(cx, |editor, window, cx| {
9085 editor.save(
9086 SaveOptions {
9087 format: true,
9088 autosave: false,
9089 },
9090 project.clone(),
9091 window,
9092 cx,
9093 )
9094 })
9095 .unwrap();
9096 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9097 cx.executor().start_waiting();
9098 save.await;
9099 assert_eq!(
9100 editor.update(cx, |editor, cx| editor.text(cx)),
9101 "one\ntwo\nthree\n"
9102 );
9103 }
9104
9105 // Set rust language override and assert overridden tabsize is sent to language server
9106 update_test_language_settings(cx, |settings| {
9107 settings.languages.insert(
9108 "Rust".into(),
9109 LanguageSettingsContent {
9110 tab_size: NonZeroU32::new(8),
9111 ..Default::default()
9112 },
9113 );
9114 });
9115
9116 {
9117 editor.update_in(cx, |editor, window, cx| {
9118 editor.set_text("somehting_new\n", window, cx)
9119 });
9120 assert!(cx.read(|cx| editor.is_dirty(cx)));
9121 let _formatting_request_signal = fake_server
9122 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9123 assert_eq!(
9124 params.text_document.uri,
9125 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9126 );
9127 assert_eq!(params.options.tab_size, 8);
9128 Ok(Some(vec![]))
9129 });
9130 let save = editor
9131 .update_in(cx, |editor, window, cx| {
9132 editor.save(
9133 SaveOptions {
9134 format: true,
9135 autosave: false,
9136 },
9137 project.clone(),
9138 window,
9139 cx,
9140 )
9141 })
9142 .unwrap();
9143 cx.executor().start_waiting();
9144 save.await;
9145 }
9146}
9147
9148#[gpui::test]
9149async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9150 init_test(cx, |_| {});
9151
9152 let cols = 4;
9153 let rows = 10;
9154 let sample_text_1 = sample_text(rows, cols, 'a');
9155 assert_eq!(
9156 sample_text_1,
9157 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9158 );
9159 let sample_text_2 = sample_text(rows, cols, 'l');
9160 assert_eq!(
9161 sample_text_2,
9162 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9163 );
9164 let sample_text_3 = sample_text(rows, cols, 'v');
9165 assert_eq!(
9166 sample_text_3,
9167 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9168 );
9169
9170 let fs = FakeFs::new(cx.executor());
9171 fs.insert_tree(
9172 path!("/a"),
9173 json!({
9174 "main.rs": sample_text_1,
9175 "other.rs": sample_text_2,
9176 "lib.rs": sample_text_3,
9177 }),
9178 )
9179 .await;
9180
9181 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9182 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9183 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9184
9185 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9186 language_registry.add(rust_lang());
9187 let mut fake_servers = language_registry.register_fake_lsp(
9188 "Rust",
9189 FakeLspAdapter {
9190 capabilities: lsp::ServerCapabilities {
9191 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9192 ..Default::default()
9193 },
9194 ..Default::default()
9195 },
9196 );
9197
9198 let worktree = project.update(cx, |project, cx| {
9199 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9200 assert_eq!(worktrees.len(), 1);
9201 worktrees.pop().unwrap()
9202 });
9203 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9204
9205 let buffer_1 = project
9206 .update(cx, |project, cx| {
9207 project.open_buffer((worktree_id, "main.rs"), cx)
9208 })
9209 .await
9210 .unwrap();
9211 let buffer_2 = project
9212 .update(cx, |project, cx| {
9213 project.open_buffer((worktree_id, "other.rs"), cx)
9214 })
9215 .await
9216 .unwrap();
9217 let buffer_3 = project
9218 .update(cx, |project, cx| {
9219 project.open_buffer((worktree_id, "lib.rs"), cx)
9220 })
9221 .await
9222 .unwrap();
9223
9224 let multi_buffer = cx.new(|cx| {
9225 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9226 multi_buffer.push_excerpts(
9227 buffer_1.clone(),
9228 [
9229 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9230 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9231 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9232 ],
9233 cx,
9234 );
9235 multi_buffer.push_excerpts(
9236 buffer_2.clone(),
9237 [
9238 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9239 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9240 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9241 ],
9242 cx,
9243 );
9244 multi_buffer.push_excerpts(
9245 buffer_3.clone(),
9246 [
9247 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9248 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9249 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9250 ],
9251 cx,
9252 );
9253 multi_buffer
9254 });
9255 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9256 Editor::new(
9257 EditorMode::full(),
9258 multi_buffer,
9259 Some(project.clone()),
9260 window,
9261 cx,
9262 )
9263 });
9264
9265 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9266 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9267 s.select_ranges(Some(1..2))
9268 });
9269 editor.insert("|one|two|three|", window, cx);
9270 });
9271 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9272 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9273 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9274 s.select_ranges(Some(60..70))
9275 });
9276 editor.insert("|four|five|six|", window, cx);
9277 });
9278 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9279
9280 // First two buffers should be edited, but not the third one.
9281 assert_eq!(
9282 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9283 "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}",
9284 );
9285 buffer_1.update(cx, |buffer, _| {
9286 assert!(buffer.is_dirty());
9287 assert_eq!(
9288 buffer.text(),
9289 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9290 )
9291 });
9292 buffer_2.update(cx, |buffer, _| {
9293 assert!(buffer.is_dirty());
9294 assert_eq!(
9295 buffer.text(),
9296 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9297 )
9298 });
9299 buffer_3.update(cx, |buffer, _| {
9300 assert!(!buffer.is_dirty());
9301 assert_eq!(buffer.text(), sample_text_3,)
9302 });
9303 cx.executor().run_until_parked();
9304
9305 cx.executor().start_waiting();
9306 let save = multi_buffer_editor
9307 .update_in(cx, |editor, window, cx| {
9308 editor.save(
9309 SaveOptions {
9310 format: true,
9311 autosave: false,
9312 },
9313 project.clone(),
9314 window,
9315 cx,
9316 )
9317 })
9318 .unwrap();
9319
9320 let fake_server = fake_servers.next().await.unwrap();
9321 fake_server
9322 .server
9323 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9324 Ok(Some(vec![lsp::TextEdit::new(
9325 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9326 format!("[{} formatted]", params.text_document.uri),
9327 )]))
9328 })
9329 .detach();
9330 save.await;
9331
9332 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9333 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9334 assert_eq!(
9335 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9336 uri!(
9337 "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}"
9338 ),
9339 );
9340 buffer_1.update(cx, |buffer, _| {
9341 assert!(!buffer.is_dirty());
9342 assert_eq!(
9343 buffer.text(),
9344 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9345 )
9346 });
9347 buffer_2.update(cx, |buffer, _| {
9348 assert!(!buffer.is_dirty());
9349 assert_eq!(
9350 buffer.text(),
9351 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9352 )
9353 });
9354 buffer_3.update(cx, |buffer, _| {
9355 assert!(!buffer.is_dirty());
9356 assert_eq!(buffer.text(), sample_text_3,)
9357 });
9358}
9359
9360#[gpui::test]
9361async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9362 init_test(cx, |_| {});
9363
9364 let fs = FakeFs::new(cx.executor());
9365 fs.insert_tree(
9366 path!("/dir"),
9367 json!({
9368 "file1.rs": "fn main() { println!(\"hello\"); }",
9369 "file2.rs": "fn test() { println!(\"test\"); }",
9370 "file3.rs": "fn other() { println!(\"other\"); }\n",
9371 }),
9372 )
9373 .await;
9374
9375 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9376 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9377 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9378
9379 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9380 language_registry.add(rust_lang());
9381
9382 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9383 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9384
9385 // Open three buffers
9386 let buffer_1 = project
9387 .update(cx, |project, cx| {
9388 project.open_buffer((worktree_id, "file1.rs"), cx)
9389 })
9390 .await
9391 .unwrap();
9392 let buffer_2 = project
9393 .update(cx, |project, cx| {
9394 project.open_buffer((worktree_id, "file2.rs"), cx)
9395 })
9396 .await
9397 .unwrap();
9398 let buffer_3 = project
9399 .update(cx, |project, cx| {
9400 project.open_buffer((worktree_id, "file3.rs"), cx)
9401 })
9402 .await
9403 .unwrap();
9404
9405 // Create a multi-buffer with all three buffers
9406 let multi_buffer = cx.new(|cx| {
9407 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9408 multi_buffer.push_excerpts(
9409 buffer_1.clone(),
9410 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9411 cx,
9412 );
9413 multi_buffer.push_excerpts(
9414 buffer_2.clone(),
9415 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9416 cx,
9417 );
9418 multi_buffer.push_excerpts(
9419 buffer_3.clone(),
9420 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9421 cx,
9422 );
9423 multi_buffer
9424 });
9425
9426 let editor = cx.new_window_entity(|window, cx| {
9427 Editor::new(
9428 EditorMode::full(),
9429 multi_buffer,
9430 Some(project.clone()),
9431 window,
9432 cx,
9433 )
9434 });
9435
9436 // Edit only the first buffer
9437 editor.update_in(cx, |editor, window, cx| {
9438 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9439 s.select_ranges(Some(10..10))
9440 });
9441 editor.insert("// edited", window, cx);
9442 });
9443
9444 // Verify that only buffer 1 is dirty
9445 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9446 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9447 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9448
9449 // Get write counts after file creation (files were created with initial content)
9450 // We expect each file to have been written once during creation
9451 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9452 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9453 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9454
9455 // Perform autosave
9456 let save_task = editor.update_in(cx, |editor, window, cx| {
9457 editor.save(
9458 SaveOptions {
9459 format: true,
9460 autosave: true,
9461 },
9462 project.clone(),
9463 window,
9464 cx,
9465 )
9466 });
9467 save_task.await.unwrap();
9468
9469 // Only the dirty buffer should have been saved
9470 assert_eq!(
9471 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9472 1,
9473 "Buffer 1 was dirty, so it should have been written once during autosave"
9474 );
9475 assert_eq!(
9476 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9477 0,
9478 "Buffer 2 was clean, so it should not have been written during autosave"
9479 );
9480 assert_eq!(
9481 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9482 0,
9483 "Buffer 3 was clean, so it should not have been written during autosave"
9484 );
9485
9486 // Verify buffer states after autosave
9487 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9488 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9489 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9490
9491 // Now perform a manual save (format = true)
9492 let save_task = editor.update_in(cx, |editor, window, cx| {
9493 editor.save(
9494 SaveOptions {
9495 format: true,
9496 autosave: false,
9497 },
9498 project.clone(),
9499 window,
9500 cx,
9501 )
9502 });
9503 save_task.await.unwrap();
9504
9505 // During manual save, clean buffers don't get written to disk
9506 // They just get did_save called for language server notifications
9507 assert_eq!(
9508 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9509 1,
9510 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9511 );
9512 assert_eq!(
9513 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9514 0,
9515 "Buffer 2 should not have been written at all"
9516 );
9517 assert_eq!(
9518 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9519 0,
9520 "Buffer 3 should not have been written at all"
9521 );
9522}
9523
9524#[gpui::test]
9525async fn test_range_format_during_save(cx: &mut TestAppContext) {
9526 init_test(cx, |_| {});
9527
9528 let fs = FakeFs::new(cx.executor());
9529 fs.insert_file(path!("/file.rs"), Default::default()).await;
9530
9531 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9532
9533 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9534 language_registry.add(rust_lang());
9535 let mut fake_servers = language_registry.register_fake_lsp(
9536 "Rust",
9537 FakeLspAdapter {
9538 capabilities: lsp::ServerCapabilities {
9539 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9540 ..Default::default()
9541 },
9542 ..Default::default()
9543 },
9544 );
9545
9546 let buffer = project
9547 .update(cx, |project, cx| {
9548 project.open_local_buffer(path!("/file.rs"), cx)
9549 })
9550 .await
9551 .unwrap();
9552
9553 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9554 let (editor, cx) = cx.add_window_view(|window, cx| {
9555 build_editor_with_project(project.clone(), buffer, window, cx)
9556 });
9557 editor.update_in(cx, |editor, window, cx| {
9558 editor.set_text("one\ntwo\nthree\n", window, cx)
9559 });
9560 assert!(cx.read(|cx| editor.is_dirty(cx)));
9561
9562 cx.executor().start_waiting();
9563 let fake_server = fake_servers.next().await.unwrap();
9564
9565 let save = editor
9566 .update_in(cx, |editor, window, cx| {
9567 editor.save(
9568 SaveOptions {
9569 format: true,
9570 autosave: false,
9571 },
9572 project.clone(),
9573 window,
9574 cx,
9575 )
9576 })
9577 .unwrap();
9578 fake_server
9579 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9580 assert_eq!(
9581 params.text_document.uri,
9582 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9583 );
9584 assert_eq!(params.options.tab_size, 4);
9585 Ok(Some(vec![lsp::TextEdit::new(
9586 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9587 ", ".to_string(),
9588 )]))
9589 })
9590 .next()
9591 .await;
9592 cx.executor().start_waiting();
9593 save.await;
9594 assert_eq!(
9595 editor.update(cx, |editor, cx| editor.text(cx)),
9596 "one, two\nthree\n"
9597 );
9598 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9599
9600 editor.update_in(cx, |editor, window, cx| {
9601 editor.set_text("one\ntwo\nthree\n", window, cx)
9602 });
9603 assert!(cx.read(|cx| editor.is_dirty(cx)));
9604
9605 // Ensure we can still save even if formatting hangs.
9606 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9607 move |params, _| async move {
9608 assert_eq!(
9609 params.text_document.uri,
9610 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9611 );
9612 futures::future::pending::<()>().await;
9613 unreachable!()
9614 },
9615 );
9616 let save = editor
9617 .update_in(cx, |editor, window, cx| {
9618 editor.save(
9619 SaveOptions {
9620 format: true,
9621 autosave: false,
9622 },
9623 project.clone(),
9624 window,
9625 cx,
9626 )
9627 })
9628 .unwrap();
9629 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9630 cx.executor().start_waiting();
9631 save.await;
9632 assert_eq!(
9633 editor.update(cx, |editor, cx| editor.text(cx)),
9634 "one\ntwo\nthree\n"
9635 );
9636 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9637
9638 // For non-dirty buffer, no formatting request should be sent
9639 let save = editor
9640 .update_in(cx, |editor, window, cx| {
9641 editor.save(
9642 SaveOptions {
9643 format: false,
9644 autosave: false,
9645 },
9646 project.clone(),
9647 window,
9648 cx,
9649 )
9650 })
9651 .unwrap();
9652 let _pending_format_request = fake_server
9653 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9654 panic!("Should not be invoked");
9655 })
9656 .next();
9657 cx.executor().start_waiting();
9658 save.await;
9659
9660 // Set Rust language override and assert overridden tabsize is sent to language server
9661 update_test_language_settings(cx, |settings| {
9662 settings.languages.insert(
9663 "Rust".into(),
9664 LanguageSettingsContent {
9665 tab_size: NonZeroU32::new(8),
9666 ..Default::default()
9667 },
9668 );
9669 });
9670
9671 editor.update_in(cx, |editor, window, cx| {
9672 editor.set_text("somehting_new\n", window, cx)
9673 });
9674 assert!(cx.read(|cx| editor.is_dirty(cx)));
9675 let save = editor
9676 .update_in(cx, |editor, window, cx| {
9677 editor.save(
9678 SaveOptions {
9679 format: true,
9680 autosave: false,
9681 },
9682 project.clone(),
9683 window,
9684 cx,
9685 )
9686 })
9687 .unwrap();
9688 fake_server
9689 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9690 assert_eq!(
9691 params.text_document.uri,
9692 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9693 );
9694 assert_eq!(params.options.tab_size, 8);
9695 Ok(Some(Vec::new()))
9696 })
9697 .next()
9698 .await;
9699 save.await;
9700}
9701
9702#[gpui::test]
9703async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9704 init_test(cx, |settings| {
9705 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9706 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9707 ))
9708 });
9709
9710 let fs = FakeFs::new(cx.executor());
9711 fs.insert_file(path!("/file.rs"), Default::default()).await;
9712
9713 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9714
9715 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9716 language_registry.add(Arc::new(Language::new(
9717 LanguageConfig {
9718 name: "Rust".into(),
9719 matcher: LanguageMatcher {
9720 path_suffixes: vec!["rs".to_string()],
9721 ..Default::default()
9722 },
9723 ..LanguageConfig::default()
9724 },
9725 Some(tree_sitter_rust::LANGUAGE.into()),
9726 )));
9727 update_test_language_settings(cx, |settings| {
9728 // Enable Prettier formatting for the same buffer, and ensure
9729 // LSP is called instead of Prettier.
9730 settings.defaults.prettier = Some(PrettierSettings {
9731 allowed: true,
9732 ..PrettierSettings::default()
9733 });
9734 });
9735 let mut fake_servers = language_registry.register_fake_lsp(
9736 "Rust",
9737 FakeLspAdapter {
9738 capabilities: lsp::ServerCapabilities {
9739 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9740 ..Default::default()
9741 },
9742 ..Default::default()
9743 },
9744 );
9745
9746 let buffer = project
9747 .update(cx, |project, cx| {
9748 project.open_local_buffer(path!("/file.rs"), cx)
9749 })
9750 .await
9751 .unwrap();
9752
9753 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9754 let (editor, cx) = cx.add_window_view(|window, cx| {
9755 build_editor_with_project(project.clone(), buffer, window, cx)
9756 });
9757 editor.update_in(cx, |editor, window, cx| {
9758 editor.set_text("one\ntwo\nthree\n", window, cx)
9759 });
9760
9761 cx.executor().start_waiting();
9762 let fake_server = fake_servers.next().await.unwrap();
9763
9764 let format = editor
9765 .update_in(cx, |editor, window, cx| {
9766 editor.perform_format(
9767 project.clone(),
9768 FormatTrigger::Manual,
9769 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9770 window,
9771 cx,
9772 )
9773 })
9774 .unwrap();
9775 fake_server
9776 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9777 assert_eq!(
9778 params.text_document.uri,
9779 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9780 );
9781 assert_eq!(params.options.tab_size, 4);
9782 Ok(Some(vec![lsp::TextEdit::new(
9783 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9784 ", ".to_string(),
9785 )]))
9786 })
9787 .next()
9788 .await;
9789 cx.executor().start_waiting();
9790 format.await;
9791 assert_eq!(
9792 editor.update(cx, |editor, cx| editor.text(cx)),
9793 "one, two\nthree\n"
9794 );
9795
9796 editor.update_in(cx, |editor, window, cx| {
9797 editor.set_text("one\ntwo\nthree\n", window, cx)
9798 });
9799 // Ensure we don't lock if formatting hangs.
9800 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9801 move |params, _| async move {
9802 assert_eq!(
9803 params.text_document.uri,
9804 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9805 );
9806 futures::future::pending::<()>().await;
9807 unreachable!()
9808 },
9809 );
9810 let format = editor
9811 .update_in(cx, |editor, window, cx| {
9812 editor.perform_format(
9813 project,
9814 FormatTrigger::Manual,
9815 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9816 window,
9817 cx,
9818 )
9819 })
9820 .unwrap();
9821 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9822 cx.executor().start_waiting();
9823 format.await;
9824 assert_eq!(
9825 editor.update(cx, |editor, cx| editor.text(cx)),
9826 "one\ntwo\nthree\n"
9827 );
9828}
9829
9830#[gpui::test]
9831async fn test_multiple_formatters(cx: &mut TestAppContext) {
9832 init_test(cx, |settings| {
9833 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9834 settings.defaults.formatter =
9835 Some(language_settings::SelectedFormatter::List(FormatterList(
9836 vec![
9837 Formatter::LanguageServer { name: None },
9838 Formatter::CodeActions(
9839 [
9840 ("code-action-1".into(), true),
9841 ("code-action-2".into(), true),
9842 ]
9843 .into_iter()
9844 .collect(),
9845 ),
9846 ]
9847 .into(),
9848 )))
9849 });
9850
9851 let fs = FakeFs::new(cx.executor());
9852 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9853 .await;
9854
9855 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9856 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9857 language_registry.add(rust_lang());
9858
9859 let mut fake_servers = language_registry.register_fake_lsp(
9860 "Rust",
9861 FakeLspAdapter {
9862 capabilities: lsp::ServerCapabilities {
9863 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9864 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9865 commands: vec!["the-command-for-code-action-1".into()],
9866 ..Default::default()
9867 }),
9868 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9869 ..Default::default()
9870 },
9871 ..Default::default()
9872 },
9873 );
9874
9875 let buffer = project
9876 .update(cx, |project, cx| {
9877 project.open_local_buffer(path!("/file.rs"), cx)
9878 })
9879 .await
9880 .unwrap();
9881
9882 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9883 let (editor, cx) = cx.add_window_view(|window, cx| {
9884 build_editor_with_project(project.clone(), buffer, window, cx)
9885 });
9886
9887 cx.executor().start_waiting();
9888
9889 let fake_server = fake_servers.next().await.unwrap();
9890 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9891 move |_params, _| async move {
9892 Ok(Some(vec![lsp::TextEdit::new(
9893 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9894 "applied-formatting\n".to_string(),
9895 )]))
9896 },
9897 );
9898 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9899 move |params, _| async move {
9900 assert_eq!(
9901 params.context.only,
9902 Some(vec!["code-action-1".into(), "code-action-2".into()])
9903 );
9904 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9905 Ok(Some(vec![
9906 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9907 kind: Some("code-action-1".into()),
9908 edit: Some(lsp::WorkspaceEdit::new(
9909 [(
9910 uri.clone(),
9911 vec![lsp::TextEdit::new(
9912 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9913 "applied-code-action-1-edit\n".to_string(),
9914 )],
9915 )]
9916 .into_iter()
9917 .collect(),
9918 )),
9919 command: Some(lsp::Command {
9920 command: "the-command-for-code-action-1".into(),
9921 ..Default::default()
9922 }),
9923 ..Default::default()
9924 }),
9925 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9926 kind: Some("code-action-2".into()),
9927 edit: Some(lsp::WorkspaceEdit::new(
9928 [(
9929 uri.clone(),
9930 vec![lsp::TextEdit::new(
9931 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9932 "applied-code-action-2-edit\n".to_string(),
9933 )],
9934 )]
9935 .into_iter()
9936 .collect(),
9937 )),
9938 ..Default::default()
9939 }),
9940 ]))
9941 },
9942 );
9943
9944 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9945 move |params, _| async move { Ok(params) }
9946 });
9947
9948 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9949 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9950 let fake = fake_server.clone();
9951 let lock = command_lock.clone();
9952 move |params, _| {
9953 assert_eq!(params.command, "the-command-for-code-action-1");
9954 let fake = fake.clone();
9955 let lock = lock.clone();
9956 async move {
9957 lock.lock().await;
9958 fake.server
9959 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9960 label: None,
9961 edit: lsp::WorkspaceEdit {
9962 changes: Some(
9963 [(
9964 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9965 vec![lsp::TextEdit {
9966 range: lsp::Range::new(
9967 lsp::Position::new(0, 0),
9968 lsp::Position::new(0, 0),
9969 ),
9970 new_text: "applied-code-action-1-command\n".into(),
9971 }],
9972 )]
9973 .into_iter()
9974 .collect(),
9975 ),
9976 ..Default::default()
9977 },
9978 })
9979 .await
9980 .into_response()
9981 .unwrap();
9982 Ok(Some(json!(null)))
9983 }
9984 }
9985 });
9986
9987 cx.executor().start_waiting();
9988 editor
9989 .update_in(cx, |editor, window, cx| {
9990 editor.perform_format(
9991 project.clone(),
9992 FormatTrigger::Manual,
9993 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9994 window,
9995 cx,
9996 )
9997 })
9998 .unwrap()
9999 .await;
10000 editor.update(cx, |editor, cx| {
10001 assert_eq!(
10002 editor.text(cx),
10003 r#"
10004 applied-code-action-2-edit
10005 applied-code-action-1-command
10006 applied-code-action-1-edit
10007 applied-formatting
10008 one
10009 two
10010 three
10011 "#
10012 .unindent()
10013 );
10014 });
10015
10016 editor.update_in(cx, |editor, window, cx| {
10017 editor.undo(&Default::default(), window, cx);
10018 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10019 });
10020
10021 // Perform a manual edit while waiting for an LSP command
10022 // that's being run as part of a formatting code action.
10023 let lock_guard = command_lock.lock().await;
10024 let format = editor
10025 .update_in(cx, |editor, window, cx| {
10026 editor.perform_format(
10027 project.clone(),
10028 FormatTrigger::Manual,
10029 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10030 window,
10031 cx,
10032 )
10033 })
10034 .unwrap();
10035 cx.run_until_parked();
10036 editor.update(cx, |editor, cx| {
10037 assert_eq!(
10038 editor.text(cx),
10039 r#"
10040 applied-code-action-1-edit
10041 applied-formatting
10042 one
10043 two
10044 three
10045 "#
10046 .unindent()
10047 );
10048
10049 editor.buffer.update(cx, |buffer, cx| {
10050 let ix = buffer.len(cx);
10051 buffer.edit([(ix..ix, "edited\n")], None, cx);
10052 });
10053 });
10054
10055 // Allow the LSP command to proceed. Because the buffer was edited,
10056 // the second code action will not be run.
10057 drop(lock_guard);
10058 format.await;
10059 editor.update_in(cx, |editor, window, cx| {
10060 assert_eq!(
10061 editor.text(cx),
10062 r#"
10063 applied-code-action-1-command
10064 applied-code-action-1-edit
10065 applied-formatting
10066 one
10067 two
10068 three
10069 edited
10070 "#
10071 .unindent()
10072 );
10073
10074 // The manual edit is undone first, because it is the last thing the user did
10075 // (even though the command completed afterwards).
10076 editor.undo(&Default::default(), window, cx);
10077 assert_eq!(
10078 editor.text(cx),
10079 r#"
10080 applied-code-action-1-command
10081 applied-code-action-1-edit
10082 applied-formatting
10083 one
10084 two
10085 three
10086 "#
10087 .unindent()
10088 );
10089
10090 // All the formatting (including the command, which completed after the manual edit)
10091 // is undone together.
10092 editor.undo(&Default::default(), window, cx);
10093 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10094 });
10095}
10096
10097#[gpui::test]
10098async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10099 init_test(cx, |settings| {
10100 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
10101 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
10102 ))
10103 });
10104
10105 let fs = FakeFs::new(cx.executor());
10106 fs.insert_file(path!("/file.ts"), Default::default()).await;
10107
10108 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10109
10110 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10111 language_registry.add(Arc::new(Language::new(
10112 LanguageConfig {
10113 name: "TypeScript".into(),
10114 matcher: LanguageMatcher {
10115 path_suffixes: vec!["ts".to_string()],
10116 ..Default::default()
10117 },
10118 ..LanguageConfig::default()
10119 },
10120 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10121 )));
10122 update_test_language_settings(cx, |settings| {
10123 settings.defaults.prettier = Some(PrettierSettings {
10124 allowed: true,
10125 ..PrettierSettings::default()
10126 });
10127 });
10128 let mut fake_servers = language_registry.register_fake_lsp(
10129 "TypeScript",
10130 FakeLspAdapter {
10131 capabilities: lsp::ServerCapabilities {
10132 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10133 ..Default::default()
10134 },
10135 ..Default::default()
10136 },
10137 );
10138
10139 let buffer = project
10140 .update(cx, |project, cx| {
10141 project.open_local_buffer(path!("/file.ts"), cx)
10142 })
10143 .await
10144 .unwrap();
10145
10146 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10147 let (editor, cx) = cx.add_window_view(|window, cx| {
10148 build_editor_with_project(project.clone(), buffer, window, cx)
10149 });
10150 editor.update_in(cx, |editor, window, cx| {
10151 editor.set_text(
10152 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10153 window,
10154 cx,
10155 )
10156 });
10157
10158 cx.executor().start_waiting();
10159 let fake_server = fake_servers.next().await.unwrap();
10160
10161 let format = editor
10162 .update_in(cx, |editor, window, cx| {
10163 editor.perform_code_action_kind(
10164 project.clone(),
10165 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10166 window,
10167 cx,
10168 )
10169 })
10170 .unwrap();
10171 fake_server
10172 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10173 assert_eq!(
10174 params.text_document.uri,
10175 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10176 );
10177 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10178 lsp::CodeAction {
10179 title: "Organize Imports".to_string(),
10180 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10181 edit: Some(lsp::WorkspaceEdit {
10182 changes: Some(
10183 [(
10184 params.text_document.uri.clone(),
10185 vec![lsp::TextEdit::new(
10186 lsp::Range::new(
10187 lsp::Position::new(1, 0),
10188 lsp::Position::new(2, 0),
10189 ),
10190 "".to_string(),
10191 )],
10192 )]
10193 .into_iter()
10194 .collect(),
10195 ),
10196 ..Default::default()
10197 }),
10198 ..Default::default()
10199 },
10200 )]))
10201 })
10202 .next()
10203 .await;
10204 cx.executor().start_waiting();
10205 format.await;
10206 assert_eq!(
10207 editor.update(cx, |editor, cx| editor.text(cx)),
10208 "import { a } from 'module';\n\nconst x = a;\n"
10209 );
10210
10211 editor.update_in(cx, |editor, window, cx| {
10212 editor.set_text(
10213 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10214 window,
10215 cx,
10216 )
10217 });
10218 // Ensure we don't lock if code action hangs.
10219 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10220 move |params, _| async move {
10221 assert_eq!(
10222 params.text_document.uri,
10223 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10224 );
10225 futures::future::pending::<()>().await;
10226 unreachable!()
10227 },
10228 );
10229 let format = editor
10230 .update_in(cx, |editor, window, cx| {
10231 editor.perform_code_action_kind(
10232 project,
10233 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10234 window,
10235 cx,
10236 )
10237 })
10238 .unwrap();
10239 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10240 cx.executor().start_waiting();
10241 format.await;
10242 assert_eq!(
10243 editor.update(cx, |editor, cx| editor.text(cx)),
10244 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10245 );
10246}
10247
10248#[gpui::test]
10249async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10250 init_test(cx, |_| {});
10251
10252 let mut cx = EditorLspTestContext::new_rust(
10253 lsp::ServerCapabilities {
10254 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10255 ..Default::default()
10256 },
10257 cx,
10258 )
10259 .await;
10260
10261 cx.set_state(indoc! {"
10262 one.twoˇ
10263 "});
10264
10265 // The format request takes a long time. When it completes, it inserts
10266 // a newline and an indent before the `.`
10267 cx.lsp
10268 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10269 let executor = cx.background_executor().clone();
10270 async move {
10271 executor.timer(Duration::from_millis(100)).await;
10272 Ok(Some(vec![lsp::TextEdit {
10273 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10274 new_text: "\n ".into(),
10275 }]))
10276 }
10277 });
10278
10279 // Submit a format request.
10280 let format_1 = cx
10281 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10282 .unwrap();
10283 cx.executor().run_until_parked();
10284
10285 // Submit a second format request.
10286 let format_2 = cx
10287 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10288 .unwrap();
10289 cx.executor().run_until_parked();
10290
10291 // Wait for both format requests to complete
10292 cx.executor().advance_clock(Duration::from_millis(200));
10293 cx.executor().start_waiting();
10294 format_1.await.unwrap();
10295 cx.executor().start_waiting();
10296 format_2.await.unwrap();
10297
10298 // The formatting edits only happens once.
10299 cx.assert_editor_state(indoc! {"
10300 one
10301 .twoˇ
10302 "});
10303}
10304
10305#[gpui::test]
10306async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10307 init_test(cx, |settings| {
10308 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10309 });
10310
10311 let mut cx = EditorLspTestContext::new_rust(
10312 lsp::ServerCapabilities {
10313 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10314 ..Default::default()
10315 },
10316 cx,
10317 )
10318 .await;
10319
10320 // Set up a buffer white some trailing whitespace and no trailing newline.
10321 cx.set_state(
10322 &[
10323 "one ", //
10324 "twoˇ", //
10325 "three ", //
10326 "four", //
10327 ]
10328 .join("\n"),
10329 );
10330
10331 // Submit a format request.
10332 let format = cx
10333 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10334 .unwrap();
10335
10336 // Record which buffer changes have been sent to the language server
10337 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10338 cx.lsp
10339 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10340 let buffer_changes = buffer_changes.clone();
10341 move |params, _| {
10342 buffer_changes.lock().extend(
10343 params
10344 .content_changes
10345 .into_iter()
10346 .map(|e| (e.range.unwrap(), e.text)),
10347 );
10348 }
10349 });
10350
10351 // Handle formatting requests to the language server.
10352 cx.lsp
10353 .set_request_handler::<lsp::request::Formatting, _, _>({
10354 let buffer_changes = buffer_changes.clone();
10355 move |_, _| {
10356 // When formatting is requested, trailing whitespace has already been stripped,
10357 // and the trailing newline has already been added.
10358 assert_eq!(
10359 &buffer_changes.lock()[1..],
10360 &[
10361 (
10362 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10363 "".into()
10364 ),
10365 (
10366 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10367 "".into()
10368 ),
10369 (
10370 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10371 "\n".into()
10372 ),
10373 ]
10374 );
10375
10376 // Insert blank lines between each line of the buffer.
10377 async move {
10378 Ok(Some(vec![
10379 lsp::TextEdit {
10380 range: lsp::Range::new(
10381 lsp::Position::new(1, 0),
10382 lsp::Position::new(1, 0),
10383 ),
10384 new_text: "\n".into(),
10385 },
10386 lsp::TextEdit {
10387 range: lsp::Range::new(
10388 lsp::Position::new(2, 0),
10389 lsp::Position::new(2, 0),
10390 ),
10391 new_text: "\n".into(),
10392 },
10393 ]))
10394 }
10395 }
10396 });
10397
10398 // After formatting the buffer, the trailing whitespace is stripped,
10399 // a newline is appended, and the edits provided by the language server
10400 // have been applied.
10401 format.await.unwrap();
10402 cx.assert_editor_state(
10403 &[
10404 "one", //
10405 "", //
10406 "twoˇ", //
10407 "", //
10408 "three", //
10409 "four", //
10410 "", //
10411 ]
10412 .join("\n"),
10413 );
10414
10415 // Undoing the formatting undoes the trailing whitespace removal, the
10416 // trailing newline, and the LSP edits.
10417 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10418 cx.assert_editor_state(
10419 &[
10420 "one ", //
10421 "twoˇ", //
10422 "three ", //
10423 "four", //
10424 ]
10425 .join("\n"),
10426 );
10427}
10428
10429#[gpui::test]
10430async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10431 cx: &mut TestAppContext,
10432) {
10433 init_test(cx, |_| {});
10434
10435 cx.update(|cx| {
10436 cx.update_global::<SettingsStore, _>(|settings, cx| {
10437 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10438 settings.auto_signature_help = Some(true);
10439 });
10440 });
10441 });
10442
10443 let mut cx = EditorLspTestContext::new_rust(
10444 lsp::ServerCapabilities {
10445 signature_help_provider: Some(lsp::SignatureHelpOptions {
10446 ..Default::default()
10447 }),
10448 ..Default::default()
10449 },
10450 cx,
10451 )
10452 .await;
10453
10454 let language = Language::new(
10455 LanguageConfig {
10456 name: "Rust".into(),
10457 brackets: BracketPairConfig {
10458 pairs: vec![
10459 BracketPair {
10460 start: "{".to_string(),
10461 end: "}".to_string(),
10462 close: true,
10463 surround: true,
10464 newline: true,
10465 },
10466 BracketPair {
10467 start: "(".to_string(),
10468 end: ")".to_string(),
10469 close: true,
10470 surround: true,
10471 newline: true,
10472 },
10473 BracketPair {
10474 start: "/*".to_string(),
10475 end: " */".to_string(),
10476 close: true,
10477 surround: true,
10478 newline: true,
10479 },
10480 BracketPair {
10481 start: "[".to_string(),
10482 end: "]".to_string(),
10483 close: false,
10484 surround: false,
10485 newline: true,
10486 },
10487 BracketPair {
10488 start: "\"".to_string(),
10489 end: "\"".to_string(),
10490 close: true,
10491 surround: true,
10492 newline: false,
10493 },
10494 BracketPair {
10495 start: "<".to_string(),
10496 end: ">".to_string(),
10497 close: false,
10498 surround: true,
10499 newline: true,
10500 },
10501 ],
10502 ..Default::default()
10503 },
10504 autoclose_before: "})]".to_string(),
10505 ..Default::default()
10506 },
10507 Some(tree_sitter_rust::LANGUAGE.into()),
10508 );
10509 let language = Arc::new(language);
10510
10511 cx.language_registry().add(language.clone());
10512 cx.update_buffer(|buffer, cx| {
10513 buffer.set_language(Some(language), cx);
10514 });
10515
10516 cx.set_state(
10517 &r#"
10518 fn main() {
10519 sampleˇ
10520 }
10521 "#
10522 .unindent(),
10523 );
10524
10525 cx.update_editor(|editor, window, cx| {
10526 editor.handle_input("(", window, cx);
10527 });
10528 cx.assert_editor_state(
10529 &"
10530 fn main() {
10531 sample(ˇ)
10532 }
10533 "
10534 .unindent(),
10535 );
10536
10537 let mocked_response = lsp::SignatureHelp {
10538 signatures: vec![lsp::SignatureInformation {
10539 label: "fn sample(param1: u8, param2: u8)".to_string(),
10540 documentation: None,
10541 parameters: Some(vec![
10542 lsp::ParameterInformation {
10543 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10544 documentation: None,
10545 },
10546 lsp::ParameterInformation {
10547 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10548 documentation: None,
10549 },
10550 ]),
10551 active_parameter: None,
10552 }],
10553 active_signature: Some(0),
10554 active_parameter: Some(0),
10555 };
10556 handle_signature_help_request(&mut cx, mocked_response).await;
10557
10558 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10559 .await;
10560
10561 cx.editor(|editor, _, _| {
10562 let signature_help_state = editor.signature_help_state.popover().cloned();
10563 assert_eq!(
10564 signature_help_state.unwrap().label,
10565 "param1: u8, param2: u8"
10566 );
10567 });
10568}
10569
10570#[gpui::test]
10571async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10572 init_test(cx, |_| {});
10573
10574 cx.update(|cx| {
10575 cx.update_global::<SettingsStore, _>(|settings, cx| {
10576 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10577 settings.auto_signature_help = Some(false);
10578 settings.show_signature_help_after_edits = Some(false);
10579 });
10580 });
10581 });
10582
10583 let mut cx = EditorLspTestContext::new_rust(
10584 lsp::ServerCapabilities {
10585 signature_help_provider: Some(lsp::SignatureHelpOptions {
10586 ..Default::default()
10587 }),
10588 ..Default::default()
10589 },
10590 cx,
10591 )
10592 .await;
10593
10594 let language = Language::new(
10595 LanguageConfig {
10596 name: "Rust".into(),
10597 brackets: BracketPairConfig {
10598 pairs: vec![
10599 BracketPair {
10600 start: "{".to_string(),
10601 end: "}".to_string(),
10602 close: true,
10603 surround: true,
10604 newline: true,
10605 },
10606 BracketPair {
10607 start: "(".to_string(),
10608 end: ")".to_string(),
10609 close: true,
10610 surround: true,
10611 newline: true,
10612 },
10613 BracketPair {
10614 start: "/*".to_string(),
10615 end: " */".to_string(),
10616 close: true,
10617 surround: true,
10618 newline: true,
10619 },
10620 BracketPair {
10621 start: "[".to_string(),
10622 end: "]".to_string(),
10623 close: false,
10624 surround: false,
10625 newline: true,
10626 },
10627 BracketPair {
10628 start: "\"".to_string(),
10629 end: "\"".to_string(),
10630 close: true,
10631 surround: true,
10632 newline: false,
10633 },
10634 BracketPair {
10635 start: "<".to_string(),
10636 end: ">".to_string(),
10637 close: false,
10638 surround: true,
10639 newline: true,
10640 },
10641 ],
10642 ..Default::default()
10643 },
10644 autoclose_before: "})]".to_string(),
10645 ..Default::default()
10646 },
10647 Some(tree_sitter_rust::LANGUAGE.into()),
10648 );
10649 let language = Arc::new(language);
10650
10651 cx.language_registry().add(language.clone());
10652 cx.update_buffer(|buffer, cx| {
10653 buffer.set_language(Some(language), cx);
10654 });
10655
10656 // Ensure that signature_help is not called when no signature help is enabled.
10657 cx.set_state(
10658 &r#"
10659 fn main() {
10660 sampleˇ
10661 }
10662 "#
10663 .unindent(),
10664 );
10665 cx.update_editor(|editor, window, cx| {
10666 editor.handle_input("(", window, cx);
10667 });
10668 cx.assert_editor_state(
10669 &"
10670 fn main() {
10671 sample(ˇ)
10672 }
10673 "
10674 .unindent(),
10675 );
10676 cx.editor(|editor, _, _| {
10677 assert!(editor.signature_help_state.task().is_none());
10678 });
10679
10680 let mocked_response = lsp::SignatureHelp {
10681 signatures: vec![lsp::SignatureInformation {
10682 label: "fn sample(param1: u8, param2: u8)".to_string(),
10683 documentation: None,
10684 parameters: Some(vec![
10685 lsp::ParameterInformation {
10686 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10687 documentation: None,
10688 },
10689 lsp::ParameterInformation {
10690 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10691 documentation: None,
10692 },
10693 ]),
10694 active_parameter: None,
10695 }],
10696 active_signature: Some(0),
10697 active_parameter: Some(0),
10698 };
10699
10700 // Ensure that signature_help is called when enabled afte edits
10701 cx.update(|_, cx| {
10702 cx.update_global::<SettingsStore, _>(|settings, cx| {
10703 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10704 settings.auto_signature_help = Some(false);
10705 settings.show_signature_help_after_edits = Some(true);
10706 });
10707 });
10708 });
10709 cx.set_state(
10710 &r#"
10711 fn main() {
10712 sampleˇ
10713 }
10714 "#
10715 .unindent(),
10716 );
10717 cx.update_editor(|editor, window, cx| {
10718 editor.handle_input("(", window, cx);
10719 });
10720 cx.assert_editor_state(
10721 &"
10722 fn main() {
10723 sample(ˇ)
10724 }
10725 "
10726 .unindent(),
10727 );
10728 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10729 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10730 .await;
10731 cx.update_editor(|editor, _, _| {
10732 let signature_help_state = editor.signature_help_state.popover().cloned();
10733 assert!(signature_help_state.is_some());
10734 assert_eq!(
10735 signature_help_state.unwrap().label,
10736 "param1: u8, param2: u8"
10737 );
10738 editor.signature_help_state = SignatureHelpState::default();
10739 });
10740
10741 // Ensure that signature_help is called when auto signature help override is enabled
10742 cx.update(|_, cx| {
10743 cx.update_global::<SettingsStore, _>(|settings, cx| {
10744 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10745 settings.auto_signature_help = Some(true);
10746 settings.show_signature_help_after_edits = Some(false);
10747 });
10748 });
10749 });
10750 cx.set_state(
10751 &r#"
10752 fn main() {
10753 sampleˇ
10754 }
10755 "#
10756 .unindent(),
10757 );
10758 cx.update_editor(|editor, window, cx| {
10759 editor.handle_input("(", window, cx);
10760 });
10761 cx.assert_editor_state(
10762 &"
10763 fn main() {
10764 sample(ˇ)
10765 }
10766 "
10767 .unindent(),
10768 );
10769 handle_signature_help_request(&mut cx, mocked_response).await;
10770 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10771 .await;
10772 cx.editor(|editor, _, _| {
10773 let signature_help_state = editor.signature_help_state.popover().cloned();
10774 assert!(signature_help_state.is_some());
10775 assert_eq!(
10776 signature_help_state.unwrap().label,
10777 "param1: u8, param2: u8"
10778 );
10779 });
10780}
10781
10782#[gpui::test]
10783async fn test_signature_help(cx: &mut TestAppContext) {
10784 init_test(cx, |_| {});
10785 cx.update(|cx| {
10786 cx.update_global::<SettingsStore, _>(|settings, cx| {
10787 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10788 settings.auto_signature_help = Some(true);
10789 });
10790 });
10791 });
10792
10793 let mut cx = EditorLspTestContext::new_rust(
10794 lsp::ServerCapabilities {
10795 signature_help_provider: Some(lsp::SignatureHelpOptions {
10796 ..Default::default()
10797 }),
10798 ..Default::default()
10799 },
10800 cx,
10801 )
10802 .await;
10803
10804 // A test that directly calls `show_signature_help`
10805 cx.update_editor(|editor, window, cx| {
10806 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10807 });
10808
10809 let mocked_response = lsp::SignatureHelp {
10810 signatures: vec![lsp::SignatureInformation {
10811 label: "fn sample(param1: u8, param2: u8)".to_string(),
10812 documentation: None,
10813 parameters: Some(vec![
10814 lsp::ParameterInformation {
10815 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10816 documentation: None,
10817 },
10818 lsp::ParameterInformation {
10819 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10820 documentation: None,
10821 },
10822 ]),
10823 active_parameter: None,
10824 }],
10825 active_signature: Some(0),
10826 active_parameter: Some(0),
10827 };
10828 handle_signature_help_request(&mut cx, mocked_response).await;
10829
10830 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10831 .await;
10832
10833 cx.editor(|editor, _, _| {
10834 let signature_help_state = editor.signature_help_state.popover().cloned();
10835 assert!(signature_help_state.is_some());
10836 assert_eq!(
10837 signature_help_state.unwrap().label,
10838 "param1: u8, param2: u8"
10839 );
10840 });
10841
10842 // When exiting outside from inside the brackets, `signature_help` is closed.
10843 cx.set_state(indoc! {"
10844 fn main() {
10845 sample(ˇ);
10846 }
10847
10848 fn sample(param1: u8, param2: u8) {}
10849 "});
10850
10851 cx.update_editor(|editor, window, cx| {
10852 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10853 });
10854
10855 let mocked_response = lsp::SignatureHelp {
10856 signatures: Vec::new(),
10857 active_signature: None,
10858 active_parameter: None,
10859 };
10860 handle_signature_help_request(&mut cx, mocked_response).await;
10861
10862 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10863 .await;
10864
10865 cx.editor(|editor, _, _| {
10866 assert!(!editor.signature_help_state.is_shown());
10867 });
10868
10869 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10870 cx.set_state(indoc! {"
10871 fn main() {
10872 sample(ˇ);
10873 }
10874
10875 fn sample(param1: u8, param2: u8) {}
10876 "});
10877
10878 let mocked_response = lsp::SignatureHelp {
10879 signatures: vec![lsp::SignatureInformation {
10880 label: "fn sample(param1: u8, param2: u8)".to_string(),
10881 documentation: None,
10882 parameters: Some(vec![
10883 lsp::ParameterInformation {
10884 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10885 documentation: None,
10886 },
10887 lsp::ParameterInformation {
10888 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10889 documentation: None,
10890 },
10891 ]),
10892 active_parameter: None,
10893 }],
10894 active_signature: Some(0),
10895 active_parameter: Some(0),
10896 };
10897 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10898 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10899 .await;
10900 cx.editor(|editor, _, _| {
10901 assert!(editor.signature_help_state.is_shown());
10902 });
10903
10904 // Restore the popover with more parameter input
10905 cx.set_state(indoc! {"
10906 fn main() {
10907 sample(param1, param2ˇ);
10908 }
10909
10910 fn sample(param1: u8, param2: u8) {}
10911 "});
10912
10913 let mocked_response = lsp::SignatureHelp {
10914 signatures: vec![lsp::SignatureInformation {
10915 label: "fn sample(param1: u8, param2: u8)".to_string(),
10916 documentation: None,
10917 parameters: Some(vec![
10918 lsp::ParameterInformation {
10919 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10920 documentation: None,
10921 },
10922 lsp::ParameterInformation {
10923 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10924 documentation: None,
10925 },
10926 ]),
10927 active_parameter: None,
10928 }],
10929 active_signature: Some(0),
10930 active_parameter: Some(1),
10931 };
10932 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10933 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10934 .await;
10935
10936 // When selecting a range, the popover is gone.
10937 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10938 cx.update_editor(|editor, window, cx| {
10939 editor.change_selections(None, window, cx, |s| {
10940 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10941 })
10942 });
10943 cx.assert_editor_state(indoc! {"
10944 fn main() {
10945 sample(param1, «ˇparam2»);
10946 }
10947
10948 fn sample(param1: u8, param2: u8) {}
10949 "});
10950 cx.editor(|editor, _, _| {
10951 assert!(!editor.signature_help_state.is_shown());
10952 });
10953
10954 // When unselecting again, the popover is back if within the brackets.
10955 cx.update_editor(|editor, window, cx| {
10956 editor.change_selections(None, window, cx, |s| {
10957 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10958 })
10959 });
10960 cx.assert_editor_state(indoc! {"
10961 fn main() {
10962 sample(param1, ˇparam2);
10963 }
10964
10965 fn sample(param1: u8, param2: u8) {}
10966 "});
10967 handle_signature_help_request(&mut cx, mocked_response).await;
10968 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10969 .await;
10970 cx.editor(|editor, _, _| {
10971 assert!(editor.signature_help_state.is_shown());
10972 });
10973
10974 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10975 cx.update_editor(|editor, window, cx| {
10976 editor.change_selections(None, window, cx, |s| {
10977 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10978 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10979 })
10980 });
10981 cx.assert_editor_state(indoc! {"
10982 fn main() {
10983 sample(param1, ˇparam2);
10984 }
10985
10986 fn sample(param1: u8, param2: u8) {}
10987 "});
10988
10989 let mocked_response = lsp::SignatureHelp {
10990 signatures: vec![lsp::SignatureInformation {
10991 label: "fn sample(param1: u8, param2: u8)".to_string(),
10992 documentation: None,
10993 parameters: Some(vec![
10994 lsp::ParameterInformation {
10995 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10996 documentation: None,
10997 },
10998 lsp::ParameterInformation {
10999 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11000 documentation: None,
11001 },
11002 ]),
11003 active_parameter: None,
11004 }],
11005 active_signature: Some(0),
11006 active_parameter: Some(1),
11007 };
11008 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11009 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11010 .await;
11011 cx.update_editor(|editor, _, cx| {
11012 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11013 });
11014 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11015 .await;
11016 cx.update_editor(|editor, window, cx| {
11017 editor.change_selections(None, window, cx, |s| {
11018 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11019 })
11020 });
11021 cx.assert_editor_state(indoc! {"
11022 fn main() {
11023 sample(param1, «ˇparam2»);
11024 }
11025
11026 fn sample(param1: u8, param2: u8) {}
11027 "});
11028 cx.update_editor(|editor, window, cx| {
11029 editor.change_selections(None, window, cx, |s| {
11030 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11031 })
11032 });
11033 cx.assert_editor_state(indoc! {"
11034 fn main() {
11035 sample(param1, ˇparam2);
11036 }
11037
11038 fn sample(param1: u8, param2: u8) {}
11039 "});
11040 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11041 .await;
11042}
11043
11044#[gpui::test]
11045async fn test_completion_mode(cx: &mut TestAppContext) {
11046 init_test(cx, |_| {});
11047 let mut cx = EditorLspTestContext::new_rust(
11048 lsp::ServerCapabilities {
11049 completion_provider: Some(lsp::CompletionOptions {
11050 resolve_provider: Some(true),
11051 ..Default::default()
11052 }),
11053 ..Default::default()
11054 },
11055 cx,
11056 )
11057 .await;
11058
11059 struct Run {
11060 run_description: &'static str,
11061 initial_state: String,
11062 buffer_marked_text: String,
11063 completion_label: &'static str,
11064 completion_text: &'static str,
11065 expected_with_insert_mode: String,
11066 expected_with_replace_mode: String,
11067 expected_with_replace_subsequence_mode: String,
11068 expected_with_replace_suffix_mode: String,
11069 }
11070
11071 let runs = [
11072 Run {
11073 run_description: "Start of word matches completion text",
11074 initial_state: "before ediˇ after".into(),
11075 buffer_marked_text: "before <edi|> after".into(),
11076 completion_label: "editor",
11077 completion_text: "editor",
11078 expected_with_insert_mode: "before editorˇ after".into(),
11079 expected_with_replace_mode: "before editorˇ after".into(),
11080 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11081 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11082 },
11083 Run {
11084 run_description: "Accept same text at the middle of the word",
11085 initial_state: "before ediˇtor after".into(),
11086 buffer_marked_text: "before <edi|tor> after".into(),
11087 completion_label: "editor",
11088 completion_text: "editor",
11089 expected_with_insert_mode: "before editorˇtor after".into(),
11090 expected_with_replace_mode: "before editorˇ after".into(),
11091 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11092 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11093 },
11094 Run {
11095 run_description: "End of word matches completion text -- cursor at end",
11096 initial_state: "before torˇ after".into(),
11097 buffer_marked_text: "before <tor|> after".into(),
11098 completion_label: "editor",
11099 completion_text: "editor",
11100 expected_with_insert_mode: "before editorˇ after".into(),
11101 expected_with_replace_mode: "before editorˇ after".into(),
11102 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11103 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11104 },
11105 Run {
11106 run_description: "End of word matches completion text -- cursor at start",
11107 initial_state: "before ˇtor after".into(),
11108 buffer_marked_text: "before <|tor> after".into(),
11109 completion_label: "editor",
11110 completion_text: "editor",
11111 expected_with_insert_mode: "before editorˇtor after".into(),
11112 expected_with_replace_mode: "before editorˇ after".into(),
11113 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11114 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11115 },
11116 Run {
11117 run_description: "Prepend text containing whitespace",
11118 initial_state: "pˇfield: bool".into(),
11119 buffer_marked_text: "<p|field>: bool".into(),
11120 completion_label: "pub ",
11121 completion_text: "pub ",
11122 expected_with_insert_mode: "pub ˇfield: bool".into(),
11123 expected_with_replace_mode: "pub ˇ: bool".into(),
11124 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11125 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11126 },
11127 Run {
11128 run_description: "Add element to start of list",
11129 initial_state: "[element_ˇelement_2]".into(),
11130 buffer_marked_text: "[<element_|element_2>]".into(),
11131 completion_label: "element_1",
11132 completion_text: "element_1",
11133 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11134 expected_with_replace_mode: "[element_1ˇ]".into(),
11135 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11136 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11137 },
11138 Run {
11139 run_description: "Add element to start of list -- first and second elements are equal",
11140 initial_state: "[elˇelement]".into(),
11141 buffer_marked_text: "[<el|element>]".into(),
11142 completion_label: "element",
11143 completion_text: "element",
11144 expected_with_insert_mode: "[elementˇelement]".into(),
11145 expected_with_replace_mode: "[elementˇ]".into(),
11146 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11147 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11148 },
11149 Run {
11150 run_description: "Ends with matching suffix",
11151 initial_state: "SubˇError".into(),
11152 buffer_marked_text: "<Sub|Error>".into(),
11153 completion_label: "SubscriptionError",
11154 completion_text: "SubscriptionError",
11155 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11156 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11157 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11158 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11159 },
11160 Run {
11161 run_description: "Suffix is a subsequence -- contiguous",
11162 initial_state: "SubˇErr".into(),
11163 buffer_marked_text: "<Sub|Err>".into(),
11164 completion_label: "SubscriptionError",
11165 completion_text: "SubscriptionError",
11166 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11167 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11168 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11169 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11170 },
11171 Run {
11172 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11173 initial_state: "Suˇscrirr".into(),
11174 buffer_marked_text: "<Su|scrirr>".into(),
11175 completion_label: "SubscriptionError",
11176 completion_text: "SubscriptionError",
11177 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11178 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11179 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11180 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11181 },
11182 Run {
11183 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11184 initial_state: "foo(indˇix)".into(),
11185 buffer_marked_text: "foo(<ind|ix>)".into(),
11186 completion_label: "node_index",
11187 completion_text: "node_index",
11188 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11189 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11190 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11191 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11192 },
11193 Run {
11194 run_description: "Replace range ends before cursor - should extend to cursor",
11195 initial_state: "before editˇo after".into(),
11196 buffer_marked_text: "before <{ed}>it|o after".into(),
11197 completion_label: "editor",
11198 completion_text: "editor",
11199 expected_with_insert_mode: "before editorˇo after".into(),
11200 expected_with_replace_mode: "before editorˇo after".into(),
11201 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11202 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11203 },
11204 Run {
11205 run_description: "Uses label for suffix matching",
11206 initial_state: "before ediˇtor after".into(),
11207 buffer_marked_text: "before <edi|tor> after".into(),
11208 completion_label: "editor",
11209 completion_text: "editor()",
11210 expected_with_insert_mode: "before editor()ˇtor after".into(),
11211 expected_with_replace_mode: "before editor()ˇ after".into(),
11212 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11213 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11214 },
11215 Run {
11216 run_description: "Case insensitive subsequence and suffix matching",
11217 initial_state: "before EDiˇtoR after".into(),
11218 buffer_marked_text: "before <EDi|toR> after".into(),
11219 completion_label: "editor",
11220 completion_text: "editor",
11221 expected_with_insert_mode: "before editorˇtoR after".into(),
11222 expected_with_replace_mode: "before editorˇ after".into(),
11223 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11224 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11225 },
11226 ];
11227
11228 for run in runs {
11229 let run_variations = [
11230 (LspInsertMode::Insert, run.expected_with_insert_mode),
11231 (LspInsertMode::Replace, run.expected_with_replace_mode),
11232 (
11233 LspInsertMode::ReplaceSubsequence,
11234 run.expected_with_replace_subsequence_mode,
11235 ),
11236 (
11237 LspInsertMode::ReplaceSuffix,
11238 run.expected_with_replace_suffix_mode,
11239 ),
11240 ];
11241
11242 for (lsp_insert_mode, expected_text) in run_variations {
11243 eprintln!(
11244 "run = {:?}, mode = {lsp_insert_mode:.?}",
11245 run.run_description,
11246 );
11247
11248 update_test_language_settings(&mut cx, |settings| {
11249 settings.defaults.completions = Some(CompletionSettings {
11250 lsp_insert_mode,
11251 words: WordsCompletionMode::Disabled,
11252 lsp: true,
11253 lsp_fetch_timeout_ms: 0,
11254 });
11255 });
11256
11257 cx.set_state(&run.initial_state);
11258 cx.update_editor(|editor, window, cx| {
11259 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11260 });
11261
11262 let counter = Arc::new(AtomicUsize::new(0));
11263 handle_completion_request_with_insert_and_replace(
11264 &mut cx,
11265 &run.buffer_marked_text,
11266 vec![(run.completion_label, run.completion_text)],
11267 counter.clone(),
11268 )
11269 .await;
11270 cx.condition(|editor, _| editor.context_menu_visible())
11271 .await;
11272 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11273
11274 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11275 editor
11276 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11277 .unwrap()
11278 });
11279 cx.assert_editor_state(&expected_text);
11280 handle_resolve_completion_request(&mut cx, None).await;
11281 apply_additional_edits.await.unwrap();
11282 }
11283 }
11284}
11285
11286#[gpui::test]
11287async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11288 init_test(cx, |_| {});
11289 let mut cx = EditorLspTestContext::new_rust(
11290 lsp::ServerCapabilities {
11291 completion_provider: Some(lsp::CompletionOptions {
11292 resolve_provider: Some(true),
11293 ..Default::default()
11294 }),
11295 ..Default::default()
11296 },
11297 cx,
11298 )
11299 .await;
11300
11301 let initial_state = "SubˇError";
11302 let buffer_marked_text = "<Sub|Error>";
11303 let completion_text = "SubscriptionError";
11304 let expected_with_insert_mode = "SubscriptionErrorˇError";
11305 let expected_with_replace_mode = "SubscriptionErrorˇ";
11306
11307 update_test_language_settings(&mut cx, |settings| {
11308 settings.defaults.completions = Some(CompletionSettings {
11309 words: WordsCompletionMode::Disabled,
11310 // set the opposite here to ensure that the action is overriding the default behavior
11311 lsp_insert_mode: LspInsertMode::Insert,
11312 lsp: true,
11313 lsp_fetch_timeout_ms: 0,
11314 });
11315 });
11316
11317 cx.set_state(initial_state);
11318 cx.update_editor(|editor, window, cx| {
11319 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11320 });
11321
11322 let counter = Arc::new(AtomicUsize::new(0));
11323 handle_completion_request_with_insert_and_replace(
11324 &mut cx,
11325 &buffer_marked_text,
11326 vec![(completion_text, completion_text)],
11327 counter.clone(),
11328 )
11329 .await;
11330 cx.condition(|editor, _| editor.context_menu_visible())
11331 .await;
11332 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11333
11334 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11335 editor
11336 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11337 .unwrap()
11338 });
11339 cx.assert_editor_state(&expected_with_replace_mode);
11340 handle_resolve_completion_request(&mut cx, None).await;
11341 apply_additional_edits.await.unwrap();
11342
11343 update_test_language_settings(&mut cx, |settings| {
11344 settings.defaults.completions = Some(CompletionSettings {
11345 words: WordsCompletionMode::Disabled,
11346 // set the opposite here to ensure that the action is overriding the default behavior
11347 lsp_insert_mode: LspInsertMode::Replace,
11348 lsp: true,
11349 lsp_fetch_timeout_ms: 0,
11350 });
11351 });
11352
11353 cx.set_state(initial_state);
11354 cx.update_editor(|editor, window, cx| {
11355 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11356 });
11357 handle_completion_request_with_insert_and_replace(
11358 &mut cx,
11359 &buffer_marked_text,
11360 vec![(completion_text, completion_text)],
11361 counter.clone(),
11362 )
11363 .await;
11364 cx.condition(|editor, _| editor.context_menu_visible())
11365 .await;
11366 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11367
11368 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11369 editor
11370 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11371 .unwrap()
11372 });
11373 cx.assert_editor_state(&expected_with_insert_mode);
11374 handle_resolve_completion_request(&mut cx, None).await;
11375 apply_additional_edits.await.unwrap();
11376}
11377
11378#[gpui::test]
11379async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11380 init_test(cx, |_| {});
11381 let mut cx = EditorLspTestContext::new_rust(
11382 lsp::ServerCapabilities {
11383 completion_provider: Some(lsp::CompletionOptions {
11384 resolve_provider: Some(true),
11385 ..Default::default()
11386 }),
11387 ..Default::default()
11388 },
11389 cx,
11390 )
11391 .await;
11392
11393 // scenario: surrounding text matches completion text
11394 let completion_text = "to_offset";
11395 let initial_state = indoc! {"
11396 1. buf.to_offˇsuffix
11397 2. buf.to_offˇsuf
11398 3. buf.to_offˇfix
11399 4. buf.to_offˇ
11400 5. into_offˇensive
11401 6. ˇsuffix
11402 7. let ˇ //
11403 8. aaˇzz
11404 9. buf.to_off«zzzzzˇ»suffix
11405 10. buf.«ˇzzzzz»suffix
11406 11. to_off«ˇzzzzz»
11407
11408 buf.to_offˇsuffix // newest cursor
11409 "};
11410 let completion_marked_buffer = indoc! {"
11411 1. buf.to_offsuffix
11412 2. buf.to_offsuf
11413 3. buf.to_offfix
11414 4. buf.to_off
11415 5. into_offensive
11416 6. suffix
11417 7. let //
11418 8. aazz
11419 9. buf.to_offzzzzzsuffix
11420 10. buf.zzzzzsuffix
11421 11. to_offzzzzz
11422
11423 buf.<to_off|suffix> // newest cursor
11424 "};
11425 let expected = indoc! {"
11426 1. buf.to_offsetˇ
11427 2. buf.to_offsetˇsuf
11428 3. buf.to_offsetˇfix
11429 4. buf.to_offsetˇ
11430 5. into_offsetˇensive
11431 6. to_offsetˇsuffix
11432 7. let to_offsetˇ //
11433 8. aato_offsetˇzz
11434 9. buf.to_offsetˇ
11435 10. buf.to_offsetˇsuffix
11436 11. to_offsetˇ
11437
11438 buf.to_offsetˇ // newest cursor
11439 "};
11440 cx.set_state(initial_state);
11441 cx.update_editor(|editor, window, cx| {
11442 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11443 });
11444 handle_completion_request_with_insert_and_replace(
11445 &mut cx,
11446 completion_marked_buffer,
11447 vec![(completion_text, completion_text)],
11448 Arc::new(AtomicUsize::new(0)),
11449 )
11450 .await;
11451 cx.condition(|editor, _| editor.context_menu_visible())
11452 .await;
11453 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11454 editor
11455 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11456 .unwrap()
11457 });
11458 cx.assert_editor_state(expected);
11459 handle_resolve_completion_request(&mut cx, None).await;
11460 apply_additional_edits.await.unwrap();
11461
11462 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11463 let completion_text = "foo_and_bar";
11464 let initial_state = indoc! {"
11465 1. ooanbˇ
11466 2. zooanbˇ
11467 3. ooanbˇz
11468 4. zooanbˇz
11469 5. ooanˇ
11470 6. oanbˇ
11471
11472 ooanbˇ
11473 "};
11474 let completion_marked_buffer = indoc! {"
11475 1. ooanb
11476 2. zooanb
11477 3. ooanbz
11478 4. zooanbz
11479 5. ooan
11480 6. oanb
11481
11482 <ooanb|>
11483 "};
11484 let expected = indoc! {"
11485 1. foo_and_barˇ
11486 2. zfoo_and_barˇ
11487 3. foo_and_barˇz
11488 4. zfoo_and_barˇz
11489 5. ooanfoo_and_barˇ
11490 6. oanbfoo_and_barˇ
11491
11492 foo_and_barˇ
11493 "};
11494 cx.set_state(initial_state);
11495 cx.update_editor(|editor, window, cx| {
11496 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11497 });
11498 handle_completion_request_with_insert_and_replace(
11499 &mut cx,
11500 completion_marked_buffer,
11501 vec![(completion_text, completion_text)],
11502 Arc::new(AtomicUsize::new(0)),
11503 )
11504 .await;
11505 cx.condition(|editor, _| editor.context_menu_visible())
11506 .await;
11507 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11508 editor
11509 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11510 .unwrap()
11511 });
11512 cx.assert_editor_state(expected);
11513 handle_resolve_completion_request(&mut cx, None).await;
11514 apply_additional_edits.await.unwrap();
11515
11516 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11517 // (expects the same as if it was inserted at the end)
11518 let completion_text = "foo_and_bar";
11519 let initial_state = indoc! {"
11520 1. ooˇanb
11521 2. zooˇanb
11522 3. ooˇanbz
11523 4. zooˇanbz
11524
11525 ooˇanb
11526 "};
11527 let completion_marked_buffer = indoc! {"
11528 1. ooanb
11529 2. zooanb
11530 3. ooanbz
11531 4. zooanbz
11532
11533 <oo|anb>
11534 "};
11535 let expected = indoc! {"
11536 1. foo_and_barˇ
11537 2. zfoo_and_barˇ
11538 3. foo_and_barˇz
11539 4. zfoo_and_barˇz
11540
11541 foo_and_barˇ
11542 "};
11543 cx.set_state(initial_state);
11544 cx.update_editor(|editor, window, cx| {
11545 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11546 });
11547 handle_completion_request_with_insert_and_replace(
11548 &mut cx,
11549 completion_marked_buffer,
11550 vec![(completion_text, completion_text)],
11551 Arc::new(AtomicUsize::new(0)),
11552 )
11553 .await;
11554 cx.condition(|editor, _| editor.context_menu_visible())
11555 .await;
11556 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11557 editor
11558 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11559 .unwrap()
11560 });
11561 cx.assert_editor_state(expected);
11562 handle_resolve_completion_request(&mut cx, None).await;
11563 apply_additional_edits.await.unwrap();
11564}
11565
11566// This used to crash
11567#[gpui::test]
11568async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11569 init_test(cx, |_| {});
11570
11571 let buffer_text = indoc! {"
11572 fn main() {
11573 10.satu;
11574
11575 //
11576 // separate cursors so they open in different excerpts (manually reproducible)
11577 //
11578
11579 10.satu20;
11580 }
11581 "};
11582 let multibuffer_text_with_selections = indoc! {"
11583 fn main() {
11584 10.satuˇ;
11585
11586 //
11587
11588 //
11589
11590 10.satuˇ20;
11591 }
11592 "};
11593 let expected_multibuffer = indoc! {"
11594 fn main() {
11595 10.saturating_sub()ˇ;
11596
11597 //
11598
11599 //
11600
11601 10.saturating_sub()ˇ;
11602 }
11603 "};
11604
11605 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11606 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11607
11608 let fs = FakeFs::new(cx.executor());
11609 fs.insert_tree(
11610 path!("/a"),
11611 json!({
11612 "main.rs": buffer_text,
11613 }),
11614 )
11615 .await;
11616
11617 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11618 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11619 language_registry.add(rust_lang());
11620 let mut fake_servers = language_registry.register_fake_lsp(
11621 "Rust",
11622 FakeLspAdapter {
11623 capabilities: lsp::ServerCapabilities {
11624 completion_provider: Some(lsp::CompletionOptions {
11625 resolve_provider: None,
11626 ..lsp::CompletionOptions::default()
11627 }),
11628 ..lsp::ServerCapabilities::default()
11629 },
11630 ..FakeLspAdapter::default()
11631 },
11632 );
11633 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11634 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11635 let buffer = project
11636 .update(cx, |project, cx| {
11637 project.open_local_buffer(path!("/a/main.rs"), cx)
11638 })
11639 .await
11640 .unwrap();
11641
11642 let multi_buffer = cx.new(|cx| {
11643 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11644 multi_buffer.push_excerpts(
11645 buffer.clone(),
11646 [ExcerptRange::new(0..first_excerpt_end)],
11647 cx,
11648 );
11649 multi_buffer.push_excerpts(
11650 buffer.clone(),
11651 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11652 cx,
11653 );
11654 multi_buffer
11655 });
11656
11657 let editor = workspace
11658 .update(cx, |_, window, cx| {
11659 cx.new(|cx| {
11660 Editor::new(
11661 EditorMode::Full {
11662 scale_ui_elements_with_buffer_font_size: false,
11663 show_active_line_background: false,
11664 sized_by_content: false,
11665 },
11666 multi_buffer.clone(),
11667 Some(project.clone()),
11668 window,
11669 cx,
11670 )
11671 })
11672 })
11673 .unwrap();
11674
11675 let pane = workspace
11676 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11677 .unwrap();
11678 pane.update_in(cx, |pane, window, cx| {
11679 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11680 });
11681
11682 let fake_server = fake_servers.next().await.unwrap();
11683
11684 editor.update_in(cx, |editor, window, cx| {
11685 editor.change_selections(None, window, cx, |s| {
11686 s.select_ranges([
11687 Point::new(1, 11)..Point::new(1, 11),
11688 Point::new(7, 11)..Point::new(7, 11),
11689 ])
11690 });
11691
11692 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11693 });
11694
11695 editor.update_in(cx, |editor, window, cx| {
11696 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11697 });
11698
11699 fake_server
11700 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11701 let completion_item = lsp::CompletionItem {
11702 label: "saturating_sub()".into(),
11703 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11704 lsp::InsertReplaceEdit {
11705 new_text: "saturating_sub()".to_owned(),
11706 insert: lsp::Range::new(
11707 lsp::Position::new(7, 7),
11708 lsp::Position::new(7, 11),
11709 ),
11710 replace: lsp::Range::new(
11711 lsp::Position::new(7, 7),
11712 lsp::Position::new(7, 13),
11713 ),
11714 },
11715 )),
11716 ..lsp::CompletionItem::default()
11717 };
11718
11719 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11720 })
11721 .next()
11722 .await
11723 .unwrap();
11724
11725 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11726 .await;
11727
11728 editor
11729 .update_in(cx, |editor, window, cx| {
11730 editor
11731 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11732 .unwrap()
11733 })
11734 .await
11735 .unwrap();
11736
11737 editor.update(cx, |editor, cx| {
11738 assert_text_with_selections(editor, expected_multibuffer, cx);
11739 })
11740}
11741
11742#[gpui::test]
11743async fn test_completion(cx: &mut TestAppContext) {
11744 init_test(cx, |_| {});
11745
11746 let mut cx = EditorLspTestContext::new_rust(
11747 lsp::ServerCapabilities {
11748 completion_provider: Some(lsp::CompletionOptions {
11749 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11750 resolve_provider: Some(true),
11751 ..Default::default()
11752 }),
11753 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11754 ..Default::default()
11755 },
11756 cx,
11757 )
11758 .await;
11759 let counter = Arc::new(AtomicUsize::new(0));
11760
11761 cx.set_state(indoc! {"
11762 oneˇ
11763 two
11764 three
11765 "});
11766 cx.simulate_keystroke(".");
11767 handle_completion_request(
11768 indoc! {"
11769 one.|<>
11770 two
11771 three
11772 "},
11773 vec!["first_completion", "second_completion"],
11774 true,
11775 counter.clone(),
11776 &mut cx,
11777 )
11778 .await;
11779 cx.condition(|editor, _| editor.context_menu_visible())
11780 .await;
11781 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11782
11783 let _handler = handle_signature_help_request(
11784 &mut cx,
11785 lsp::SignatureHelp {
11786 signatures: vec![lsp::SignatureInformation {
11787 label: "test signature".to_string(),
11788 documentation: None,
11789 parameters: Some(vec![lsp::ParameterInformation {
11790 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11791 documentation: None,
11792 }]),
11793 active_parameter: None,
11794 }],
11795 active_signature: None,
11796 active_parameter: None,
11797 },
11798 );
11799 cx.update_editor(|editor, window, cx| {
11800 assert!(
11801 !editor.signature_help_state.is_shown(),
11802 "No signature help was called for"
11803 );
11804 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11805 });
11806 cx.run_until_parked();
11807 cx.update_editor(|editor, _, _| {
11808 assert!(
11809 !editor.signature_help_state.is_shown(),
11810 "No signature help should be shown when completions menu is open"
11811 );
11812 });
11813
11814 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11815 editor.context_menu_next(&Default::default(), window, cx);
11816 editor
11817 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11818 .unwrap()
11819 });
11820 cx.assert_editor_state(indoc! {"
11821 one.second_completionˇ
11822 two
11823 three
11824 "});
11825
11826 handle_resolve_completion_request(
11827 &mut cx,
11828 Some(vec![
11829 (
11830 //This overlaps with the primary completion edit which is
11831 //misbehavior from the LSP spec, test that we filter it out
11832 indoc! {"
11833 one.second_ˇcompletion
11834 two
11835 threeˇ
11836 "},
11837 "overlapping additional edit",
11838 ),
11839 (
11840 indoc! {"
11841 one.second_completion
11842 two
11843 threeˇ
11844 "},
11845 "\nadditional edit",
11846 ),
11847 ]),
11848 )
11849 .await;
11850 apply_additional_edits.await.unwrap();
11851 cx.assert_editor_state(indoc! {"
11852 one.second_completionˇ
11853 two
11854 three
11855 additional edit
11856 "});
11857
11858 cx.set_state(indoc! {"
11859 one.second_completion
11860 twoˇ
11861 threeˇ
11862 additional edit
11863 "});
11864 cx.simulate_keystroke(" ");
11865 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11866 cx.simulate_keystroke("s");
11867 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11868
11869 cx.assert_editor_state(indoc! {"
11870 one.second_completion
11871 two sˇ
11872 three sˇ
11873 additional edit
11874 "});
11875 handle_completion_request(
11876 indoc! {"
11877 one.second_completion
11878 two s
11879 three <s|>
11880 additional edit
11881 "},
11882 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11883 true,
11884 counter.clone(),
11885 &mut cx,
11886 )
11887 .await;
11888 cx.condition(|editor, _| editor.context_menu_visible())
11889 .await;
11890 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11891
11892 cx.simulate_keystroke("i");
11893
11894 handle_completion_request(
11895 indoc! {"
11896 one.second_completion
11897 two si
11898 three <si|>
11899 additional edit
11900 "},
11901 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11902 true,
11903 counter.clone(),
11904 &mut cx,
11905 )
11906 .await;
11907 cx.condition(|editor, _| editor.context_menu_visible())
11908 .await;
11909 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11910
11911 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11912 editor
11913 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11914 .unwrap()
11915 });
11916 cx.assert_editor_state(indoc! {"
11917 one.second_completion
11918 two sixth_completionˇ
11919 three sixth_completionˇ
11920 additional edit
11921 "});
11922
11923 apply_additional_edits.await.unwrap();
11924
11925 update_test_language_settings(&mut cx, |settings| {
11926 settings.defaults.show_completions_on_input = Some(false);
11927 });
11928 cx.set_state("editorˇ");
11929 cx.simulate_keystroke(".");
11930 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11931 cx.simulate_keystrokes("c l o");
11932 cx.assert_editor_state("editor.cloˇ");
11933 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11934 cx.update_editor(|editor, window, cx| {
11935 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11936 });
11937 handle_completion_request(
11938 "editor.<clo|>",
11939 vec!["close", "clobber"],
11940 true,
11941 counter.clone(),
11942 &mut cx,
11943 )
11944 .await;
11945 cx.condition(|editor, _| editor.context_menu_visible())
11946 .await;
11947 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11948
11949 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11950 editor
11951 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11952 .unwrap()
11953 });
11954 cx.assert_editor_state("editor.clobberˇ");
11955 handle_resolve_completion_request(&mut cx, None).await;
11956 apply_additional_edits.await.unwrap();
11957}
11958
11959#[gpui::test]
11960async fn test_completion_reuse(cx: &mut TestAppContext) {
11961 init_test(cx, |_| {});
11962
11963 let mut cx = EditorLspTestContext::new_rust(
11964 lsp::ServerCapabilities {
11965 completion_provider: Some(lsp::CompletionOptions {
11966 trigger_characters: Some(vec![".".to_string()]),
11967 ..Default::default()
11968 }),
11969 ..Default::default()
11970 },
11971 cx,
11972 )
11973 .await;
11974
11975 let counter = Arc::new(AtomicUsize::new(0));
11976 cx.set_state("objˇ");
11977 cx.simulate_keystroke(".");
11978
11979 // Initial completion request returns complete results
11980 let is_incomplete = false;
11981 handle_completion_request(
11982 "obj.|<>",
11983 vec!["a", "ab", "abc"],
11984 is_incomplete,
11985 counter.clone(),
11986 &mut cx,
11987 )
11988 .await;
11989 cx.run_until_parked();
11990 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11991 cx.assert_editor_state("obj.ˇ");
11992 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11993
11994 // Type "a" - filters existing completions
11995 cx.simulate_keystroke("a");
11996 cx.run_until_parked();
11997 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11998 cx.assert_editor_state("obj.aˇ");
11999 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12000
12001 // Type "b" - filters existing completions
12002 cx.simulate_keystroke("b");
12003 cx.run_until_parked();
12004 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12005 cx.assert_editor_state("obj.abˇ");
12006 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12007
12008 // Type "c" - filters existing completions
12009 cx.simulate_keystroke("c");
12010 cx.run_until_parked();
12011 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12012 cx.assert_editor_state("obj.abcˇ");
12013 check_displayed_completions(vec!["abc"], &mut cx);
12014
12015 // Backspace to delete "c" - filters existing completions
12016 cx.update_editor(|editor, window, cx| {
12017 editor.backspace(&Backspace, window, cx);
12018 });
12019 cx.run_until_parked();
12020 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12021 cx.assert_editor_state("obj.abˇ");
12022 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12023
12024 // Moving cursor to the left dismisses menu.
12025 cx.update_editor(|editor, window, cx| {
12026 editor.move_left(&MoveLeft, window, cx);
12027 });
12028 cx.run_until_parked();
12029 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12030 cx.assert_editor_state("obj.aˇb");
12031 cx.update_editor(|editor, _, _| {
12032 assert_eq!(editor.context_menu_visible(), false);
12033 });
12034
12035 // Type "b" - new request
12036 cx.simulate_keystroke("b");
12037 let is_incomplete = false;
12038 handle_completion_request(
12039 "obj.<ab|>a",
12040 vec!["ab", "abc"],
12041 is_incomplete,
12042 counter.clone(),
12043 &mut cx,
12044 )
12045 .await;
12046 cx.run_until_parked();
12047 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12048 cx.assert_editor_state("obj.abˇb");
12049 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12050
12051 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12052 cx.update_editor(|editor, window, cx| {
12053 editor.backspace(&Backspace, window, cx);
12054 });
12055 let is_incomplete = false;
12056 handle_completion_request(
12057 "obj.<a|>b",
12058 vec!["a", "ab", "abc"],
12059 is_incomplete,
12060 counter.clone(),
12061 &mut cx,
12062 )
12063 .await;
12064 cx.run_until_parked();
12065 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12066 cx.assert_editor_state("obj.aˇb");
12067 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12068
12069 // Backspace to delete "a" - dismisses menu.
12070 cx.update_editor(|editor, window, cx| {
12071 editor.backspace(&Backspace, window, cx);
12072 });
12073 cx.run_until_parked();
12074 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12075 cx.assert_editor_state("obj.ˇb");
12076 cx.update_editor(|editor, _, _| {
12077 assert_eq!(editor.context_menu_visible(), false);
12078 });
12079}
12080
12081#[gpui::test]
12082async fn test_word_completion(cx: &mut TestAppContext) {
12083 let lsp_fetch_timeout_ms = 10;
12084 init_test(cx, |language_settings| {
12085 language_settings.defaults.completions = Some(CompletionSettings {
12086 words: WordsCompletionMode::Fallback,
12087 lsp: true,
12088 lsp_fetch_timeout_ms: 10,
12089 lsp_insert_mode: LspInsertMode::Insert,
12090 });
12091 });
12092
12093 let mut cx = EditorLspTestContext::new_rust(
12094 lsp::ServerCapabilities {
12095 completion_provider: Some(lsp::CompletionOptions {
12096 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12097 ..lsp::CompletionOptions::default()
12098 }),
12099 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12100 ..lsp::ServerCapabilities::default()
12101 },
12102 cx,
12103 )
12104 .await;
12105
12106 let throttle_completions = Arc::new(AtomicBool::new(false));
12107
12108 let lsp_throttle_completions = throttle_completions.clone();
12109 let _completion_requests_handler =
12110 cx.lsp
12111 .server
12112 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12113 let lsp_throttle_completions = lsp_throttle_completions.clone();
12114 let cx = cx.clone();
12115 async move {
12116 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12117 cx.background_executor()
12118 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12119 .await;
12120 }
12121 Ok(Some(lsp::CompletionResponse::Array(vec![
12122 lsp::CompletionItem {
12123 label: "first".into(),
12124 ..lsp::CompletionItem::default()
12125 },
12126 lsp::CompletionItem {
12127 label: "last".into(),
12128 ..lsp::CompletionItem::default()
12129 },
12130 ])))
12131 }
12132 });
12133
12134 cx.set_state(indoc! {"
12135 oneˇ
12136 two
12137 three
12138 "});
12139 cx.simulate_keystroke(".");
12140 cx.executor().run_until_parked();
12141 cx.condition(|editor, _| editor.context_menu_visible())
12142 .await;
12143 cx.update_editor(|editor, window, cx| {
12144 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12145 {
12146 assert_eq!(
12147 completion_menu_entries(&menu),
12148 &["first", "last"],
12149 "When LSP server is fast to reply, no fallback word completions are used"
12150 );
12151 } else {
12152 panic!("expected completion menu to be open");
12153 }
12154 editor.cancel(&Cancel, window, cx);
12155 });
12156 cx.executor().run_until_parked();
12157 cx.condition(|editor, _| !editor.context_menu_visible())
12158 .await;
12159
12160 throttle_completions.store(true, atomic::Ordering::Release);
12161 cx.simulate_keystroke(".");
12162 cx.executor()
12163 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12164 cx.executor().run_until_parked();
12165 cx.condition(|editor, _| editor.context_menu_visible())
12166 .await;
12167 cx.update_editor(|editor, _, _| {
12168 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12169 {
12170 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12171 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12172 } else {
12173 panic!("expected completion menu to be open");
12174 }
12175 });
12176}
12177
12178#[gpui::test]
12179async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12180 init_test(cx, |language_settings| {
12181 language_settings.defaults.completions = Some(CompletionSettings {
12182 words: WordsCompletionMode::Enabled,
12183 lsp: true,
12184 lsp_fetch_timeout_ms: 0,
12185 lsp_insert_mode: LspInsertMode::Insert,
12186 });
12187 });
12188
12189 let mut cx = EditorLspTestContext::new_rust(
12190 lsp::ServerCapabilities {
12191 completion_provider: Some(lsp::CompletionOptions {
12192 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12193 ..lsp::CompletionOptions::default()
12194 }),
12195 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12196 ..lsp::ServerCapabilities::default()
12197 },
12198 cx,
12199 )
12200 .await;
12201
12202 let _completion_requests_handler =
12203 cx.lsp
12204 .server
12205 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12206 Ok(Some(lsp::CompletionResponse::Array(vec![
12207 lsp::CompletionItem {
12208 label: "first".into(),
12209 ..lsp::CompletionItem::default()
12210 },
12211 lsp::CompletionItem {
12212 label: "last".into(),
12213 ..lsp::CompletionItem::default()
12214 },
12215 ])))
12216 });
12217
12218 cx.set_state(indoc! {"ˇ
12219 first
12220 last
12221 second
12222 "});
12223 cx.simulate_keystroke(".");
12224 cx.executor().run_until_parked();
12225 cx.condition(|editor, _| editor.context_menu_visible())
12226 .await;
12227 cx.update_editor(|editor, _, _| {
12228 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12229 {
12230 assert_eq!(
12231 completion_menu_entries(&menu),
12232 &["first", "last", "second"],
12233 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12234 );
12235 } else {
12236 panic!("expected completion menu to be open");
12237 }
12238 });
12239}
12240
12241#[gpui::test]
12242async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12243 init_test(cx, |language_settings| {
12244 language_settings.defaults.completions = Some(CompletionSettings {
12245 words: WordsCompletionMode::Disabled,
12246 lsp: true,
12247 lsp_fetch_timeout_ms: 0,
12248 lsp_insert_mode: LspInsertMode::Insert,
12249 });
12250 });
12251
12252 let mut cx = EditorLspTestContext::new_rust(
12253 lsp::ServerCapabilities {
12254 completion_provider: Some(lsp::CompletionOptions {
12255 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12256 ..lsp::CompletionOptions::default()
12257 }),
12258 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12259 ..lsp::ServerCapabilities::default()
12260 },
12261 cx,
12262 )
12263 .await;
12264
12265 let _completion_requests_handler =
12266 cx.lsp
12267 .server
12268 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12269 panic!("LSP completions should not be queried when dealing with word completions")
12270 });
12271
12272 cx.set_state(indoc! {"ˇ
12273 first
12274 last
12275 second
12276 "});
12277 cx.update_editor(|editor, window, cx| {
12278 editor.show_word_completions(&ShowWordCompletions, window, cx);
12279 });
12280 cx.executor().run_until_parked();
12281 cx.condition(|editor, _| editor.context_menu_visible())
12282 .await;
12283 cx.update_editor(|editor, _, _| {
12284 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12285 {
12286 assert_eq!(
12287 completion_menu_entries(&menu),
12288 &["first", "last", "second"],
12289 "`ShowWordCompletions` action should show word completions"
12290 );
12291 } else {
12292 panic!("expected completion menu to be open");
12293 }
12294 });
12295
12296 cx.simulate_keystroke("l");
12297 cx.executor().run_until_parked();
12298 cx.condition(|editor, _| editor.context_menu_visible())
12299 .await;
12300 cx.update_editor(|editor, _, _| {
12301 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12302 {
12303 assert_eq!(
12304 completion_menu_entries(&menu),
12305 &["last"],
12306 "After showing word completions, further editing should filter them and not query the LSP"
12307 );
12308 } else {
12309 panic!("expected completion menu to be open");
12310 }
12311 });
12312}
12313
12314#[gpui::test]
12315async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12316 init_test(cx, |language_settings| {
12317 language_settings.defaults.completions = Some(CompletionSettings {
12318 words: WordsCompletionMode::Fallback,
12319 lsp: false,
12320 lsp_fetch_timeout_ms: 0,
12321 lsp_insert_mode: LspInsertMode::Insert,
12322 });
12323 });
12324
12325 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12326
12327 cx.set_state(indoc! {"ˇ
12328 0_usize
12329 let
12330 33
12331 4.5f32
12332 "});
12333 cx.update_editor(|editor, window, cx| {
12334 editor.show_completions(&ShowCompletions::default(), window, cx);
12335 });
12336 cx.executor().run_until_parked();
12337 cx.condition(|editor, _| editor.context_menu_visible())
12338 .await;
12339 cx.update_editor(|editor, window, cx| {
12340 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12341 {
12342 assert_eq!(
12343 completion_menu_entries(&menu),
12344 &["let"],
12345 "With no digits in the completion query, no digits should be in the word completions"
12346 );
12347 } else {
12348 panic!("expected completion menu to be open");
12349 }
12350 editor.cancel(&Cancel, window, cx);
12351 });
12352
12353 cx.set_state(indoc! {"3ˇ
12354 0_usize
12355 let
12356 3
12357 33.35f32
12358 "});
12359 cx.update_editor(|editor, window, cx| {
12360 editor.show_completions(&ShowCompletions::default(), window, cx);
12361 });
12362 cx.executor().run_until_parked();
12363 cx.condition(|editor, _| editor.context_menu_visible())
12364 .await;
12365 cx.update_editor(|editor, _, _| {
12366 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12367 {
12368 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12369 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12370 } else {
12371 panic!("expected completion menu to be open");
12372 }
12373 });
12374}
12375
12376fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12377 let position = || lsp::Position {
12378 line: params.text_document_position.position.line,
12379 character: params.text_document_position.position.character,
12380 };
12381 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12382 range: lsp::Range {
12383 start: position(),
12384 end: position(),
12385 },
12386 new_text: text.to_string(),
12387 }))
12388}
12389
12390#[gpui::test]
12391async fn test_multiline_completion(cx: &mut TestAppContext) {
12392 init_test(cx, |_| {});
12393
12394 let fs = FakeFs::new(cx.executor());
12395 fs.insert_tree(
12396 path!("/a"),
12397 json!({
12398 "main.ts": "a",
12399 }),
12400 )
12401 .await;
12402
12403 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12404 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12405 let typescript_language = Arc::new(Language::new(
12406 LanguageConfig {
12407 name: "TypeScript".into(),
12408 matcher: LanguageMatcher {
12409 path_suffixes: vec!["ts".to_string()],
12410 ..LanguageMatcher::default()
12411 },
12412 line_comments: vec!["// ".into()],
12413 ..LanguageConfig::default()
12414 },
12415 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12416 ));
12417 language_registry.add(typescript_language.clone());
12418 let mut fake_servers = language_registry.register_fake_lsp(
12419 "TypeScript",
12420 FakeLspAdapter {
12421 capabilities: lsp::ServerCapabilities {
12422 completion_provider: Some(lsp::CompletionOptions {
12423 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12424 ..lsp::CompletionOptions::default()
12425 }),
12426 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12427 ..lsp::ServerCapabilities::default()
12428 },
12429 // Emulate vtsls label generation
12430 label_for_completion: Some(Box::new(|item, _| {
12431 let text = if let Some(description) = item
12432 .label_details
12433 .as_ref()
12434 .and_then(|label_details| label_details.description.as_ref())
12435 {
12436 format!("{} {}", item.label, description)
12437 } else if let Some(detail) = &item.detail {
12438 format!("{} {}", item.label, detail)
12439 } else {
12440 item.label.clone()
12441 };
12442 let len = text.len();
12443 Some(language::CodeLabel {
12444 text,
12445 runs: Vec::new(),
12446 filter_range: 0..len,
12447 })
12448 })),
12449 ..FakeLspAdapter::default()
12450 },
12451 );
12452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12453 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12454 let worktree_id = workspace
12455 .update(cx, |workspace, _window, cx| {
12456 workspace.project().update(cx, |project, cx| {
12457 project.worktrees(cx).next().unwrap().read(cx).id()
12458 })
12459 })
12460 .unwrap();
12461 let _buffer = project
12462 .update(cx, |project, cx| {
12463 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12464 })
12465 .await
12466 .unwrap();
12467 let editor = workspace
12468 .update(cx, |workspace, window, cx| {
12469 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12470 })
12471 .unwrap()
12472 .await
12473 .unwrap()
12474 .downcast::<Editor>()
12475 .unwrap();
12476 let fake_server = fake_servers.next().await.unwrap();
12477
12478 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12479 let multiline_label_2 = "a\nb\nc\n";
12480 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12481 let multiline_description = "d\ne\nf\n";
12482 let multiline_detail_2 = "g\nh\ni\n";
12483
12484 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12485 move |params, _| async move {
12486 Ok(Some(lsp::CompletionResponse::Array(vec![
12487 lsp::CompletionItem {
12488 label: multiline_label.to_string(),
12489 text_edit: gen_text_edit(¶ms, "new_text_1"),
12490 ..lsp::CompletionItem::default()
12491 },
12492 lsp::CompletionItem {
12493 label: "single line label 1".to_string(),
12494 detail: Some(multiline_detail.to_string()),
12495 text_edit: gen_text_edit(¶ms, "new_text_2"),
12496 ..lsp::CompletionItem::default()
12497 },
12498 lsp::CompletionItem {
12499 label: "single line label 2".to_string(),
12500 label_details: Some(lsp::CompletionItemLabelDetails {
12501 description: Some(multiline_description.to_string()),
12502 detail: None,
12503 }),
12504 text_edit: gen_text_edit(¶ms, "new_text_2"),
12505 ..lsp::CompletionItem::default()
12506 },
12507 lsp::CompletionItem {
12508 label: multiline_label_2.to_string(),
12509 detail: Some(multiline_detail_2.to_string()),
12510 text_edit: gen_text_edit(¶ms, "new_text_3"),
12511 ..lsp::CompletionItem::default()
12512 },
12513 lsp::CompletionItem {
12514 label: "Label with many spaces and \t but without newlines".to_string(),
12515 detail: Some(
12516 "Details with many spaces and \t but without newlines".to_string(),
12517 ),
12518 text_edit: gen_text_edit(¶ms, "new_text_4"),
12519 ..lsp::CompletionItem::default()
12520 },
12521 ])))
12522 },
12523 );
12524
12525 editor.update_in(cx, |editor, window, cx| {
12526 cx.focus_self(window);
12527 editor.move_to_end(&MoveToEnd, window, cx);
12528 editor.handle_input(".", window, cx);
12529 });
12530 cx.run_until_parked();
12531 completion_handle.next().await.unwrap();
12532
12533 editor.update(cx, |editor, _| {
12534 assert!(editor.context_menu_visible());
12535 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12536 {
12537 let completion_labels = menu
12538 .completions
12539 .borrow()
12540 .iter()
12541 .map(|c| c.label.text.clone())
12542 .collect::<Vec<_>>();
12543 assert_eq!(
12544 completion_labels,
12545 &[
12546 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12547 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12548 "single line label 2 d e f ",
12549 "a b c g h i ",
12550 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12551 ],
12552 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12553 );
12554
12555 for completion in menu
12556 .completions
12557 .borrow()
12558 .iter() {
12559 assert_eq!(
12560 completion.label.filter_range,
12561 0..completion.label.text.len(),
12562 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12563 );
12564 }
12565 } else {
12566 panic!("expected completion menu to be open");
12567 }
12568 });
12569}
12570
12571#[gpui::test]
12572async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12573 init_test(cx, |_| {});
12574 let mut cx = EditorLspTestContext::new_rust(
12575 lsp::ServerCapabilities {
12576 completion_provider: Some(lsp::CompletionOptions {
12577 trigger_characters: Some(vec![".".to_string()]),
12578 ..Default::default()
12579 }),
12580 ..Default::default()
12581 },
12582 cx,
12583 )
12584 .await;
12585 cx.lsp
12586 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12587 Ok(Some(lsp::CompletionResponse::Array(vec![
12588 lsp::CompletionItem {
12589 label: "first".into(),
12590 ..Default::default()
12591 },
12592 lsp::CompletionItem {
12593 label: "last".into(),
12594 ..Default::default()
12595 },
12596 ])))
12597 });
12598 cx.set_state("variableˇ");
12599 cx.simulate_keystroke(".");
12600 cx.executor().run_until_parked();
12601
12602 cx.update_editor(|editor, _, _| {
12603 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12604 {
12605 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12606 } else {
12607 panic!("expected completion menu to be open");
12608 }
12609 });
12610
12611 cx.update_editor(|editor, window, cx| {
12612 editor.move_page_down(&MovePageDown::default(), window, cx);
12613 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12614 {
12615 assert!(
12616 menu.selected_item == 1,
12617 "expected PageDown to select the last item from the context menu"
12618 );
12619 } else {
12620 panic!("expected completion menu to stay open after PageDown");
12621 }
12622 });
12623
12624 cx.update_editor(|editor, window, cx| {
12625 editor.move_page_up(&MovePageUp::default(), window, cx);
12626 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12627 {
12628 assert!(
12629 menu.selected_item == 0,
12630 "expected PageUp to select the first item from the context menu"
12631 );
12632 } else {
12633 panic!("expected completion menu to stay open after PageUp");
12634 }
12635 });
12636}
12637
12638#[gpui::test]
12639async fn test_as_is_completions(cx: &mut TestAppContext) {
12640 init_test(cx, |_| {});
12641 let mut cx = EditorLspTestContext::new_rust(
12642 lsp::ServerCapabilities {
12643 completion_provider: Some(lsp::CompletionOptions {
12644 ..Default::default()
12645 }),
12646 ..Default::default()
12647 },
12648 cx,
12649 )
12650 .await;
12651 cx.lsp
12652 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12653 Ok(Some(lsp::CompletionResponse::Array(vec![
12654 lsp::CompletionItem {
12655 label: "unsafe".into(),
12656 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12657 range: lsp::Range {
12658 start: lsp::Position {
12659 line: 1,
12660 character: 2,
12661 },
12662 end: lsp::Position {
12663 line: 1,
12664 character: 3,
12665 },
12666 },
12667 new_text: "unsafe".to_string(),
12668 })),
12669 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12670 ..Default::default()
12671 },
12672 ])))
12673 });
12674 cx.set_state("fn a() {}\n nˇ");
12675 cx.executor().run_until_parked();
12676 cx.update_editor(|editor, window, cx| {
12677 editor.show_completions(
12678 &ShowCompletions {
12679 trigger: Some("\n".into()),
12680 },
12681 window,
12682 cx,
12683 );
12684 });
12685 cx.executor().run_until_parked();
12686
12687 cx.update_editor(|editor, window, cx| {
12688 editor.confirm_completion(&Default::default(), window, cx)
12689 });
12690 cx.executor().run_until_parked();
12691 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12692}
12693
12694#[gpui::test]
12695async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12696 init_test(cx, |_| {});
12697
12698 let mut cx = EditorLspTestContext::new_rust(
12699 lsp::ServerCapabilities {
12700 completion_provider: Some(lsp::CompletionOptions {
12701 trigger_characters: Some(vec![".".to_string()]),
12702 resolve_provider: Some(true),
12703 ..Default::default()
12704 }),
12705 ..Default::default()
12706 },
12707 cx,
12708 )
12709 .await;
12710
12711 cx.set_state("fn main() { let a = 2ˇ; }");
12712 cx.simulate_keystroke(".");
12713 let completion_item = lsp::CompletionItem {
12714 label: "Some".into(),
12715 kind: Some(lsp::CompletionItemKind::SNIPPET),
12716 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12717 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12718 kind: lsp::MarkupKind::Markdown,
12719 value: "```rust\nSome(2)\n```".to_string(),
12720 })),
12721 deprecated: Some(false),
12722 sort_text: Some("Some".to_string()),
12723 filter_text: Some("Some".to_string()),
12724 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12725 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12726 range: lsp::Range {
12727 start: lsp::Position {
12728 line: 0,
12729 character: 22,
12730 },
12731 end: lsp::Position {
12732 line: 0,
12733 character: 22,
12734 },
12735 },
12736 new_text: "Some(2)".to_string(),
12737 })),
12738 additional_text_edits: Some(vec![lsp::TextEdit {
12739 range: lsp::Range {
12740 start: lsp::Position {
12741 line: 0,
12742 character: 20,
12743 },
12744 end: lsp::Position {
12745 line: 0,
12746 character: 22,
12747 },
12748 },
12749 new_text: "".to_string(),
12750 }]),
12751 ..Default::default()
12752 };
12753
12754 let closure_completion_item = completion_item.clone();
12755 let counter = Arc::new(AtomicUsize::new(0));
12756 let counter_clone = counter.clone();
12757 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12758 let task_completion_item = closure_completion_item.clone();
12759 counter_clone.fetch_add(1, atomic::Ordering::Release);
12760 async move {
12761 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12762 is_incomplete: true,
12763 item_defaults: None,
12764 items: vec![task_completion_item],
12765 })))
12766 }
12767 });
12768
12769 cx.condition(|editor, _| editor.context_menu_visible())
12770 .await;
12771 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12772 assert!(request.next().await.is_some());
12773 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12774
12775 cx.simulate_keystrokes("S o m");
12776 cx.condition(|editor, _| editor.context_menu_visible())
12777 .await;
12778 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12779 assert!(request.next().await.is_some());
12780 assert!(request.next().await.is_some());
12781 assert!(request.next().await.is_some());
12782 request.close();
12783 assert!(request.next().await.is_none());
12784 assert_eq!(
12785 counter.load(atomic::Ordering::Acquire),
12786 4,
12787 "With the completions menu open, only one LSP request should happen per input"
12788 );
12789}
12790
12791#[gpui::test]
12792async fn test_toggle_comment(cx: &mut TestAppContext) {
12793 init_test(cx, |_| {});
12794 let mut cx = EditorTestContext::new(cx).await;
12795 let language = Arc::new(Language::new(
12796 LanguageConfig {
12797 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12798 ..Default::default()
12799 },
12800 Some(tree_sitter_rust::LANGUAGE.into()),
12801 ));
12802 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12803
12804 // If multiple selections intersect a line, the line is only toggled once.
12805 cx.set_state(indoc! {"
12806 fn a() {
12807 «//b();
12808 ˇ»// «c();
12809 //ˇ» d();
12810 }
12811 "});
12812
12813 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12814
12815 cx.assert_editor_state(indoc! {"
12816 fn a() {
12817 «b();
12818 c();
12819 ˇ» d();
12820 }
12821 "});
12822
12823 // The comment prefix is inserted at the same column for every line in a
12824 // selection.
12825 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12826
12827 cx.assert_editor_state(indoc! {"
12828 fn a() {
12829 // «b();
12830 // c();
12831 ˇ»// d();
12832 }
12833 "});
12834
12835 // If a selection ends at the beginning of a line, that line is not toggled.
12836 cx.set_selections_state(indoc! {"
12837 fn a() {
12838 // b();
12839 «// c();
12840 ˇ» // d();
12841 }
12842 "});
12843
12844 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12845
12846 cx.assert_editor_state(indoc! {"
12847 fn a() {
12848 // b();
12849 «c();
12850 ˇ» // d();
12851 }
12852 "});
12853
12854 // If a selection span a single line and is empty, the line is toggled.
12855 cx.set_state(indoc! {"
12856 fn a() {
12857 a();
12858 b();
12859 ˇ
12860 }
12861 "});
12862
12863 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12864
12865 cx.assert_editor_state(indoc! {"
12866 fn a() {
12867 a();
12868 b();
12869 //•ˇ
12870 }
12871 "});
12872
12873 // If a selection span multiple lines, empty lines are not toggled.
12874 cx.set_state(indoc! {"
12875 fn a() {
12876 «a();
12877
12878 c();ˇ»
12879 }
12880 "});
12881
12882 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12883
12884 cx.assert_editor_state(indoc! {"
12885 fn a() {
12886 // «a();
12887
12888 // c();ˇ»
12889 }
12890 "});
12891
12892 // If a selection includes multiple comment prefixes, all lines are uncommented.
12893 cx.set_state(indoc! {"
12894 fn a() {
12895 «// a();
12896 /// b();
12897 //! c();ˇ»
12898 }
12899 "});
12900
12901 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12902
12903 cx.assert_editor_state(indoc! {"
12904 fn a() {
12905 «a();
12906 b();
12907 c();ˇ»
12908 }
12909 "});
12910}
12911
12912#[gpui::test]
12913async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12914 init_test(cx, |_| {});
12915 let mut cx = EditorTestContext::new(cx).await;
12916 let language = Arc::new(Language::new(
12917 LanguageConfig {
12918 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12919 ..Default::default()
12920 },
12921 Some(tree_sitter_rust::LANGUAGE.into()),
12922 ));
12923 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12924
12925 let toggle_comments = &ToggleComments {
12926 advance_downwards: false,
12927 ignore_indent: true,
12928 };
12929
12930 // If multiple selections intersect a line, the line is only toggled once.
12931 cx.set_state(indoc! {"
12932 fn a() {
12933 // «b();
12934 // c();
12935 // ˇ» d();
12936 }
12937 "});
12938
12939 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12940
12941 cx.assert_editor_state(indoc! {"
12942 fn a() {
12943 «b();
12944 c();
12945 ˇ» d();
12946 }
12947 "});
12948
12949 // The comment prefix is inserted at the beginning of each line
12950 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12951
12952 cx.assert_editor_state(indoc! {"
12953 fn a() {
12954 // «b();
12955 // c();
12956 // ˇ» d();
12957 }
12958 "});
12959
12960 // If a selection ends at the beginning of a line, that line is not toggled.
12961 cx.set_selections_state(indoc! {"
12962 fn a() {
12963 // b();
12964 // «c();
12965 ˇ»// d();
12966 }
12967 "});
12968
12969 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12970
12971 cx.assert_editor_state(indoc! {"
12972 fn a() {
12973 // b();
12974 «c();
12975 ˇ»// d();
12976 }
12977 "});
12978
12979 // If a selection span a single line and is empty, the line is toggled.
12980 cx.set_state(indoc! {"
12981 fn a() {
12982 a();
12983 b();
12984 ˇ
12985 }
12986 "});
12987
12988 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12989
12990 cx.assert_editor_state(indoc! {"
12991 fn a() {
12992 a();
12993 b();
12994 //ˇ
12995 }
12996 "});
12997
12998 // If a selection span multiple lines, empty lines are not toggled.
12999 cx.set_state(indoc! {"
13000 fn a() {
13001 «a();
13002
13003 c();ˇ»
13004 }
13005 "});
13006
13007 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13008
13009 cx.assert_editor_state(indoc! {"
13010 fn a() {
13011 // «a();
13012
13013 // c();ˇ»
13014 }
13015 "});
13016
13017 // If a selection includes multiple comment prefixes, all lines are uncommented.
13018 cx.set_state(indoc! {"
13019 fn a() {
13020 // «a();
13021 /// b();
13022 //! c();ˇ»
13023 }
13024 "});
13025
13026 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13027
13028 cx.assert_editor_state(indoc! {"
13029 fn a() {
13030 «a();
13031 b();
13032 c();ˇ»
13033 }
13034 "});
13035}
13036
13037#[gpui::test]
13038async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13039 init_test(cx, |_| {});
13040
13041 let language = Arc::new(Language::new(
13042 LanguageConfig {
13043 line_comments: vec!["// ".into()],
13044 ..Default::default()
13045 },
13046 Some(tree_sitter_rust::LANGUAGE.into()),
13047 ));
13048
13049 let mut cx = EditorTestContext::new(cx).await;
13050
13051 cx.language_registry().add(language.clone());
13052 cx.update_buffer(|buffer, cx| {
13053 buffer.set_language(Some(language), cx);
13054 });
13055
13056 let toggle_comments = &ToggleComments {
13057 advance_downwards: true,
13058 ignore_indent: false,
13059 };
13060
13061 // Single cursor on one line -> advance
13062 // Cursor moves horizontally 3 characters as well on non-blank line
13063 cx.set_state(indoc!(
13064 "fn a() {
13065 ˇdog();
13066 cat();
13067 }"
13068 ));
13069 cx.update_editor(|editor, window, cx| {
13070 editor.toggle_comments(toggle_comments, window, cx);
13071 });
13072 cx.assert_editor_state(indoc!(
13073 "fn a() {
13074 // dog();
13075 catˇ();
13076 }"
13077 ));
13078
13079 // Single selection on one line -> don't advance
13080 cx.set_state(indoc!(
13081 "fn a() {
13082 «dog()ˇ»;
13083 cat();
13084 }"
13085 ));
13086 cx.update_editor(|editor, window, cx| {
13087 editor.toggle_comments(toggle_comments, window, cx);
13088 });
13089 cx.assert_editor_state(indoc!(
13090 "fn a() {
13091 // «dog()ˇ»;
13092 cat();
13093 }"
13094 ));
13095
13096 // Multiple cursors on one line -> advance
13097 cx.set_state(indoc!(
13098 "fn a() {
13099 ˇdˇog();
13100 cat();
13101 }"
13102 ));
13103 cx.update_editor(|editor, window, cx| {
13104 editor.toggle_comments(toggle_comments, window, cx);
13105 });
13106 cx.assert_editor_state(indoc!(
13107 "fn a() {
13108 // dog();
13109 catˇ(ˇ);
13110 }"
13111 ));
13112
13113 // Multiple cursors on one line, with selection -> don't advance
13114 cx.set_state(indoc!(
13115 "fn a() {
13116 ˇdˇog«()ˇ»;
13117 cat();
13118 }"
13119 ));
13120 cx.update_editor(|editor, window, cx| {
13121 editor.toggle_comments(toggle_comments, window, cx);
13122 });
13123 cx.assert_editor_state(indoc!(
13124 "fn a() {
13125 // ˇdˇog«()ˇ»;
13126 cat();
13127 }"
13128 ));
13129
13130 // Single cursor on one line -> advance
13131 // Cursor moves to column 0 on blank line
13132 cx.set_state(indoc!(
13133 "fn a() {
13134 ˇdog();
13135
13136 cat();
13137 }"
13138 ));
13139 cx.update_editor(|editor, window, cx| {
13140 editor.toggle_comments(toggle_comments, window, cx);
13141 });
13142 cx.assert_editor_state(indoc!(
13143 "fn a() {
13144 // dog();
13145 ˇ
13146 cat();
13147 }"
13148 ));
13149
13150 // Single cursor on one line -> advance
13151 // Cursor starts and ends at column 0
13152 cx.set_state(indoc!(
13153 "fn a() {
13154 ˇ dog();
13155 cat();
13156 }"
13157 ));
13158 cx.update_editor(|editor, window, cx| {
13159 editor.toggle_comments(toggle_comments, window, cx);
13160 });
13161 cx.assert_editor_state(indoc!(
13162 "fn a() {
13163 // dog();
13164 ˇ cat();
13165 }"
13166 ));
13167}
13168
13169#[gpui::test]
13170async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13171 init_test(cx, |_| {});
13172
13173 let mut cx = EditorTestContext::new(cx).await;
13174
13175 let html_language = Arc::new(
13176 Language::new(
13177 LanguageConfig {
13178 name: "HTML".into(),
13179 block_comment: Some(("<!-- ".into(), " -->".into())),
13180 ..Default::default()
13181 },
13182 Some(tree_sitter_html::LANGUAGE.into()),
13183 )
13184 .with_injection_query(
13185 r#"
13186 (script_element
13187 (raw_text) @injection.content
13188 (#set! injection.language "javascript"))
13189 "#,
13190 )
13191 .unwrap(),
13192 );
13193
13194 let javascript_language = Arc::new(Language::new(
13195 LanguageConfig {
13196 name: "JavaScript".into(),
13197 line_comments: vec!["// ".into()],
13198 ..Default::default()
13199 },
13200 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13201 ));
13202
13203 cx.language_registry().add(html_language.clone());
13204 cx.language_registry().add(javascript_language.clone());
13205 cx.update_buffer(|buffer, cx| {
13206 buffer.set_language(Some(html_language), cx);
13207 });
13208
13209 // Toggle comments for empty selections
13210 cx.set_state(
13211 &r#"
13212 <p>A</p>ˇ
13213 <p>B</p>ˇ
13214 <p>C</p>ˇ
13215 "#
13216 .unindent(),
13217 );
13218 cx.update_editor(|editor, window, cx| {
13219 editor.toggle_comments(&ToggleComments::default(), window, cx)
13220 });
13221 cx.assert_editor_state(
13222 &r#"
13223 <!-- <p>A</p>ˇ -->
13224 <!-- <p>B</p>ˇ -->
13225 <!-- <p>C</p>ˇ -->
13226 "#
13227 .unindent(),
13228 );
13229 cx.update_editor(|editor, window, cx| {
13230 editor.toggle_comments(&ToggleComments::default(), window, cx)
13231 });
13232 cx.assert_editor_state(
13233 &r#"
13234 <p>A</p>ˇ
13235 <p>B</p>ˇ
13236 <p>C</p>ˇ
13237 "#
13238 .unindent(),
13239 );
13240
13241 // Toggle comments for mixture of empty and non-empty selections, where
13242 // multiple selections occupy a given line.
13243 cx.set_state(
13244 &r#"
13245 <p>A«</p>
13246 <p>ˇ»B</p>ˇ
13247 <p>C«</p>
13248 <p>ˇ»D</p>ˇ
13249 "#
13250 .unindent(),
13251 );
13252
13253 cx.update_editor(|editor, window, cx| {
13254 editor.toggle_comments(&ToggleComments::default(), window, cx)
13255 });
13256 cx.assert_editor_state(
13257 &r#"
13258 <!-- <p>A«</p>
13259 <p>ˇ»B</p>ˇ -->
13260 <!-- <p>C«</p>
13261 <p>ˇ»D</p>ˇ -->
13262 "#
13263 .unindent(),
13264 );
13265 cx.update_editor(|editor, window, cx| {
13266 editor.toggle_comments(&ToggleComments::default(), window, cx)
13267 });
13268 cx.assert_editor_state(
13269 &r#"
13270 <p>A«</p>
13271 <p>ˇ»B</p>ˇ
13272 <p>C«</p>
13273 <p>ˇ»D</p>ˇ
13274 "#
13275 .unindent(),
13276 );
13277
13278 // Toggle comments when different languages are active for different
13279 // selections.
13280 cx.set_state(
13281 &r#"
13282 ˇ<script>
13283 ˇvar x = new Y();
13284 ˇ</script>
13285 "#
13286 .unindent(),
13287 );
13288 cx.executor().run_until_parked();
13289 cx.update_editor(|editor, window, cx| {
13290 editor.toggle_comments(&ToggleComments::default(), window, cx)
13291 });
13292 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13293 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13294 cx.assert_editor_state(
13295 &r#"
13296 <!-- ˇ<script> -->
13297 // ˇvar x = new Y();
13298 <!-- ˇ</script> -->
13299 "#
13300 .unindent(),
13301 );
13302}
13303
13304#[gpui::test]
13305fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13306 init_test(cx, |_| {});
13307
13308 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13309 let multibuffer = cx.new(|cx| {
13310 let mut multibuffer = MultiBuffer::new(ReadWrite);
13311 multibuffer.push_excerpts(
13312 buffer.clone(),
13313 [
13314 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13315 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13316 ],
13317 cx,
13318 );
13319 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13320 multibuffer
13321 });
13322
13323 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13324 editor.update_in(cx, |editor, window, cx| {
13325 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13326 editor.change_selections(None, window, cx, |s| {
13327 s.select_ranges([
13328 Point::new(0, 0)..Point::new(0, 0),
13329 Point::new(1, 0)..Point::new(1, 0),
13330 ])
13331 });
13332
13333 editor.handle_input("X", window, cx);
13334 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13335 assert_eq!(
13336 editor.selections.ranges(cx),
13337 [
13338 Point::new(0, 1)..Point::new(0, 1),
13339 Point::new(1, 1)..Point::new(1, 1),
13340 ]
13341 );
13342
13343 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13344 editor.change_selections(None, window, cx, |s| {
13345 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13346 });
13347 editor.backspace(&Default::default(), window, cx);
13348 assert_eq!(editor.text(cx), "Xa\nbbb");
13349 assert_eq!(
13350 editor.selections.ranges(cx),
13351 [Point::new(1, 0)..Point::new(1, 0)]
13352 );
13353
13354 editor.change_selections(None, window, cx, |s| {
13355 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13356 });
13357 editor.backspace(&Default::default(), window, cx);
13358 assert_eq!(editor.text(cx), "X\nbb");
13359 assert_eq!(
13360 editor.selections.ranges(cx),
13361 [Point::new(0, 1)..Point::new(0, 1)]
13362 );
13363 });
13364}
13365
13366#[gpui::test]
13367fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13368 init_test(cx, |_| {});
13369
13370 let markers = vec![('[', ']').into(), ('(', ')').into()];
13371 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13372 indoc! {"
13373 [aaaa
13374 (bbbb]
13375 cccc)",
13376 },
13377 markers.clone(),
13378 );
13379 let excerpt_ranges = markers.into_iter().map(|marker| {
13380 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13381 ExcerptRange::new(context.clone())
13382 });
13383 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13384 let multibuffer = cx.new(|cx| {
13385 let mut multibuffer = MultiBuffer::new(ReadWrite);
13386 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13387 multibuffer
13388 });
13389
13390 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13391 editor.update_in(cx, |editor, window, cx| {
13392 let (expected_text, selection_ranges) = marked_text_ranges(
13393 indoc! {"
13394 aaaa
13395 bˇbbb
13396 bˇbbˇb
13397 cccc"
13398 },
13399 true,
13400 );
13401 assert_eq!(editor.text(cx), expected_text);
13402 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13403
13404 editor.handle_input("X", window, cx);
13405
13406 let (expected_text, expected_selections) = marked_text_ranges(
13407 indoc! {"
13408 aaaa
13409 bXˇbbXb
13410 bXˇbbXˇb
13411 cccc"
13412 },
13413 false,
13414 );
13415 assert_eq!(editor.text(cx), expected_text);
13416 assert_eq!(editor.selections.ranges(cx), expected_selections);
13417
13418 editor.newline(&Newline, window, cx);
13419 let (expected_text, expected_selections) = marked_text_ranges(
13420 indoc! {"
13421 aaaa
13422 bX
13423 ˇbbX
13424 b
13425 bX
13426 ˇbbX
13427 ˇb
13428 cccc"
13429 },
13430 false,
13431 );
13432 assert_eq!(editor.text(cx), expected_text);
13433 assert_eq!(editor.selections.ranges(cx), expected_selections);
13434 });
13435}
13436
13437#[gpui::test]
13438fn test_refresh_selections(cx: &mut TestAppContext) {
13439 init_test(cx, |_| {});
13440
13441 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13442 let mut excerpt1_id = None;
13443 let multibuffer = cx.new(|cx| {
13444 let mut multibuffer = MultiBuffer::new(ReadWrite);
13445 excerpt1_id = multibuffer
13446 .push_excerpts(
13447 buffer.clone(),
13448 [
13449 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13450 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13451 ],
13452 cx,
13453 )
13454 .into_iter()
13455 .next();
13456 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13457 multibuffer
13458 });
13459
13460 let editor = cx.add_window(|window, cx| {
13461 let mut editor = build_editor(multibuffer.clone(), window, cx);
13462 let snapshot = editor.snapshot(window, cx);
13463 editor.change_selections(None, window, cx, |s| {
13464 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13465 });
13466 editor.begin_selection(
13467 Point::new(2, 1).to_display_point(&snapshot),
13468 true,
13469 1,
13470 window,
13471 cx,
13472 );
13473 assert_eq!(
13474 editor.selections.ranges(cx),
13475 [
13476 Point::new(1, 3)..Point::new(1, 3),
13477 Point::new(2, 1)..Point::new(2, 1),
13478 ]
13479 );
13480 editor
13481 });
13482
13483 // Refreshing selections is a no-op when excerpts haven't changed.
13484 _ = editor.update(cx, |editor, window, cx| {
13485 editor.change_selections(None, window, cx, |s| s.refresh());
13486 assert_eq!(
13487 editor.selections.ranges(cx),
13488 [
13489 Point::new(1, 3)..Point::new(1, 3),
13490 Point::new(2, 1)..Point::new(2, 1),
13491 ]
13492 );
13493 });
13494
13495 multibuffer.update(cx, |multibuffer, cx| {
13496 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13497 });
13498 _ = editor.update(cx, |editor, window, cx| {
13499 // Removing an excerpt causes the first selection to become degenerate.
13500 assert_eq!(
13501 editor.selections.ranges(cx),
13502 [
13503 Point::new(0, 0)..Point::new(0, 0),
13504 Point::new(0, 1)..Point::new(0, 1)
13505 ]
13506 );
13507
13508 // Refreshing selections will relocate the first selection to the original buffer
13509 // location.
13510 editor.change_selections(None, window, cx, |s| s.refresh());
13511 assert_eq!(
13512 editor.selections.ranges(cx),
13513 [
13514 Point::new(0, 1)..Point::new(0, 1),
13515 Point::new(0, 3)..Point::new(0, 3)
13516 ]
13517 );
13518 assert!(editor.selections.pending_anchor().is_some());
13519 });
13520}
13521
13522#[gpui::test]
13523fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13524 init_test(cx, |_| {});
13525
13526 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13527 let mut excerpt1_id = None;
13528 let multibuffer = cx.new(|cx| {
13529 let mut multibuffer = MultiBuffer::new(ReadWrite);
13530 excerpt1_id = multibuffer
13531 .push_excerpts(
13532 buffer.clone(),
13533 [
13534 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13535 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13536 ],
13537 cx,
13538 )
13539 .into_iter()
13540 .next();
13541 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13542 multibuffer
13543 });
13544
13545 let editor = cx.add_window(|window, cx| {
13546 let mut editor = build_editor(multibuffer.clone(), window, cx);
13547 let snapshot = editor.snapshot(window, cx);
13548 editor.begin_selection(
13549 Point::new(1, 3).to_display_point(&snapshot),
13550 false,
13551 1,
13552 window,
13553 cx,
13554 );
13555 assert_eq!(
13556 editor.selections.ranges(cx),
13557 [Point::new(1, 3)..Point::new(1, 3)]
13558 );
13559 editor
13560 });
13561
13562 multibuffer.update(cx, |multibuffer, cx| {
13563 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13564 });
13565 _ = editor.update(cx, |editor, window, cx| {
13566 assert_eq!(
13567 editor.selections.ranges(cx),
13568 [Point::new(0, 0)..Point::new(0, 0)]
13569 );
13570
13571 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13572 editor.change_selections(None, window, cx, |s| s.refresh());
13573 assert_eq!(
13574 editor.selections.ranges(cx),
13575 [Point::new(0, 3)..Point::new(0, 3)]
13576 );
13577 assert!(editor.selections.pending_anchor().is_some());
13578 });
13579}
13580
13581#[gpui::test]
13582async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13583 init_test(cx, |_| {});
13584
13585 let language = Arc::new(
13586 Language::new(
13587 LanguageConfig {
13588 brackets: BracketPairConfig {
13589 pairs: vec![
13590 BracketPair {
13591 start: "{".to_string(),
13592 end: "}".to_string(),
13593 close: true,
13594 surround: true,
13595 newline: true,
13596 },
13597 BracketPair {
13598 start: "/* ".to_string(),
13599 end: " */".to_string(),
13600 close: true,
13601 surround: true,
13602 newline: true,
13603 },
13604 ],
13605 ..Default::default()
13606 },
13607 ..Default::default()
13608 },
13609 Some(tree_sitter_rust::LANGUAGE.into()),
13610 )
13611 .with_indents_query("")
13612 .unwrap(),
13613 );
13614
13615 let text = concat!(
13616 "{ }\n", //
13617 " x\n", //
13618 " /* */\n", //
13619 "x\n", //
13620 "{{} }\n", //
13621 );
13622
13623 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13624 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13625 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13626 editor
13627 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13628 .await;
13629
13630 editor.update_in(cx, |editor, window, cx| {
13631 editor.change_selections(None, window, cx, |s| {
13632 s.select_display_ranges([
13633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13634 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13635 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13636 ])
13637 });
13638 editor.newline(&Newline, window, cx);
13639
13640 assert_eq!(
13641 editor.buffer().read(cx).read(cx).text(),
13642 concat!(
13643 "{ \n", // Suppress rustfmt
13644 "\n", //
13645 "}\n", //
13646 " x\n", //
13647 " /* \n", //
13648 " \n", //
13649 " */\n", //
13650 "x\n", //
13651 "{{} \n", //
13652 "}\n", //
13653 )
13654 );
13655 });
13656}
13657
13658#[gpui::test]
13659fn test_highlighted_ranges(cx: &mut TestAppContext) {
13660 init_test(cx, |_| {});
13661
13662 let editor = cx.add_window(|window, cx| {
13663 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13664 build_editor(buffer.clone(), window, cx)
13665 });
13666
13667 _ = editor.update(cx, |editor, window, cx| {
13668 struct Type1;
13669 struct Type2;
13670
13671 let buffer = editor.buffer.read(cx).snapshot(cx);
13672
13673 let anchor_range =
13674 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13675
13676 editor.highlight_background::<Type1>(
13677 &[
13678 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13679 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13680 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13681 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13682 ],
13683 |_| Hsla::red(),
13684 cx,
13685 );
13686 editor.highlight_background::<Type2>(
13687 &[
13688 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13689 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13690 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13691 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13692 ],
13693 |_| Hsla::green(),
13694 cx,
13695 );
13696
13697 let snapshot = editor.snapshot(window, cx);
13698 let mut highlighted_ranges = editor.background_highlights_in_range(
13699 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13700 &snapshot,
13701 cx.theme(),
13702 );
13703 // Enforce a consistent ordering based on color without relying on the ordering of the
13704 // highlight's `TypeId` which is non-executor.
13705 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13706 assert_eq!(
13707 highlighted_ranges,
13708 &[
13709 (
13710 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13711 Hsla::red(),
13712 ),
13713 (
13714 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13715 Hsla::red(),
13716 ),
13717 (
13718 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13719 Hsla::green(),
13720 ),
13721 (
13722 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13723 Hsla::green(),
13724 ),
13725 ]
13726 );
13727 assert_eq!(
13728 editor.background_highlights_in_range(
13729 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13730 &snapshot,
13731 cx.theme(),
13732 ),
13733 &[(
13734 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13735 Hsla::red(),
13736 )]
13737 );
13738 });
13739}
13740
13741#[gpui::test]
13742async fn test_following(cx: &mut TestAppContext) {
13743 init_test(cx, |_| {});
13744
13745 let fs = FakeFs::new(cx.executor());
13746 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13747
13748 let buffer = project.update(cx, |project, cx| {
13749 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13750 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13751 });
13752 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13753 let follower = cx.update(|cx| {
13754 cx.open_window(
13755 WindowOptions {
13756 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13757 gpui::Point::new(px(0.), px(0.)),
13758 gpui::Point::new(px(10.), px(80.)),
13759 ))),
13760 ..Default::default()
13761 },
13762 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13763 )
13764 .unwrap()
13765 });
13766
13767 let is_still_following = Rc::new(RefCell::new(true));
13768 let follower_edit_event_count = Rc::new(RefCell::new(0));
13769 let pending_update = Rc::new(RefCell::new(None));
13770 let leader_entity = leader.root(cx).unwrap();
13771 let follower_entity = follower.root(cx).unwrap();
13772 _ = follower.update(cx, {
13773 let update = pending_update.clone();
13774 let is_still_following = is_still_following.clone();
13775 let follower_edit_event_count = follower_edit_event_count.clone();
13776 |_, window, cx| {
13777 cx.subscribe_in(
13778 &leader_entity,
13779 window,
13780 move |_, leader, event, window, cx| {
13781 leader.read(cx).add_event_to_update_proto(
13782 event,
13783 &mut update.borrow_mut(),
13784 window,
13785 cx,
13786 );
13787 },
13788 )
13789 .detach();
13790
13791 cx.subscribe_in(
13792 &follower_entity,
13793 window,
13794 move |_, _, event: &EditorEvent, _window, _cx| {
13795 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13796 *is_still_following.borrow_mut() = false;
13797 }
13798
13799 if let EditorEvent::BufferEdited = event {
13800 *follower_edit_event_count.borrow_mut() += 1;
13801 }
13802 },
13803 )
13804 .detach();
13805 }
13806 });
13807
13808 // Update the selections only
13809 _ = leader.update(cx, |leader, window, cx| {
13810 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13811 });
13812 follower
13813 .update(cx, |follower, window, cx| {
13814 follower.apply_update_proto(
13815 &project,
13816 pending_update.borrow_mut().take().unwrap(),
13817 window,
13818 cx,
13819 )
13820 })
13821 .unwrap()
13822 .await
13823 .unwrap();
13824 _ = follower.update(cx, |follower, _, cx| {
13825 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13826 });
13827 assert!(*is_still_following.borrow());
13828 assert_eq!(*follower_edit_event_count.borrow(), 0);
13829
13830 // Update the scroll position only
13831 _ = leader.update(cx, |leader, window, cx| {
13832 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13833 });
13834 follower
13835 .update(cx, |follower, window, cx| {
13836 follower.apply_update_proto(
13837 &project,
13838 pending_update.borrow_mut().take().unwrap(),
13839 window,
13840 cx,
13841 )
13842 })
13843 .unwrap()
13844 .await
13845 .unwrap();
13846 assert_eq!(
13847 follower
13848 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13849 .unwrap(),
13850 gpui::Point::new(1.5, 3.5)
13851 );
13852 assert!(*is_still_following.borrow());
13853 assert_eq!(*follower_edit_event_count.borrow(), 0);
13854
13855 // Update the selections and scroll position. The follower's scroll position is updated
13856 // via autoscroll, not via the leader's exact scroll position.
13857 _ = leader.update(cx, |leader, window, cx| {
13858 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13859 leader.request_autoscroll(Autoscroll::newest(), cx);
13860 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13861 });
13862 follower
13863 .update(cx, |follower, window, cx| {
13864 follower.apply_update_proto(
13865 &project,
13866 pending_update.borrow_mut().take().unwrap(),
13867 window,
13868 cx,
13869 )
13870 })
13871 .unwrap()
13872 .await
13873 .unwrap();
13874 _ = follower.update(cx, |follower, _, cx| {
13875 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13876 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13877 });
13878 assert!(*is_still_following.borrow());
13879
13880 // Creating a pending selection that precedes another selection
13881 _ = leader.update(cx, |leader, window, cx| {
13882 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13883 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13884 });
13885 follower
13886 .update(cx, |follower, window, cx| {
13887 follower.apply_update_proto(
13888 &project,
13889 pending_update.borrow_mut().take().unwrap(),
13890 window,
13891 cx,
13892 )
13893 })
13894 .unwrap()
13895 .await
13896 .unwrap();
13897 _ = follower.update(cx, |follower, _, cx| {
13898 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13899 });
13900 assert!(*is_still_following.borrow());
13901
13902 // Extend the pending selection so that it surrounds another selection
13903 _ = leader.update(cx, |leader, window, cx| {
13904 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13905 });
13906 follower
13907 .update(cx, |follower, window, cx| {
13908 follower.apply_update_proto(
13909 &project,
13910 pending_update.borrow_mut().take().unwrap(),
13911 window,
13912 cx,
13913 )
13914 })
13915 .unwrap()
13916 .await
13917 .unwrap();
13918 _ = follower.update(cx, |follower, _, cx| {
13919 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13920 });
13921
13922 // Scrolling locally breaks the follow
13923 _ = follower.update(cx, |follower, window, cx| {
13924 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13925 follower.set_scroll_anchor(
13926 ScrollAnchor {
13927 anchor: top_anchor,
13928 offset: gpui::Point::new(0.0, 0.5),
13929 },
13930 window,
13931 cx,
13932 );
13933 });
13934 assert!(!(*is_still_following.borrow()));
13935}
13936
13937#[gpui::test]
13938async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13939 init_test(cx, |_| {});
13940
13941 let fs = FakeFs::new(cx.executor());
13942 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13943 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13944 let pane = workspace
13945 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13946 .unwrap();
13947
13948 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13949
13950 let leader = pane.update_in(cx, |_, window, cx| {
13951 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13952 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13953 });
13954
13955 // Start following the editor when it has no excerpts.
13956 let mut state_message =
13957 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13958 let workspace_entity = workspace.root(cx).unwrap();
13959 let follower_1 = cx
13960 .update_window(*workspace.deref(), |_, window, cx| {
13961 Editor::from_state_proto(
13962 workspace_entity,
13963 ViewId {
13964 creator: CollaboratorId::PeerId(PeerId::default()),
13965 id: 0,
13966 },
13967 &mut state_message,
13968 window,
13969 cx,
13970 )
13971 })
13972 .unwrap()
13973 .unwrap()
13974 .await
13975 .unwrap();
13976
13977 let update_message = Rc::new(RefCell::new(None));
13978 follower_1.update_in(cx, {
13979 let update = update_message.clone();
13980 |_, window, cx| {
13981 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13982 leader.read(cx).add_event_to_update_proto(
13983 event,
13984 &mut update.borrow_mut(),
13985 window,
13986 cx,
13987 );
13988 })
13989 .detach();
13990 }
13991 });
13992
13993 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13994 (
13995 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13996 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13997 )
13998 });
13999
14000 // Insert some excerpts.
14001 leader.update(cx, |leader, cx| {
14002 leader.buffer.update(cx, |multibuffer, cx| {
14003 multibuffer.set_excerpts_for_path(
14004 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14005 buffer_1.clone(),
14006 vec![
14007 Point::row_range(0..3),
14008 Point::row_range(1..6),
14009 Point::row_range(12..15),
14010 ],
14011 0,
14012 cx,
14013 );
14014 multibuffer.set_excerpts_for_path(
14015 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14016 buffer_2.clone(),
14017 vec![Point::row_range(0..6), Point::row_range(8..12)],
14018 0,
14019 cx,
14020 );
14021 });
14022 });
14023
14024 // Apply the update of adding the excerpts.
14025 follower_1
14026 .update_in(cx, |follower, window, cx| {
14027 follower.apply_update_proto(
14028 &project,
14029 update_message.borrow().clone().unwrap(),
14030 window,
14031 cx,
14032 )
14033 })
14034 .await
14035 .unwrap();
14036 assert_eq!(
14037 follower_1.update(cx, |editor, cx| editor.text(cx)),
14038 leader.update(cx, |editor, cx| editor.text(cx))
14039 );
14040 update_message.borrow_mut().take();
14041
14042 // Start following separately after it already has excerpts.
14043 let mut state_message =
14044 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14045 let workspace_entity = workspace.root(cx).unwrap();
14046 let follower_2 = cx
14047 .update_window(*workspace.deref(), |_, window, cx| {
14048 Editor::from_state_proto(
14049 workspace_entity,
14050 ViewId {
14051 creator: CollaboratorId::PeerId(PeerId::default()),
14052 id: 0,
14053 },
14054 &mut state_message,
14055 window,
14056 cx,
14057 )
14058 })
14059 .unwrap()
14060 .unwrap()
14061 .await
14062 .unwrap();
14063 assert_eq!(
14064 follower_2.update(cx, |editor, cx| editor.text(cx)),
14065 leader.update(cx, |editor, cx| editor.text(cx))
14066 );
14067
14068 // Remove some excerpts.
14069 leader.update(cx, |leader, cx| {
14070 leader.buffer.update(cx, |multibuffer, cx| {
14071 let excerpt_ids = multibuffer.excerpt_ids();
14072 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14073 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14074 });
14075 });
14076
14077 // Apply the update of removing the excerpts.
14078 follower_1
14079 .update_in(cx, |follower, window, cx| {
14080 follower.apply_update_proto(
14081 &project,
14082 update_message.borrow().clone().unwrap(),
14083 window,
14084 cx,
14085 )
14086 })
14087 .await
14088 .unwrap();
14089 follower_2
14090 .update_in(cx, |follower, window, cx| {
14091 follower.apply_update_proto(
14092 &project,
14093 update_message.borrow().clone().unwrap(),
14094 window,
14095 cx,
14096 )
14097 })
14098 .await
14099 .unwrap();
14100 update_message.borrow_mut().take();
14101 assert_eq!(
14102 follower_1.update(cx, |editor, cx| editor.text(cx)),
14103 leader.update(cx, |editor, cx| editor.text(cx))
14104 );
14105}
14106
14107#[gpui::test]
14108async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14109 init_test(cx, |_| {});
14110
14111 let mut cx = EditorTestContext::new(cx).await;
14112 let lsp_store =
14113 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14114
14115 cx.set_state(indoc! {"
14116 ˇfn func(abc def: i32) -> u32 {
14117 }
14118 "});
14119
14120 cx.update(|_, cx| {
14121 lsp_store.update(cx, |lsp_store, cx| {
14122 lsp_store
14123 .update_diagnostics(
14124 LanguageServerId(0),
14125 lsp::PublishDiagnosticsParams {
14126 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14127 version: None,
14128 diagnostics: vec![
14129 lsp::Diagnostic {
14130 range: lsp::Range::new(
14131 lsp::Position::new(0, 11),
14132 lsp::Position::new(0, 12),
14133 ),
14134 severity: Some(lsp::DiagnosticSeverity::ERROR),
14135 ..Default::default()
14136 },
14137 lsp::Diagnostic {
14138 range: lsp::Range::new(
14139 lsp::Position::new(0, 12),
14140 lsp::Position::new(0, 15),
14141 ),
14142 severity: Some(lsp::DiagnosticSeverity::ERROR),
14143 ..Default::default()
14144 },
14145 lsp::Diagnostic {
14146 range: lsp::Range::new(
14147 lsp::Position::new(0, 25),
14148 lsp::Position::new(0, 28),
14149 ),
14150 severity: Some(lsp::DiagnosticSeverity::ERROR),
14151 ..Default::default()
14152 },
14153 ],
14154 },
14155 None,
14156 DiagnosticSourceKind::Pushed,
14157 &[],
14158 cx,
14159 )
14160 .unwrap()
14161 });
14162 });
14163
14164 executor.run_until_parked();
14165
14166 cx.update_editor(|editor, window, cx| {
14167 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14168 });
14169
14170 cx.assert_editor_state(indoc! {"
14171 fn func(abc def: i32) -> ˇu32 {
14172 }
14173 "});
14174
14175 cx.update_editor(|editor, window, cx| {
14176 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14177 });
14178
14179 cx.assert_editor_state(indoc! {"
14180 fn func(abc ˇdef: i32) -> u32 {
14181 }
14182 "});
14183
14184 cx.update_editor(|editor, window, cx| {
14185 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14186 });
14187
14188 cx.assert_editor_state(indoc! {"
14189 fn func(abcˇ def: i32) -> u32 {
14190 }
14191 "});
14192
14193 cx.update_editor(|editor, window, cx| {
14194 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14195 });
14196
14197 cx.assert_editor_state(indoc! {"
14198 fn func(abc def: i32) -> ˇu32 {
14199 }
14200 "});
14201}
14202
14203#[gpui::test]
14204async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14205 init_test(cx, |_| {});
14206
14207 let mut cx = EditorTestContext::new(cx).await;
14208
14209 let diff_base = r#"
14210 use some::mod;
14211
14212 const A: u32 = 42;
14213
14214 fn main() {
14215 println!("hello");
14216
14217 println!("world");
14218 }
14219 "#
14220 .unindent();
14221
14222 // Edits are modified, removed, modified, added
14223 cx.set_state(
14224 &r#"
14225 use some::modified;
14226
14227 ˇ
14228 fn main() {
14229 println!("hello there");
14230
14231 println!("around the");
14232 println!("world");
14233 }
14234 "#
14235 .unindent(),
14236 );
14237
14238 cx.set_head_text(&diff_base);
14239 executor.run_until_parked();
14240
14241 cx.update_editor(|editor, window, cx| {
14242 //Wrap around the bottom of the buffer
14243 for _ in 0..3 {
14244 editor.go_to_next_hunk(&GoToHunk, window, cx);
14245 }
14246 });
14247
14248 cx.assert_editor_state(
14249 &r#"
14250 ˇuse some::modified;
14251
14252
14253 fn main() {
14254 println!("hello there");
14255
14256 println!("around the");
14257 println!("world");
14258 }
14259 "#
14260 .unindent(),
14261 );
14262
14263 cx.update_editor(|editor, window, cx| {
14264 //Wrap around the top of the buffer
14265 for _ in 0..2 {
14266 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14267 }
14268 });
14269
14270 cx.assert_editor_state(
14271 &r#"
14272 use some::modified;
14273
14274
14275 fn main() {
14276 ˇ println!("hello there");
14277
14278 println!("around the");
14279 println!("world");
14280 }
14281 "#
14282 .unindent(),
14283 );
14284
14285 cx.update_editor(|editor, window, cx| {
14286 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14287 });
14288
14289 cx.assert_editor_state(
14290 &r#"
14291 use some::modified;
14292
14293 ˇ
14294 fn main() {
14295 println!("hello there");
14296
14297 println!("around the");
14298 println!("world");
14299 }
14300 "#
14301 .unindent(),
14302 );
14303
14304 cx.update_editor(|editor, window, cx| {
14305 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14306 });
14307
14308 cx.assert_editor_state(
14309 &r#"
14310 ˇuse some::modified;
14311
14312
14313 fn main() {
14314 println!("hello there");
14315
14316 println!("around the");
14317 println!("world");
14318 }
14319 "#
14320 .unindent(),
14321 );
14322
14323 cx.update_editor(|editor, window, cx| {
14324 for _ in 0..2 {
14325 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14326 }
14327 });
14328
14329 cx.assert_editor_state(
14330 &r#"
14331 use some::modified;
14332
14333
14334 fn main() {
14335 ˇ println!("hello there");
14336
14337 println!("around the");
14338 println!("world");
14339 }
14340 "#
14341 .unindent(),
14342 );
14343
14344 cx.update_editor(|editor, window, cx| {
14345 editor.fold(&Fold, window, cx);
14346 });
14347
14348 cx.update_editor(|editor, window, cx| {
14349 editor.go_to_next_hunk(&GoToHunk, window, cx);
14350 });
14351
14352 cx.assert_editor_state(
14353 &r#"
14354 ˇuse some::modified;
14355
14356
14357 fn main() {
14358 println!("hello there");
14359
14360 println!("around the");
14361 println!("world");
14362 }
14363 "#
14364 .unindent(),
14365 );
14366}
14367
14368#[test]
14369fn test_split_words() {
14370 fn split(text: &str) -> Vec<&str> {
14371 split_words(text).collect()
14372 }
14373
14374 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14375 assert_eq!(split("hello_world"), &["hello_", "world"]);
14376 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14377 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14378 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14379 assert_eq!(split("helloworld"), &["helloworld"]);
14380
14381 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14382}
14383
14384#[gpui::test]
14385async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14386 init_test(cx, |_| {});
14387
14388 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14389 let mut assert = |before, after| {
14390 let _state_context = cx.set_state(before);
14391 cx.run_until_parked();
14392 cx.update_editor(|editor, window, cx| {
14393 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14394 });
14395 cx.run_until_parked();
14396 cx.assert_editor_state(after);
14397 };
14398
14399 // Outside bracket jumps to outside of matching bracket
14400 assert("console.logˇ(var);", "console.log(var)ˇ;");
14401 assert("console.log(var)ˇ;", "console.logˇ(var);");
14402
14403 // Inside bracket jumps to inside of matching bracket
14404 assert("console.log(ˇvar);", "console.log(varˇ);");
14405 assert("console.log(varˇ);", "console.log(ˇvar);");
14406
14407 // When outside a bracket and inside, favor jumping to the inside bracket
14408 assert(
14409 "console.log('foo', [1, 2, 3]ˇ);",
14410 "console.log(ˇ'foo', [1, 2, 3]);",
14411 );
14412 assert(
14413 "console.log(ˇ'foo', [1, 2, 3]);",
14414 "console.log('foo', [1, 2, 3]ˇ);",
14415 );
14416
14417 // Bias forward if two options are equally likely
14418 assert(
14419 "let result = curried_fun()ˇ();",
14420 "let result = curried_fun()()ˇ;",
14421 );
14422
14423 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14424 assert(
14425 indoc! {"
14426 function test() {
14427 console.log('test')ˇ
14428 }"},
14429 indoc! {"
14430 function test() {
14431 console.logˇ('test')
14432 }"},
14433 );
14434}
14435
14436#[gpui::test]
14437async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14438 init_test(cx, |_| {});
14439
14440 let fs = FakeFs::new(cx.executor());
14441 fs.insert_tree(
14442 path!("/a"),
14443 json!({
14444 "main.rs": "fn main() { let a = 5; }",
14445 "other.rs": "// Test file",
14446 }),
14447 )
14448 .await;
14449 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14450
14451 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14452 language_registry.add(Arc::new(Language::new(
14453 LanguageConfig {
14454 name: "Rust".into(),
14455 matcher: LanguageMatcher {
14456 path_suffixes: vec!["rs".to_string()],
14457 ..Default::default()
14458 },
14459 brackets: BracketPairConfig {
14460 pairs: vec![BracketPair {
14461 start: "{".to_string(),
14462 end: "}".to_string(),
14463 close: true,
14464 surround: true,
14465 newline: true,
14466 }],
14467 disabled_scopes_by_bracket_ix: Vec::new(),
14468 },
14469 ..Default::default()
14470 },
14471 Some(tree_sitter_rust::LANGUAGE.into()),
14472 )));
14473 let mut fake_servers = language_registry.register_fake_lsp(
14474 "Rust",
14475 FakeLspAdapter {
14476 capabilities: lsp::ServerCapabilities {
14477 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14478 first_trigger_character: "{".to_string(),
14479 more_trigger_character: None,
14480 }),
14481 ..Default::default()
14482 },
14483 ..Default::default()
14484 },
14485 );
14486
14487 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14488
14489 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14490
14491 let worktree_id = workspace
14492 .update(cx, |workspace, _, cx| {
14493 workspace.project().update(cx, |project, cx| {
14494 project.worktrees(cx).next().unwrap().read(cx).id()
14495 })
14496 })
14497 .unwrap();
14498
14499 let buffer = project
14500 .update(cx, |project, cx| {
14501 project.open_local_buffer(path!("/a/main.rs"), cx)
14502 })
14503 .await
14504 .unwrap();
14505 let editor_handle = workspace
14506 .update(cx, |workspace, window, cx| {
14507 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14508 })
14509 .unwrap()
14510 .await
14511 .unwrap()
14512 .downcast::<Editor>()
14513 .unwrap();
14514
14515 cx.executor().start_waiting();
14516 let fake_server = fake_servers.next().await.unwrap();
14517
14518 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14519 |params, _| async move {
14520 assert_eq!(
14521 params.text_document_position.text_document.uri,
14522 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14523 );
14524 assert_eq!(
14525 params.text_document_position.position,
14526 lsp::Position::new(0, 21),
14527 );
14528
14529 Ok(Some(vec![lsp::TextEdit {
14530 new_text: "]".to_string(),
14531 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14532 }]))
14533 },
14534 );
14535
14536 editor_handle.update_in(cx, |editor, window, cx| {
14537 window.focus(&editor.focus_handle(cx));
14538 editor.change_selections(None, window, cx, |s| {
14539 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14540 });
14541 editor.handle_input("{", window, cx);
14542 });
14543
14544 cx.executor().run_until_parked();
14545
14546 buffer.update(cx, |buffer, _| {
14547 assert_eq!(
14548 buffer.text(),
14549 "fn main() { let a = {5}; }",
14550 "No extra braces from on type formatting should appear in the buffer"
14551 )
14552 });
14553}
14554
14555#[gpui::test]
14556async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14557 init_test(cx, |_| {});
14558
14559 let fs = FakeFs::new(cx.executor());
14560 fs.insert_tree(
14561 path!("/a"),
14562 json!({
14563 "main.rs": "fn main() { let a = 5; }",
14564 "other.rs": "// Test file",
14565 }),
14566 )
14567 .await;
14568
14569 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14570
14571 let server_restarts = Arc::new(AtomicUsize::new(0));
14572 let closure_restarts = Arc::clone(&server_restarts);
14573 let language_server_name = "test language server";
14574 let language_name: LanguageName = "Rust".into();
14575
14576 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14577 language_registry.add(Arc::new(Language::new(
14578 LanguageConfig {
14579 name: language_name.clone(),
14580 matcher: LanguageMatcher {
14581 path_suffixes: vec!["rs".to_string()],
14582 ..Default::default()
14583 },
14584 ..Default::default()
14585 },
14586 Some(tree_sitter_rust::LANGUAGE.into()),
14587 )));
14588 let mut fake_servers = language_registry.register_fake_lsp(
14589 "Rust",
14590 FakeLspAdapter {
14591 name: language_server_name,
14592 initialization_options: Some(json!({
14593 "testOptionValue": true
14594 })),
14595 initializer: Some(Box::new(move |fake_server| {
14596 let task_restarts = Arc::clone(&closure_restarts);
14597 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14598 task_restarts.fetch_add(1, atomic::Ordering::Release);
14599 futures::future::ready(Ok(()))
14600 });
14601 })),
14602 ..Default::default()
14603 },
14604 );
14605
14606 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14607 let _buffer = project
14608 .update(cx, |project, cx| {
14609 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14610 })
14611 .await
14612 .unwrap();
14613 let _fake_server = fake_servers.next().await.unwrap();
14614 update_test_language_settings(cx, |language_settings| {
14615 language_settings.languages.insert(
14616 language_name.clone(),
14617 LanguageSettingsContent {
14618 tab_size: NonZeroU32::new(8),
14619 ..Default::default()
14620 },
14621 );
14622 });
14623 cx.executor().run_until_parked();
14624 assert_eq!(
14625 server_restarts.load(atomic::Ordering::Acquire),
14626 0,
14627 "Should not restart LSP server on an unrelated change"
14628 );
14629
14630 update_test_project_settings(cx, |project_settings| {
14631 project_settings.lsp.insert(
14632 "Some other server name".into(),
14633 LspSettings {
14634 binary: None,
14635 settings: None,
14636 initialization_options: Some(json!({
14637 "some other init value": false
14638 })),
14639 enable_lsp_tasks: false,
14640 },
14641 );
14642 });
14643 cx.executor().run_until_parked();
14644 assert_eq!(
14645 server_restarts.load(atomic::Ordering::Acquire),
14646 0,
14647 "Should not restart LSP server on an unrelated LSP settings change"
14648 );
14649
14650 update_test_project_settings(cx, |project_settings| {
14651 project_settings.lsp.insert(
14652 language_server_name.into(),
14653 LspSettings {
14654 binary: None,
14655 settings: None,
14656 initialization_options: Some(json!({
14657 "anotherInitValue": false
14658 })),
14659 enable_lsp_tasks: false,
14660 },
14661 );
14662 });
14663 cx.executor().run_until_parked();
14664 assert_eq!(
14665 server_restarts.load(atomic::Ordering::Acquire),
14666 1,
14667 "Should restart LSP server on a related LSP settings change"
14668 );
14669
14670 update_test_project_settings(cx, |project_settings| {
14671 project_settings.lsp.insert(
14672 language_server_name.into(),
14673 LspSettings {
14674 binary: None,
14675 settings: None,
14676 initialization_options: Some(json!({
14677 "anotherInitValue": false
14678 })),
14679 enable_lsp_tasks: false,
14680 },
14681 );
14682 });
14683 cx.executor().run_until_parked();
14684 assert_eq!(
14685 server_restarts.load(atomic::Ordering::Acquire),
14686 1,
14687 "Should not restart LSP server on a related LSP settings change that is the same"
14688 );
14689
14690 update_test_project_settings(cx, |project_settings| {
14691 project_settings.lsp.insert(
14692 language_server_name.into(),
14693 LspSettings {
14694 binary: None,
14695 settings: None,
14696 initialization_options: None,
14697 enable_lsp_tasks: false,
14698 },
14699 );
14700 });
14701 cx.executor().run_until_parked();
14702 assert_eq!(
14703 server_restarts.load(atomic::Ordering::Acquire),
14704 2,
14705 "Should restart LSP server on another related LSP settings change"
14706 );
14707}
14708
14709#[gpui::test]
14710async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14711 init_test(cx, |_| {});
14712
14713 let mut cx = EditorLspTestContext::new_rust(
14714 lsp::ServerCapabilities {
14715 completion_provider: Some(lsp::CompletionOptions {
14716 trigger_characters: Some(vec![".".to_string()]),
14717 resolve_provider: Some(true),
14718 ..Default::default()
14719 }),
14720 ..Default::default()
14721 },
14722 cx,
14723 )
14724 .await;
14725
14726 cx.set_state("fn main() { let a = 2ˇ; }");
14727 cx.simulate_keystroke(".");
14728 let completion_item = lsp::CompletionItem {
14729 label: "some".into(),
14730 kind: Some(lsp::CompletionItemKind::SNIPPET),
14731 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14732 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14733 kind: lsp::MarkupKind::Markdown,
14734 value: "```rust\nSome(2)\n```".to_string(),
14735 })),
14736 deprecated: Some(false),
14737 sort_text: Some("fffffff2".to_string()),
14738 filter_text: Some("some".to_string()),
14739 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14740 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14741 range: lsp::Range {
14742 start: lsp::Position {
14743 line: 0,
14744 character: 22,
14745 },
14746 end: lsp::Position {
14747 line: 0,
14748 character: 22,
14749 },
14750 },
14751 new_text: "Some(2)".to_string(),
14752 })),
14753 additional_text_edits: Some(vec![lsp::TextEdit {
14754 range: lsp::Range {
14755 start: lsp::Position {
14756 line: 0,
14757 character: 20,
14758 },
14759 end: lsp::Position {
14760 line: 0,
14761 character: 22,
14762 },
14763 },
14764 new_text: "".to_string(),
14765 }]),
14766 ..Default::default()
14767 };
14768
14769 let closure_completion_item = completion_item.clone();
14770 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14771 let task_completion_item = closure_completion_item.clone();
14772 async move {
14773 Ok(Some(lsp::CompletionResponse::Array(vec![
14774 task_completion_item,
14775 ])))
14776 }
14777 });
14778
14779 request.next().await;
14780
14781 cx.condition(|editor, _| editor.context_menu_visible())
14782 .await;
14783 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14784 editor
14785 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14786 .unwrap()
14787 });
14788 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14789
14790 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14791 let task_completion_item = completion_item.clone();
14792 async move { Ok(task_completion_item) }
14793 })
14794 .next()
14795 .await
14796 .unwrap();
14797 apply_additional_edits.await.unwrap();
14798 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14799}
14800
14801#[gpui::test]
14802async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14803 init_test(cx, |_| {});
14804
14805 let mut cx = EditorLspTestContext::new_rust(
14806 lsp::ServerCapabilities {
14807 completion_provider: Some(lsp::CompletionOptions {
14808 trigger_characters: Some(vec![".".to_string()]),
14809 resolve_provider: Some(true),
14810 ..Default::default()
14811 }),
14812 ..Default::default()
14813 },
14814 cx,
14815 )
14816 .await;
14817
14818 cx.set_state("fn main() { let a = 2ˇ; }");
14819 cx.simulate_keystroke(".");
14820
14821 let item1 = lsp::CompletionItem {
14822 label: "method id()".to_string(),
14823 filter_text: Some("id".to_string()),
14824 detail: None,
14825 documentation: None,
14826 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14827 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14828 new_text: ".id".to_string(),
14829 })),
14830 ..lsp::CompletionItem::default()
14831 };
14832
14833 let item2 = lsp::CompletionItem {
14834 label: "other".to_string(),
14835 filter_text: Some("other".to_string()),
14836 detail: None,
14837 documentation: None,
14838 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14839 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14840 new_text: ".other".to_string(),
14841 })),
14842 ..lsp::CompletionItem::default()
14843 };
14844
14845 let item1 = item1.clone();
14846 cx.set_request_handler::<lsp::request::Completion, _, _>({
14847 let item1 = item1.clone();
14848 move |_, _, _| {
14849 let item1 = item1.clone();
14850 let item2 = item2.clone();
14851 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14852 }
14853 })
14854 .next()
14855 .await;
14856
14857 cx.condition(|editor, _| editor.context_menu_visible())
14858 .await;
14859 cx.update_editor(|editor, _, _| {
14860 let context_menu = editor.context_menu.borrow_mut();
14861 let context_menu = context_menu
14862 .as_ref()
14863 .expect("Should have the context menu deployed");
14864 match context_menu {
14865 CodeContextMenu::Completions(completions_menu) => {
14866 let completions = completions_menu.completions.borrow_mut();
14867 assert_eq!(
14868 completions
14869 .iter()
14870 .map(|completion| &completion.label.text)
14871 .collect::<Vec<_>>(),
14872 vec!["method id()", "other"]
14873 )
14874 }
14875 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14876 }
14877 });
14878
14879 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14880 let item1 = item1.clone();
14881 move |_, item_to_resolve, _| {
14882 let item1 = item1.clone();
14883 async move {
14884 if item1 == item_to_resolve {
14885 Ok(lsp::CompletionItem {
14886 label: "method id()".to_string(),
14887 filter_text: Some("id".to_string()),
14888 detail: Some("Now resolved!".to_string()),
14889 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14890 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14891 range: lsp::Range::new(
14892 lsp::Position::new(0, 22),
14893 lsp::Position::new(0, 22),
14894 ),
14895 new_text: ".id".to_string(),
14896 })),
14897 ..lsp::CompletionItem::default()
14898 })
14899 } else {
14900 Ok(item_to_resolve)
14901 }
14902 }
14903 }
14904 })
14905 .next()
14906 .await
14907 .unwrap();
14908 cx.run_until_parked();
14909
14910 cx.update_editor(|editor, window, cx| {
14911 editor.context_menu_next(&Default::default(), window, cx);
14912 });
14913
14914 cx.update_editor(|editor, _, _| {
14915 let context_menu = editor.context_menu.borrow_mut();
14916 let context_menu = context_menu
14917 .as_ref()
14918 .expect("Should have the context menu deployed");
14919 match context_menu {
14920 CodeContextMenu::Completions(completions_menu) => {
14921 let completions = completions_menu.completions.borrow_mut();
14922 assert_eq!(
14923 completions
14924 .iter()
14925 .map(|completion| &completion.label.text)
14926 .collect::<Vec<_>>(),
14927 vec!["method id() Now resolved!", "other"],
14928 "Should update first completion label, but not second as the filter text did not match."
14929 );
14930 }
14931 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14932 }
14933 });
14934}
14935
14936#[gpui::test]
14937async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14938 init_test(cx, |_| {});
14939 let mut cx = EditorLspTestContext::new_rust(
14940 lsp::ServerCapabilities {
14941 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14942 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14943 completion_provider: Some(lsp::CompletionOptions {
14944 resolve_provider: Some(true),
14945 ..Default::default()
14946 }),
14947 ..Default::default()
14948 },
14949 cx,
14950 )
14951 .await;
14952 cx.set_state(indoc! {"
14953 struct TestStruct {
14954 field: i32
14955 }
14956
14957 fn mainˇ() {
14958 let unused_var = 42;
14959 let test_struct = TestStruct { field: 42 };
14960 }
14961 "});
14962 let symbol_range = cx.lsp_range(indoc! {"
14963 struct TestStruct {
14964 field: i32
14965 }
14966
14967 «fn main»() {
14968 let unused_var = 42;
14969 let test_struct = TestStruct { field: 42 };
14970 }
14971 "});
14972 let mut hover_requests =
14973 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14974 Ok(Some(lsp::Hover {
14975 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14976 kind: lsp::MarkupKind::Markdown,
14977 value: "Function documentation".to_string(),
14978 }),
14979 range: Some(symbol_range),
14980 }))
14981 });
14982
14983 // Case 1: Test that code action menu hide hover popover
14984 cx.dispatch_action(Hover);
14985 hover_requests.next().await;
14986 cx.condition(|editor, _| editor.hover_state.visible()).await;
14987 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14988 move |_, _, _| async move {
14989 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14990 lsp::CodeAction {
14991 title: "Remove unused variable".to_string(),
14992 kind: Some(CodeActionKind::QUICKFIX),
14993 edit: Some(lsp::WorkspaceEdit {
14994 changes: Some(
14995 [(
14996 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14997 vec![lsp::TextEdit {
14998 range: lsp::Range::new(
14999 lsp::Position::new(5, 4),
15000 lsp::Position::new(5, 27),
15001 ),
15002 new_text: "".to_string(),
15003 }],
15004 )]
15005 .into_iter()
15006 .collect(),
15007 ),
15008 ..Default::default()
15009 }),
15010 ..Default::default()
15011 },
15012 )]))
15013 },
15014 );
15015 cx.update_editor(|editor, window, cx| {
15016 editor.toggle_code_actions(
15017 &ToggleCodeActions {
15018 deployed_from: None,
15019 quick_launch: false,
15020 },
15021 window,
15022 cx,
15023 );
15024 });
15025 code_action_requests.next().await;
15026 cx.run_until_parked();
15027 cx.condition(|editor, _| editor.context_menu_visible())
15028 .await;
15029 cx.update_editor(|editor, _, _| {
15030 assert!(
15031 !editor.hover_state.visible(),
15032 "Hover popover should be hidden when code action menu is shown"
15033 );
15034 // Hide code actions
15035 editor.context_menu.take();
15036 });
15037
15038 // Case 2: Test that code completions hide hover popover
15039 cx.dispatch_action(Hover);
15040 hover_requests.next().await;
15041 cx.condition(|editor, _| editor.hover_state.visible()).await;
15042 let counter = Arc::new(AtomicUsize::new(0));
15043 let mut completion_requests =
15044 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15045 let counter = counter.clone();
15046 async move {
15047 counter.fetch_add(1, atomic::Ordering::Release);
15048 Ok(Some(lsp::CompletionResponse::Array(vec![
15049 lsp::CompletionItem {
15050 label: "main".into(),
15051 kind: Some(lsp::CompletionItemKind::FUNCTION),
15052 detail: Some("() -> ()".to_string()),
15053 ..Default::default()
15054 },
15055 lsp::CompletionItem {
15056 label: "TestStruct".into(),
15057 kind: Some(lsp::CompletionItemKind::STRUCT),
15058 detail: Some("struct TestStruct".to_string()),
15059 ..Default::default()
15060 },
15061 ])))
15062 }
15063 });
15064 cx.update_editor(|editor, window, cx| {
15065 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15066 });
15067 completion_requests.next().await;
15068 cx.condition(|editor, _| editor.context_menu_visible())
15069 .await;
15070 cx.update_editor(|editor, _, _| {
15071 assert!(
15072 !editor.hover_state.visible(),
15073 "Hover popover should be hidden when completion menu is shown"
15074 );
15075 });
15076}
15077
15078#[gpui::test]
15079async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15080 init_test(cx, |_| {});
15081
15082 let mut cx = EditorLspTestContext::new_rust(
15083 lsp::ServerCapabilities {
15084 completion_provider: Some(lsp::CompletionOptions {
15085 trigger_characters: Some(vec![".".to_string()]),
15086 resolve_provider: Some(true),
15087 ..Default::default()
15088 }),
15089 ..Default::default()
15090 },
15091 cx,
15092 )
15093 .await;
15094
15095 cx.set_state("fn main() { let a = 2ˇ; }");
15096 cx.simulate_keystroke(".");
15097
15098 let unresolved_item_1 = lsp::CompletionItem {
15099 label: "id".to_string(),
15100 filter_text: Some("id".to_string()),
15101 detail: None,
15102 documentation: None,
15103 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15104 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15105 new_text: ".id".to_string(),
15106 })),
15107 ..lsp::CompletionItem::default()
15108 };
15109 let resolved_item_1 = lsp::CompletionItem {
15110 additional_text_edits: Some(vec![lsp::TextEdit {
15111 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15112 new_text: "!!".to_string(),
15113 }]),
15114 ..unresolved_item_1.clone()
15115 };
15116 let unresolved_item_2 = lsp::CompletionItem {
15117 label: "other".to_string(),
15118 filter_text: Some("other".to_string()),
15119 detail: None,
15120 documentation: None,
15121 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15122 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15123 new_text: ".other".to_string(),
15124 })),
15125 ..lsp::CompletionItem::default()
15126 };
15127 let resolved_item_2 = lsp::CompletionItem {
15128 additional_text_edits: Some(vec![lsp::TextEdit {
15129 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15130 new_text: "??".to_string(),
15131 }]),
15132 ..unresolved_item_2.clone()
15133 };
15134
15135 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15136 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15137 cx.lsp
15138 .server
15139 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15140 let unresolved_item_1 = unresolved_item_1.clone();
15141 let resolved_item_1 = resolved_item_1.clone();
15142 let unresolved_item_2 = unresolved_item_2.clone();
15143 let resolved_item_2 = resolved_item_2.clone();
15144 let resolve_requests_1 = resolve_requests_1.clone();
15145 let resolve_requests_2 = resolve_requests_2.clone();
15146 move |unresolved_request, _| {
15147 let unresolved_item_1 = unresolved_item_1.clone();
15148 let resolved_item_1 = resolved_item_1.clone();
15149 let unresolved_item_2 = unresolved_item_2.clone();
15150 let resolved_item_2 = resolved_item_2.clone();
15151 let resolve_requests_1 = resolve_requests_1.clone();
15152 let resolve_requests_2 = resolve_requests_2.clone();
15153 async move {
15154 if unresolved_request == unresolved_item_1 {
15155 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15156 Ok(resolved_item_1.clone())
15157 } else if unresolved_request == unresolved_item_2 {
15158 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15159 Ok(resolved_item_2.clone())
15160 } else {
15161 panic!("Unexpected completion item {unresolved_request:?}")
15162 }
15163 }
15164 }
15165 })
15166 .detach();
15167
15168 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15169 let unresolved_item_1 = unresolved_item_1.clone();
15170 let unresolved_item_2 = unresolved_item_2.clone();
15171 async move {
15172 Ok(Some(lsp::CompletionResponse::Array(vec![
15173 unresolved_item_1,
15174 unresolved_item_2,
15175 ])))
15176 }
15177 })
15178 .next()
15179 .await;
15180
15181 cx.condition(|editor, _| editor.context_menu_visible())
15182 .await;
15183 cx.update_editor(|editor, _, _| {
15184 let context_menu = editor.context_menu.borrow_mut();
15185 let context_menu = context_menu
15186 .as_ref()
15187 .expect("Should have the context menu deployed");
15188 match context_menu {
15189 CodeContextMenu::Completions(completions_menu) => {
15190 let completions = completions_menu.completions.borrow_mut();
15191 assert_eq!(
15192 completions
15193 .iter()
15194 .map(|completion| &completion.label.text)
15195 .collect::<Vec<_>>(),
15196 vec!["id", "other"]
15197 )
15198 }
15199 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15200 }
15201 });
15202 cx.run_until_parked();
15203
15204 cx.update_editor(|editor, window, cx| {
15205 editor.context_menu_next(&ContextMenuNext, window, cx);
15206 });
15207 cx.run_until_parked();
15208 cx.update_editor(|editor, window, cx| {
15209 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15210 });
15211 cx.run_until_parked();
15212 cx.update_editor(|editor, window, cx| {
15213 editor.context_menu_next(&ContextMenuNext, window, cx);
15214 });
15215 cx.run_until_parked();
15216 cx.update_editor(|editor, window, cx| {
15217 editor
15218 .compose_completion(&ComposeCompletion::default(), window, cx)
15219 .expect("No task returned")
15220 })
15221 .await
15222 .expect("Completion failed");
15223 cx.run_until_parked();
15224
15225 cx.update_editor(|editor, _, cx| {
15226 assert_eq!(
15227 resolve_requests_1.load(atomic::Ordering::Acquire),
15228 1,
15229 "Should always resolve once despite multiple selections"
15230 );
15231 assert_eq!(
15232 resolve_requests_2.load(atomic::Ordering::Acquire),
15233 1,
15234 "Should always resolve once after multiple selections and applying the completion"
15235 );
15236 assert_eq!(
15237 editor.text(cx),
15238 "fn main() { let a = ??.other; }",
15239 "Should use resolved data when applying the completion"
15240 );
15241 });
15242}
15243
15244#[gpui::test]
15245async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15246 init_test(cx, |_| {});
15247
15248 let item_0 = lsp::CompletionItem {
15249 label: "abs".into(),
15250 insert_text: Some("abs".into()),
15251 data: Some(json!({ "very": "special"})),
15252 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15253 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15254 lsp::InsertReplaceEdit {
15255 new_text: "abs".to_string(),
15256 insert: lsp::Range::default(),
15257 replace: lsp::Range::default(),
15258 },
15259 )),
15260 ..lsp::CompletionItem::default()
15261 };
15262 let items = iter::once(item_0.clone())
15263 .chain((11..51).map(|i| lsp::CompletionItem {
15264 label: format!("item_{}", i),
15265 insert_text: Some(format!("item_{}", i)),
15266 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15267 ..lsp::CompletionItem::default()
15268 }))
15269 .collect::<Vec<_>>();
15270
15271 let default_commit_characters = vec!["?".to_string()];
15272 let default_data = json!({ "default": "data"});
15273 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15274 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15275 let default_edit_range = lsp::Range {
15276 start: lsp::Position {
15277 line: 0,
15278 character: 5,
15279 },
15280 end: lsp::Position {
15281 line: 0,
15282 character: 5,
15283 },
15284 };
15285
15286 let mut cx = EditorLspTestContext::new_rust(
15287 lsp::ServerCapabilities {
15288 completion_provider: Some(lsp::CompletionOptions {
15289 trigger_characters: Some(vec![".".to_string()]),
15290 resolve_provider: Some(true),
15291 ..Default::default()
15292 }),
15293 ..Default::default()
15294 },
15295 cx,
15296 )
15297 .await;
15298
15299 cx.set_state("fn main() { let a = 2ˇ; }");
15300 cx.simulate_keystroke(".");
15301
15302 let completion_data = default_data.clone();
15303 let completion_characters = default_commit_characters.clone();
15304 let completion_items = items.clone();
15305 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15306 let default_data = completion_data.clone();
15307 let default_commit_characters = completion_characters.clone();
15308 let items = completion_items.clone();
15309 async move {
15310 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15311 items,
15312 item_defaults: Some(lsp::CompletionListItemDefaults {
15313 data: Some(default_data.clone()),
15314 commit_characters: Some(default_commit_characters.clone()),
15315 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15316 default_edit_range,
15317 )),
15318 insert_text_format: Some(default_insert_text_format),
15319 insert_text_mode: Some(default_insert_text_mode),
15320 }),
15321 ..lsp::CompletionList::default()
15322 })))
15323 }
15324 })
15325 .next()
15326 .await;
15327
15328 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15329 cx.lsp
15330 .server
15331 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15332 let closure_resolved_items = resolved_items.clone();
15333 move |item_to_resolve, _| {
15334 let closure_resolved_items = closure_resolved_items.clone();
15335 async move {
15336 closure_resolved_items.lock().push(item_to_resolve.clone());
15337 Ok(item_to_resolve)
15338 }
15339 }
15340 })
15341 .detach();
15342
15343 cx.condition(|editor, _| editor.context_menu_visible())
15344 .await;
15345 cx.run_until_parked();
15346 cx.update_editor(|editor, _, _| {
15347 let menu = editor.context_menu.borrow_mut();
15348 match menu.as_ref().expect("should have the completions menu") {
15349 CodeContextMenu::Completions(completions_menu) => {
15350 assert_eq!(
15351 completions_menu
15352 .entries
15353 .borrow()
15354 .iter()
15355 .map(|mat| mat.string.clone())
15356 .collect::<Vec<String>>(),
15357 items
15358 .iter()
15359 .map(|completion| completion.label.clone())
15360 .collect::<Vec<String>>()
15361 );
15362 }
15363 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15364 }
15365 });
15366 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15367 // with 4 from the end.
15368 assert_eq!(
15369 *resolved_items.lock(),
15370 [&items[0..16], &items[items.len() - 4..items.len()]]
15371 .concat()
15372 .iter()
15373 .cloned()
15374 .map(|mut item| {
15375 if item.data.is_none() {
15376 item.data = Some(default_data.clone());
15377 }
15378 item
15379 })
15380 .collect::<Vec<lsp::CompletionItem>>(),
15381 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15382 );
15383 resolved_items.lock().clear();
15384
15385 cx.update_editor(|editor, window, cx| {
15386 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15387 });
15388 cx.run_until_parked();
15389 // Completions that have already been resolved are skipped.
15390 assert_eq!(
15391 *resolved_items.lock(),
15392 items[items.len() - 16..items.len() - 4]
15393 .iter()
15394 .cloned()
15395 .map(|mut item| {
15396 if item.data.is_none() {
15397 item.data = Some(default_data.clone());
15398 }
15399 item
15400 })
15401 .collect::<Vec<lsp::CompletionItem>>()
15402 );
15403 resolved_items.lock().clear();
15404}
15405
15406#[gpui::test]
15407async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15408 init_test(cx, |_| {});
15409
15410 let mut cx = EditorLspTestContext::new(
15411 Language::new(
15412 LanguageConfig {
15413 matcher: LanguageMatcher {
15414 path_suffixes: vec!["jsx".into()],
15415 ..Default::default()
15416 },
15417 overrides: [(
15418 "element".into(),
15419 LanguageConfigOverride {
15420 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15421 ..Default::default()
15422 },
15423 )]
15424 .into_iter()
15425 .collect(),
15426 ..Default::default()
15427 },
15428 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15429 )
15430 .with_override_query("(jsx_self_closing_element) @element")
15431 .unwrap(),
15432 lsp::ServerCapabilities {
15433 completion_provider: Some(lsp::CompletionOptions {
15434 trigger_characters: Some(vec![":".to_string()]),
15435 ..Default::default()
15436 }),
15437 ..Default::default()
15438 },
15439 cx,
15440 )
15441 .await;
15442
15443 cx.lsp
15444 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15445 Ok(Some(lsp::CompletionResponse::Array(vec![
15446 lsp::CompletionItem {
15447 label: "bg-blue".into(),
15448 ..Default::default()
15449 },
15450 lsp::CompletionItem {
15451 label: "bg-red".into(),
15452 ..Default::default()
15453 },
15454 lsp::CompletionItem {
15455 label: "bg-yellow".into(),
15456 ..Default::default()
15457 },
15458 ])))
15459 });
15460
15461 cx.set_state(r#"<p class="bgˇ" />"#);
15462
15463 // Trigger completion when typing a dash, because the dash is an extra
15464 // word character in the 'element' scope, which contains the cursor.
15465 cx.simulate_keystroke("-");
15466 cx.executor().run_until_parked();
15467 cx.update_editor(|editor, _, _| {
15468 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15469 {
15470 assert_eq!(
15471 completion_menu_entries(&menu),
15472 &["bg-blue", "bg-red", "bg-yellow"]
15473 );
15474 } else {
15475 panic!("expected completion menu to be open");
15476 }
15477 });
15478
15479 cx.simulate_keystroke("l");
15480 cx.executor().run_until_parked();
15481 cx.update_editor(|editor, _, _| {
15482 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15483 {
15484 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15485 } else {
15486 panic!("expected completion menu to be open");
15487 }
15488 });
15489
15490 // When filtering completions, consider the character after the '-' to
15491 // be the start of a subword.
15492 cx.set_state(r#"<p class="yelˇ" />"#);
15493 cx.simulate_keystroke("l");
15494 cx.executor().run_until_parked();
15495 cx.update_editor(|editor, _, _| {
15496 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15497 {
15498 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15499 } else {
15500 panic!("expected completion menu to be open");
15501 }
15502 });
15503}
15504
15505fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15506 let entries = menu.entries.borrow();
15507 entries.iter().map(|mat| mat.string.clone()).collect()
15508}
15509
15510#[gpui::test]
15511async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15512 init_test(cx, |settings| {
15513 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15514 FormatterList(vec![Formatter::Prettier].into()),
15515 ))
15516 });
15517
15518 let fs = FakeFs::new(cx.executor());
15519 fs.insert_file(path!("/file.ts"), Default::default()).await;
15520
15521 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15522 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15523
15524 language_registry.add(Arc::new(Language::new(
15525 LanguageConfig {
15526 name: "TypeScript".into(),
15527 matcher: LanguageMatcher {
15528 path_suffixes: vec!["ts".to_string()],
15529 ..Default::default()
15530 },
15531 ..Default::default()
15532 },
15533 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15534 )));
15535 update_test_language_settings(cx, |settings| {
15536 settings.defaults.prettier = Some(PrettierSettings {
15537 allowed: true,
15538 ..PrettierSettings::default()
15539 });
15540 });
15541
15542 let test_plugin = "test_plugin";
15543 let _ = language_registry.register_fake_lsp(
15544 "TypeScript",
15545 FakeLspAdapter {
15546 prettier_plugins: vec![test_plugin],
15547 ..Default::default()
15548 },
15549 );
15550
15551 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15552 let buffer = project
15553 .update(cx, |project, cx| {
15554 project.open_local_buffer(path!("/file.ts"), cx)
15555 })
15556 .await
15557 .unwrap();
15558
15559 let buffer_text = "one\ntwo\nthree\n";
15560 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15561 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15562 editor.update_in(cx, |editor, window, cx| {
15563 editor.set_text(buffer_text, window, cx)
15564 });
15565
15566 editor
15567 .update_in(cx, |editor, window, cx| {
15568 editor.perform_format(
15569 project.clone(),
15570 FormatTrigger::Manual,
15571 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15572 window,
15573 cx,
15574 )
15575 })
15576 .unwrap()
15577 .await;
15578 assert_eq!(
15579 editor.update(cx, |editor, cx| editor.text(cx)),
15580 buffer_text.to_string() + prettier_format_suffix,
15581 "Test prettier formatting was not applied to the original buffer text",
15582 );
15583
15584 update_test_language_settings(cx, |settings| {
15585 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15586 });
15587 let format = editor.update_in(cx, |editor, window, cx| {
15588 editor.perform_format(
15589 project.clone(),
15590 FormatTrigger::Manual,
15591 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15592 window,
15593 cx,
15594 )
15595 });
15596 format.await.unwrap();
15597 assert_eq!(
15598 editor.update(cx, |editor, cx| editor.text(cx)),
15599 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15600 "Autoformatting (via test prettier) was not applied to the original buffer text",
15601 );
15602}
15603
15604#[gpui::test]
15605async fn test_addition_reverts(cx: &mut TestAppContext) {
15606 init_test(cx, |_| {});
15607 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15608 let base_text = indoc! {r#"
15609 struct Row;
15610 struct Row1;
15611 struct Row2;
15612
15613 struct Row4;
15614 struct Row5;
15615 struct Row6;
15616
15617 struct Row8;
15618 struct Row9;
15619 struct Row10;"#};
15620
15621 // When addition hunks are not adjacent to carets, no hunk revert is performed
15622 assert_hunk_revert(
15623 indoc! {r#"struct Row;
15624 struct Row1;
15625 struct Row1.1;
15626 struct Row1.2;
15627 struct Row2;ˇ
15628
15629 struct Row4;
15630 struct Row5;
15631 struct Row6;
15632
15633 struct Row8;
15634 ˇstruct Row9;
15635 struct Row9.1;
15636 struct Row9.2;
15637 struct Row9.3;
15638 struct Row10;"#},
15639 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15640 indoc! {r#"struct Row;
15641 struct Row1;
15642 struct Row1.1;
15643 struct Row1.2;
15644 struct Row2;ˇ
15645
15646 struct Row4;
15647 struct Row5;
15648 struct Row6;
15649
15650 struct Row8;
15651 ˇstruct Row9;
15652 struct Row9.1;
15653 struct Row9.2;
15654 struct Row9.3;
15655 struct Row10;"#},
15656 base_text,
15657 &mut cx,
15658 );
15659 // Same for selections
15660 assert_hunk_revert(
15661 indoc! {r#"struct Row;
15662 struct Row1;
15663 struct Row2;
15664 struct Row2.1;
15665 struct Row2.2;
15666 «ˇ
15667 struct Row4;
15668 struct» Row5;
15669 «struct Row6;
15670 ˇ»
15671 struct Row9.1;
15672 struct Row9.2;
15673 struct Row9.3;
15674 struct Row8;
15675 struct Row9;
15676 struct Row10;"#},
15677 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15678 indoc! {r#"struct Row;
15679 struct Row1;
15680 struct Row2;
15681 struct Row2.1;
15682 struct Row2.2;
15683 «ˇ
15684 struct Row4;
15685 struct» Row5;
15686 «struct Row6;
15687 ˇ»
15688 struct Row9.1;
15689 struct Row9.2;
15690 struct Row9.3;
15691 struct Row8;
15692 struct Row9;
15693 struct Row10;"#},
15694 base_text,
15695 &mut cx,
15696 );
15697
15698 // When carets and selections intersect the addition hunks, those are reverted.
15699 // Adjacent carets got merged.
15700 assert_hunk_revert(
15701 indoc! {r#"struct Row;
15702 ˇ// something on the top
15703 struct Row1;
15704 struct Row2;
15705 struct Roˇw3.1;
15706 struct Row2.2;
15707 struct Row2.3;ˇ
15708
15709 struct Row4;
15710 struct ˇRow5.1;
15711 struct Row5.2;
15712 struct «Rowˇ»5.3;
15713 struct Row5;
15714 struct Row6;
15715 ˇ
15716 struct Row9.1;
15717 struct «Rowˇ»9.2;
15718 struct «ˇRow»9.3;
15719 struct Row8;
15720 struct Row9;
15721 «ˇ// something on bottom»
15722 struct Row10;"#},
15723 vec![
15724 DiffHunkStatusKind::Added,
15725 DiffHunkStatusKind::Added,
15726 DiffHunkStatusKind::Added,
15727 DiffHunkStatusKind::Added,
15728 DiffHunkStatusKind::Added,
15729 ],
15730 indoc! {r#"struct Row;
15731 ˇstruct Row1;
15732 struct Row2;
15733 ˇ
15734 struct Row4;
15735 ˇstruct Row5;
15736 struct Row6;
15737 ˇ
15738 ˇstruct Row8;
15739 struct Row9;
15740 ˇstruct Row10;"#},
15741 base_text,
15742 &mut cx,
15743 );
15744}
15745
15746#[gpui::test]
15747async fn test_modification_reverts(cx: &mut TestAppContext) {
15748 init_test(cx, |_| {});
15749 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15750 let base_text = indoc! {r#"
15751 struct Row;
15752 struct Row1;
15753 struct Row2;
15754
15755 struct Row4;
15756 struct Row5;
15757 struct Row6;
15758
15759 struct Row8;
15760 struct Row9;
15761 struct Row10;"#};
15762
15763 // Modification hunks behave the same as the addition ones.
15764 assert_hunk_revert(
15765 indoc! {r#"struct Row;
15766 struct Row1;
15767 struct Row33;
15768 ˇ
15769 struct Row4;
15770 struct Row5;
15771 struct Row6;
15772 ˇ
15773 struct Row99;
15774 struct Row9;
15775 struct Row10;"#},
15776 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15777 indoc! {r#"struct Row;
15778 struct Row1;
15779 struct Row33;
15780 ˇ
15781 struct Row4;
15782 struct Row5;
15783 struct Row6;
15784 ˇ
15785 struct Row99;
15786 struct Row9;
15787 struct Row10;"#},
15788 base_text,
15789 &mut cx,
15790 );
15791 assert_hunk_revert(
15792 indoc! {r#"struct Row;
15793 struct Row1;
15794 struct Row33;
15795 «ˇ
15796 struct Row4;
15797 struct» Row5;
15798 «struct Row6;
15799 ˇ»
15800 struct Row99;
15801 struct Row9;
15802 struct Row10;"#},
15803 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15804 indoc! {r#"struct Row;
15805 struct Row1;
15806 struct Row33;
15807 «ˇ
15808 struct Row4;
15809 struct» Row5;
15810 «struct Row6;
15811 ˇ»
15812 struct Row99;
15813 struct Row9;
15814 struct Row10;"#},
15815 base_text,
15816 &mut cx,
15817 );
15818
15819 assert_hunk_revert(
15820 indoc! {r#"ˇstruct Row1.1;
15821 struct Row1;
15822 «ˇstr»uct Row22;
15823
15824 struct ˇRow44;
15825 struct Row5;
15826 struct «Rˇ»ow66;ˇ
15827
15828 «struˇ»ct Row88;
15829 struct Row9;
15830 struct Row1011;ˇ"#},
15831 vec![
15832 DiffHunkStatusKind::Modified,
15833 DiffHunkStatusKind::Modified,
15834 DiffHunkStatusKind::Modified,
15835 DiffHunkStatusKind::Modified,
15836 DiffHunkStatusKind::Modified,
15837 DiffHunkStatusKind::Modified,
15838 ],
15839 indoc! {r#"struct Row;
15840 ˇstruct Row1;
15841 struct Row2;
15842 ˇ
15843 struct Row4;
15844 ˇstruct Row5;
15845 struct Row6;
15846 ˇ
15847 struct Row8;
15848 ˇstruct Row9;
15849 struct Row10;ˇ"#},
15850 base_text,
15851 &mut cx,
15852 );
15853}
15854
15855#[gpui::test]
15856async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15857 init_test(cx, |_| {});
15858 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15859 let base_text = indoc! {r#"
15860 one
15861
15862 two
15863 three
15864 "#};
15865
15866 cx.set_head_text(base_text);
15867 cx.set_state("\nˇ\n");
15868 cx.executor().run_until_parked();
15869 cx.update_editor(|editor, _window, cx| {
15870 editor.expand_selected_diff_hunks(cx);
15871 });
15872 cx.executor().run_until_parked();
15873 cx.update_editor(|editor, window, cx| {
15874 editor.backspace(&Default::default(), window, cx);
15875 });
15876 cx.run_until_parked();
15877 cx.assert_state_with_diff(
15878 indoc! {r#"
15879
15880 - two
15881 - threeˇ
15882 +
15883 "#}
15884 .to_string(),
15885 );
15886}
15887
15888#[gpui::test]
15889async fn test_deletion_reverts(cx: &mut TestAppContext) {
15890 init_test(cx, |_| {});
15891 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15892 let base_text = indoc! {r#"struct Row;
15893struct Row1;
15894struct Row2;
15895
15896struct Row4;
15897struct Row5;
15898struct Row6;
15899
15900struct Row8;
15901struct Row9;
15902struct Row10;"#};
15903
15904 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15905 assert_hunk_revert(
15906 indoc! {r#"struct Row;
15907 struct Row2;
15908
15909 ˇstruct Row4;
15910 struct Row5;
15911 struct Row6;
15912 ˇ
15913 struct Row8;
15914 struct Row10;"#},
15915 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15916 indoc! {r#"struct Row;
15917 struct Row2;
15918
15919 ˇstruct Row4;
15920 struct Row5;
15921 struct Row6;
15922 ˇ
15923 struct Row8;
15924 struct Row10;"#},
15925 base_text,
15926 &mut cx,
15927 );
15928 assert_hunk_revert(
15929 indoc! {r#"struct Row;
15930 struct Row2;
15931
15932 «ˇstruct Row4;
15933 struct» Row5;
15934 «struct Row6;
15935 ˇ»
15936 struct Row8;
15937 struct Row10;"#},
15938 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15939 indoc! {r#"struct Row;
15940 struct Row2;
15941
15942 «ˇstruct Row4;
15943 struct» Row5;
15944 «struct Row6;
15945 ˇ»
15946 struct Row8;
15947 struct Row10;"#},
15948 base_text,
15949 &mut cx,
15950 );
15951
15952 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15953 assert_hunk_revert(
15954 indoc! {r#"struct Row;
15955 ˇstruct Row2;
15956
15957 struct Row4;
15958 struct Row5;
15959 struct Row6;
15960
15961 struct Row8;ˇ
15962 struct Row10;"#},
15963 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15964 indoc! {r#"struct Row;
15965 struct Row1;
15966 ˇstruct Row2;
15967
15968 struct Row4;
15969 struct Row5;
15970 struct Row6;
15971
15972 struct Row8;ˇ
15973 struct Row9;
15974 struct Row10;"#},
15975 base_text,
15976 &mut cx,
15977 );
15978 assert_hunk_revert(
15979 indoc! {r#"struct Row;
15980 struct Row2«ˇ;
15981 struct Row4;
15982 struct» Row5;
15983 «struct Row6;
15984
15985 struct Row8;ˇ»
15986 struct Row10;"#},
15987 vec![
15988 DiffHunkStatusKind::Deleted,
15989 DiffHunkStatusKind::Deleted,
15990 DiffHunkStatusKind::Deleted,
15991 ],
15992 indoc! {r#"struct Row;
15993 struct Row1;
15994 struct Row2«ˇ;
15995
15996 struct Row4;
15997 struct» Row5;
15998 «struct Row6;
15999
16000 struct Row8;ˇ»
16001 struct Row9;
16002 struct Row10;"#},
16003 base_text,
16004 &mut cx,
16005 );
16006}
16007
16008#[gpui::test]
16009async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16010 init_test(cx, |_| {});
16011
16012 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16013 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16014 let base_text_3 =
16015 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16016
16017 let text_1 = edit_first_char_of_every_line(base_text_1);
16018 let text_2 = edit_first_char_of_every_line(base_text_2);
16019 let text_3 = edit_first_char_of_every_line(base_text_3);
16020
16021 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16022 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16023 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16024
16025 let multibuffer = cx.new(|cx| {
16026 let mut multibuffer = MultiBuffer::new(ReadWrite);
16027 multibuffer.push_excerpts(
16028 buffer_1.clone(),
16029 [
16030 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16031 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16032 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16033 ],
16034 cx,
16035 );
16036 multibuffer.push_excerpts(
16037 buffer_2.clone(),
16038 [
16039 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16040 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16041 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16042 ],
16043 cx,
16044 );
16045 multibuffer.push_excerpts(
16046 buffer_3.clone(),
16047 [
16048 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16049 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16050 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16051 ],
16052 cx,
16053 );
16054 multibuffer
16055 });
16056
16057 let fs = FakeFs::new(cx.executor());
16058 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16059 let (editor, cx) = cx
16060 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16061 editor.update_in(cx, |editor, _window, cx| {
16062 for (buffer, diff_base) in [
16063 (buffer_1.clone(), base_text_1),
16064 (buffer_2.clone(), base_text_2),
16065 (buffer_3.clone(), base_text_3),
16066 ] {
16067 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16068 editor
16069 .buffer
16070 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16071 }
16072 });
16073 cx.executor().run_until_parked();
16074
16075 editor.update_in(cx, |editor, window, cx| {
16076 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}");
16077 editor.select_all(&SelectAll, window, cx);
16078 editor.git_restore(&Default::default(), window, cx);
16079 });
16080 cx.executor().run_until_parked();
16081
16082 // When all ranges are selected, all buffer hunks are reverted.
16083 editor.update(cx, |editor, cx| {
16084 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");
16085 });
16086 buffer_1.update(cx, |buffer, _| {
16087 assert_eq!(buffer.text(), base_text_1);
16088 });
16089 buffer_2.update(cx, |buffer, _| {
16090 assert_eq!(buffer.text(), base_text_2);
16091 });
16092 buffer_3.update(cx, |buffer, _| {
16093 assert_eq!(buffer.text(), base_text_3);
16094 });
16095
16096 editor.update_in(cx, |editor, window, cx| {
16097 editor.undo(&Default::default(), window, cx);
16098 });
16099
16100 editor.update_in(cx, |editor, window, cx| {
16101 editor.change_selections(None, window, cx, |s| {
16102 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16103 });
16104 editor.git_restore(&Default::default(), window, cx);
16105 });
16106
16107 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16108 // but not affect buffer_2 and its related excerpts.
16109 editor.update(cx, |editor, cx| {
16110 assert_eq!(
16111 editor.text(cx),
16112 "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}"
16113 );
16114 });
16115 buffer_1.update(cx, |buffer, _| {
16116 assert_eq!(buffer.text(), base_text_1);
16117 });
16118 buffer_2.update(cx, |buffer, _| {
16119 assert_eq!(
16120 buffer.text(),
16121 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16122 );
16123 });
16124 buffer_3.update(cx, |buffer, _| {
16125 assert_eq!(
16126 buffer.text(),
16127 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16128 );
16129 });
16130
16131 fn edit_first_char_of_every_line(text: &str) -> String {
16132 text.split('\n')
16133 .map(|line| format!("X{}", &line[1..]))
16134 .collect::<Vec<_>>()
16135 .join("\n")
16136 }
16137}
16138
16139#[gpui::test]
16140async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16141 init_test(cx, |_| {});
16142
16143 let cols = 4;
16144 let rows = 10;
16145 let sample_text_1 = sample_text(rows, cols, 'a');
16146 assert_eq!(
16147 sample_text_1,
16148 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16149 );
16150 let sample_text_2 = sample_text(rows, cols, 'l');
16151 assert_eq!(
16152 sample_text_2,
16153 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16154 );
16155 let sample_text_3 = sample_text(rows, cols, 'v');
16156 assert_eq!(
16157 sample_text_3,
16158 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16159 );
16160
16161 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16162 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16163 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16164
16165 let multi_buffer = cx.new(|cx| {
16166 let mut multibuffer = MultiBuffer::new(ReadWrite);
16167 multibuffer.push_excerpts(
16168 buffer_1.clone(),
16169 [
16170 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16171 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16172 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16173 ],
16174 cx,
16175 );
16176 multibuffer.push_excerpts(
16177 buffer_2.clone(),
16178 [
16179 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16180 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16181 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16182 ],
16183 cx,
16184 );
16185 multibuffer.push_excerpts(
16186 buffer_3.clone(),
16187 [
16188 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16189 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16190 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16191 ],
16192 cx,
16193 );
16194 multibuffer
16195 });
16196
16197 let fs = FakeFs::new(cx.executor());
16198 fs.insert_tree(
16199 "/a",
16200 json!({
16201 "main.rs": sample_text_1,
16202 "other.rs": sample_text_2,
16203 "lib.rs": sample_text_3,
16204 }),
16205 )
16206 .await;
16207 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16208 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16209 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16210 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16211 Editor::new(
16212 EditorMode::full(),
16213 multi_buffer,
16214 Some(project.clone()),
16215 window,
16216 cx,
16217 )
16218 });
16219 let multibuffer_item_id = workspace
16220 .update(cx, |workspace, window, cx| {
16221 assert!(
16222 workspace.active_item(cx).is_none(),
16223 "active item should be None before the first item is added"
16224 );
16225 workspace.add_item_to_active_pane(
16226 Box::new(multi_buffer_editor.clone()),
16227 None,
16228 true,
16229 window,
16230 cx,
16231 );
16232 let active_item = workspace
16233 .active_item(cx)
16234 .expect("should have an active item after adding the multi buffer");
16235 assert!(
16236 !active_item.is_singleton(cx),
16237 "A multi buffer was expected to active after adding"
16238 );
16239 active_item.item_id()
16240 })
16241 .unwrap();
16242 cx.executor().run_until_parked();
16243
16244 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16245 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16246 s.select_ranges(Some(1..2))
16247 });
16248 editor.open_excerpts(&OpenExcerpts, window, cx);
16249 });
16250 cx.executor().run_until_parked();
16251 let first_item_id = workspace
16252 .update(cx, |workspace, window, cx| {
16253 let active_item = workspace
16254 .active_item(cx)
16255 .expect("should have an active item after navigating into the 1st buffer");
16256 let first_item_id = active_item.item_id();
16257 assert_ne!(
16258 first_item_id, multibuffer_item_id,
16259 "Should navigate into the 1st buffer and activate it"
16260 );
16261 assert!(
16262 active_item.is_singleton(cx),
16263 "New active item should be a singleton buffer"
16264 );
16265 assert_eq!(
16266 active_item
16267 .act_as::<Editor>(cx)
16268 .expect("should have navigated into an editor for the 1st buffer")
16269 .read(cx)
16270 .text(cx),
16271 sample_text_1
16272 );
16273
16274 workspace
16275 .go_back(workspace.active_pane().downgrade(), window, cx)
16276 .detach_and_log_err(cx);
16277
16278 first_item_id
16279 })
16280 .unwrap();
16281 cx.executor().run_until_parked();
16282 workspace
16283 .update(cx, |workspace, _, cx| {
16284 let active_item = workspace
16285 .active_item(cx)
16286 .expect("should have an active item after navigating back");
16287 assert_eq!(
16288 active_item.item_id(),
16289 multibuffer_item_id,
16290 "Should navigate back to the multi buffer"
16291 );
16292 assert!(!active_item.is_singleton(cx));
16293 })
16294 .unwrap();
16295
16296 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16297 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16298 s.select_ranges(Some(39..40))
16299 });
16300 editor.open_excerpts(&OpenExcerpts, window, cx);
16301 });
16302 cx.executor().run_until_parked();
16303 let second_item_id = workspace
16304 .update(cx, |workspace, window, cx| {
16305 let active_item = workspace
16306 .active_item(cx)
16307 .expect("should have an active item after navigating into the 2nd buffer");
16308 let second_item_id = active_item.item_id();
16309 assert_ne!(
16310 second_item_id, multibuffer_item_id,
16311 "Should navigate away from the multibuffer"
16312 );
16313 assert_ne!(
16314 second_item_id, first_item_id,
16315 "Should navigate into the 2nd buffer and activate it"
16316 );
16317 assert!(
16318 active_item.is_singleton(cx),
16319 "New active item should be a singleton buffer"
16320 );
16321 assert_eq!(
16322 active_item
16323 .act_as::<Editor>(cx)
16324 .expect("should have navigated into an editor")
16325 .read(cx)
16326 .text(cx),
16327 sample_text_2
16328 );
16329
16330 workspace
16331 .go_back(workspace.active_pane().downgrade(), window, cx)
16332 .detach_and_log_err(cx);
16333
16334 second_item_id
16335 })
16336 .unwrap();
16337 cx.executor().run_until_parked();
16338 workspace
16339 .update(cx, |workspace, _, cx| {
16340 let active_item = workspace
16341 .active_item(cx)
16342 .expect("should have an active item after navigating back from the 2nd buffer");
16343 assert_eq!(
16344 active_item.item_id(),
16345 multibuffer_item_id,
16346 "Should navigate back from the 2nd buffer to the multi buffer"
16347 );
16348 assert!(!active_item.is_singleton(cx));
16349 })
16350 .unwrap();
16351
16352 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16353 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16354 s.select_ranges(Some(70..70))
16355 });
16356 editor.open_excerpts(&OpenExcerpts, window, cx);
16357 });
16358 cx.executor().run_until_parked();
16359 workspace
16360 .update(cx, |workspace, window, cx| {
16361 let active_item = workspace
16362 .active_item(cx)
16363 .expect("should have an active item after navigating into the 3rd buffer");
16364 let third_item_id = active_item.item_id();
16365 assert_ne!(
16366 third_item_id, multibuffer_item_id,
16367 "Should navigate into the 3rd buffer and activate it"
16368 );
16369 assert_ne!(third_item_id, first_item_id);
16370 assert_ne!(third_item_id, second_item_id);
16371 assert!(
16372 active_item.is_singleton(cx),
16373 "New active item should be a singleton buffer"
16374 );
16375 assert_eq!(
16376 active_item
16377 .act_as::<Editor>(cx)
16378 .expect("should have navigated into an editor")
16379 .read(cx)
16380 .text(cx),
16381 sample_text_3
16382 );
16383
16384 workspace
16385 .go_back(workspace.active_pane().downgrade(), window, cx)
16386 .detach_and_log_err(cx);
16387 })
16388 .unwrap();
16389 cx.executor().run_until_parked();
16390 workspace
16391 .update(cx, |workspace, _, cx| {
16392 let active_item = workspace
16393 .active_item(cx)
16394 .expect("should have an active item after navigating back from the 3rd buffer");
16395 assert_eq!(
16396 active_item.item_id(),
16397 multibuffer_item_id,
16398 "Should navigate back from the 3rd buffer to the multi buffer"
16399 );
16400 assert!(!active_item.is_singleton(cx));
16401 })
16402 .unwrap();
16403}
16404
16405#[gpui::test]
16406async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16407 init_test(cx, |_| {});
16408
16409 let mut cx = EditorTestContext::new(cx).await;
16410
16411 let diff_base = r#"
16412 use some::mod;
16413
16414 const A: u32 = 42;
16415
16416 fn main() {
16417 println!("hello");
16418
16419 println!("world");
16420 }
16421 "#
16422 .unindent();
16423
16424 cx.set_state(
16425 &r#"
16426 use some::modified;
16427
16428 ˇ
16429 fn main() {
16430 println!("hello there");
16431
16432 println!("around the");
16433 println!("world");
16434 }
16435 "#
16436 .unindent(),
16437 );
16438
16439 cx.set_head_text(&diff_base);
16440 executor.run_until_parked();
16441
16442 cx.update_editor(|editor, window, cx| {
16443 editor.go_to_next_hunk(&GoToHunk, window, cx);
16444 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16445 });
16446 executor.run_until_parked();
16447 cx.assert_state_with_diff(
16448 r#"
16449 use some::modified;
16450
16451
16452 fn main() {
16453 - println!("hello");
16454 + ˇ println!("hello there");
16455
16456 println!("around the");
16457 println!("world");
16458 }
16459 "#
16460 .unindent(),
16461 );
16462
16463 cx.update_editor(|editor, window, cx| {
16464 for _ in 0..2 {
16465 editor.go_to_next_hunk(&GoToHunk, window, cx);
16466 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16467 }
16468 });
16469 executor.run_until_parked();
16470 cx.assert_state_with_diff(
16471 r#"
16472 - use some::mod;
16473 + ˇuse some::modified;
16474
16475
16476 fn main() {
16477 - println!("hello");
16478 + println!("hello there");
16479
16480 + println!("around the");
16481 println!("world");
16482 }
16483 "#
16484 .unindent(),
16485 );
16486
16487 cx.update_editor(|editor, window, cx| {
16488 editor.go_to_next_hunk(&GoToHunk, window, cx);
16489 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16490 });
16491 executor.run_until_parked();
16492 cx.assert_state_with_diff(
16493 r#"
16494 - use some::mod;
16495 + use some::modified;
16496
16497 - const A: u32 = 42;
16498 ˇ
16499 fn main() {
16500 - println!("hello");
16501 + println!("hello there");
16502
16503 + println!("around the");
16504 println!("world");
16505 }
16506 "#
16507 .unindent(),
16508 );
16509
16510 cx.update_editor(|editor, window, cx| {
16511 editor.cancel(&Cancel, window, cx);
16512 });
16513
16514 cx.assert_state_with_diff(
16515 r#"
16516 use some::modified;
16517
16518 ˇ
16519 fn main() {
16520 println!("hello there");
16521
16522 println!("around the");
16523 println!("world");
16524 }
16525 "#
16526 .unindent(),
16527 );
16528}
16529
16530#[gpui::test]
16531async fn test_diff_base_change_with_expanded_diff_hunks(
16532 executor: BackgroundExecutor,
16533 cx: &mut TestAppContext,
16534) {
16535 init_test(cx, |_| {});
16536
16537 let mut cx = EditorTestContext::new(cx).await;
16538
16539 let diff_base = r#"
16540 use some::mod1;
16541 use some::mod2;
16542
16543 const A: u32 = 42;
16544 const B: u32 = 42;
16545 const C: u32 = 42;
16546
16547 fn main() {
16548 println!("hello");
16549
16550 println!("world");
16551 }
16552 "#
16553 .unindent();
16554
16555 cx.set_state(
16556 &r#"
16557 use some::mod2;
16558
16559 const A: u32 = 42;
16560 const C: u32 = 42;
16561
16562 fn main(ˇ) {
16563 //println!("hello");
16564
16565 println!("world");
16566 //
16567 //
16568 }
16569 "#
16570 .unindent(),
16571 );
16572
16573 cx.set_head_text(&diff_base);
16574 executor.run_until_parked();
16575
16576 cx.update_editor(|editor, window, cx| {
16577 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16578 });
16579 executor.run_until_parked();
16580 cx.assert_state_with_diff(
16581 r#"
16582 - use some::mod1;
16583 use some::mod2;
16584
16585 const A: u32 = 42;
16586 - const B: u32 = 42;
16587 const C: u32 = 42;
16588
16589 fn main(ˇ) {
16590 - println!("hello");
16591 + //println!("hello");
16592
16593 println!("world");
16594 + //
16595 + //
16596 }
16597 "#
16598 .unindent(),
16599 );
16600
16601 cx.set_head_text("new diff base!");
16602 executor.run_until_parked();
16603 cx.assert_state_with_diff(
16604 r#"
16605 - new diff base!
16606 + use some::mod2;
16607 +
16608 + const A: u32 = 42;
16609 + const C: u32 = 42;
16610 +
16611 + fn main(ˇ) {
16612 + //println!("hello");
16613 +
16614 + println!("world");
16615 + //
16616 + //
16617 + }
16618 "#
16619 .unindent(),
16620 );
16621}
16622
16623#[gpui::test]
16624async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16625 init_test(cx, |_| {});
16626
16627 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16628 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16629 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16630 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16631 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16632 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16633
16634 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16635 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16636 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16637
16638 let multi_buffer = cx.new(|cx| {
16639 let mut multibuffer = MultiBuffer::new(ReadWrite);
16640 multibuffer.push_excerpts(
16641 buffer_1.clone(),
16642 [
16643 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16644 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16645 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16646 ],
16647 cx,
16648 );
16649 multibuffer.push_excerpts(
16650 buffer_2.clone(),
16651 [
16652 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16653 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16654 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16655 ],
16656 cx,
16657 );
16658 multibuffer.push_excerpts(
16659 buffer_3.clone(),
16660 [
16661 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16662 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16663 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16664 ],
16665 cx,
16666 );
16667 multibuffer
16668 });
16669
16670 let editor =
16671 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16672 editor
16673 .update(cx, |editor, _window, cx| {
16674 for (buffer, diff_base) in [
16675 (buffer_1.clone(), file_1_old),
16676 (buffer_2.clone(), file_2_old),
16677 (buffer_3.clone(), file_3_old),
16678 ] {
16679 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16680 editor
16681 .buffer
16682 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16683 }
16684 })
16685 .unwrap();
16686
16687 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16688 cx.run_until_parked();
16689
16690 cx.assert_editor_state(
16691 &"
16692 ˇaaa
16693 ccc
16694 ddd
16695
16696 ggg
16697 hhh
16698
16699
16700 lll
16701 mmm
16702 NNN
16703
16704 qqq
16705 rrr
16706
16707 uuu
16708 111
16709 222
16710 333
16711
16712 666
16713 777
16714
16715 000
16716 !!!"
16717 .unindent(),
16718 );
16719
16720 cx.update_editor(|editor, window, cx| {
16721 editor.select_all(&SelectAll, window, cx);
16722 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16723 });
16724 cx.executor().run_until_parked();
16725
16726 cx.assert_state_with_diff(
16727 "
16728 «aaa
16729 - bbb
16730 ccc
16731 ddd
16732
16733 ggg
16734 hhh
16735
16736
16737 lll
16738 mmm
16739 - nnn
16740 + NNN
16741
16742 qqq
16743 rrr
16744
16745 uuu
16746 111
16747 222
16748 333
16749
16750 + 666
16751 777
16752
16753 000
16754 !!!ˇ»"
16755 .unindent(),
16756 );
16757}
16758
16759#[gpui::test]
16760async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16761 init_test(cx, |_| {});
16762
16763 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16764 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16765
16766 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16767 let multi_buffer = cx.new(|cx| {
16768 let mut multibuffer = MultiBuffer::new(ReadWrite);
16769 multibuffer.push_excerpts(
16770 buffer.clone(),
16771 [
16772 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16773 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16774 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16775 ],
16776 cx,
16777 );
16778 multibuffer
16779 });
16780
16781 let editor =
16782 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16783 editor
16784 .update(cx, |editor, _window, cx| {
16785 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16786 editor
16787 .buffer
16788 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16789 })
16790 .unwrap();
16791
16792 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16793 cx.run_until_parked();
16794
16795 cx.update_editor(|editor, window, cx| {
16796 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16797 });
16798 cx.executor().run_until_parked();
16799
16800 // When the start of a hunk coincides with the start of its excerpt,
16801 // the hunk is expanded. When the start of a a hunk is earlier than
16802 // the start of its excerpt, the hunk is not expanded.
16803 cx.assert_state_with_diff(
16804 "
16805 ˇaaa
16806 - bbb
16807 + BBB
16808
16809 - ddd
16810 - eee
16811 + DDD
16812 + EEE
16813 fff
16814
16815 iii
16816 "
16817 .unindent(),
16818 );
16819}
16820
16821#[gpui::test]
16822async fn test_edits_around_expanded_insertion_hunks(
16823 executor: BackgroundExecutor,
16824 cx: &mut TestAppContext,
16825) {
16826 init_test(cx, |_| {});
16827
16828 let mut cx = EditorTestContext::new(cx).await;
16829
16830 let diff_base = r#"
16831 use some::mod1;
16832 use some::mod2;
16833
16834 const A: u32 = 42;
16835
16836 fn main() {
16837 println!("hello");
16838
16839 println!("world");
16840 }
16841 "#
16842 .unindent();
16843 executor.run_until_parked();
16844 cx.set_state(
16845 &r#"
16846 use some::mod1;
16847 use some::mod2;
16848
16849 const A: u32 = 42;
16850 const B: u32 = 42;
16851 const C: u32 = 42;
16852 ˇ
16853
16854 fn main() {
16855 println!("hello");
16856
16857 println!("world");
16858 }
16859 "#
16860 .unindent(),
16861 );
16862
16863 cx.set_head_text(&diff_base);
16864 executor.run_until_parked();
16865
16866 cx.update_editor(|editor, window, cx| {
16867 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16868 });
16869 executor.run_until_parked();
16870
16871 cx.assert_state_with_diff(
16872 r#"
16873 use some::mod1;
16874 use some::mod2;
16875
16876 const A: u32 = 42;
16877 + const B: u32 = 42;
16878 + const C: u32 = 42;
16879 + ˇ
16880
16881 fn main() {
16882 println!("hello");
16883
16884 println!("world");
16885 }
16886 "#
16887 .unindent(),
16888 );
16889
16890 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16891 executor.run_until_parked();
16892
16893 cx.assert_state_with_diff(
16894 r#"
16895 use some::mod1;
16896 use some::mod2;
16897
16898 const A: u32 = 42;
16899 + const B: u32 = 42;
16900 + const C: u32 = 42;
16901 + const D: u32 = 42;
16902 + ˇ
16903
16904 fn main() {
16905 println!("hello");
16906
16907 println!("world");
16908 }
16909 "#
16910 .unindent(),
16911 );
16912
16913 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16914 executor.run_until_parked();
16915
16916 cx.assert_state_with_diff(
16917 r#"
16918 use some::mod1;
16919 use some::mod2;
16920
16921 const A: u32 = 42;
16922 + const B: u32 = 42;
16923 + const C: u32 = 42;
16924 + const D: u32 = 42;
16925 + const E: u32 = 42;
16926 + ˇ
16927
16928 fn main() {
16929 println!("hello");
16930
16931 println!("world");
16932 }
16933 "#
16934 .unindent(),
16935 );
16936
16937 cx.update_editor(|editor, window, cx| {
16938 editor.delete_line(&DeleteLine, window, cx);
16939 });
16940 executor.run_until_parked();
16941
16942 cx.assert_state_with_diff(
16943 r#"
16944 use some::mod1;
16945 use some::mod2;
16946
16947 const A: u32 = 42;
16948 + const B: u32 = 42;
16949 + const C: u32 = 42;
16950 + const D: u32 = 42;
16951 + const E: u32 = 42;
16952 ˇ
16953 fn main() {
16954 println!("hello");
16955
16956 println!("world");
16957 }
16958 "#
16959 .unindent(),
16960 );
16961
16962 cx.update_editor(|editor, window, cx| {
16963 editor.move_up(&MoveUp, window, cx);
16964 editor.delete_line(&DeleteLine, window, cx);
16965 editor.move_up(&MoveUp, window, cx);
16966 editor.delete_line(&DeleteLine, window, cx);
16967 editor.move_up(&MoveUp, window, cx);
16968 editor.delete_line(&DeleteLine, window, cx);
16969 });
16970 executor.run_until_parked();
16971 cx.assert_state_with_diff(
16972 r#"
16973 use some::mod1;
16974 use some::mod2;
16975
16976 const A: u32 = 42;
16977 + const B: u32 = 42;
16978 ˇ
16979 fn main() {
16980 println!("hello");
16981
16982 println!("world");
16983 }
16984 "#
16985 .unindent(),
16986 );
16987
16988 cx.update_editor(|editor, window, cx| {
16989 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16990 editor.delete_line(&DeleteLine, window, cx);
16991 });
16992 executor.run_until_parked();
16993 cx.assert_state_with_diff(
16994 r#"
16995 ˇ
16996 fn main() {
16997 println!("hello");
16998
16999 println!("world");
17000 }
17001 "#
17002 .unindent(),
17003 );
17004}
17005
17006#[gpui::test]
17007async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17008 init_test(cx, |_| {});
17009
17010 let mut cx = EditorTestContext::new(cx).await;
17011 cx.set_head_text(indoc! { "
17012 one
17013 two
17014 three
17015 four
17016 five
17017 "
17018 });
17019 cx.set_state(indoc! { "
17020 one
17021 ˇthree
17022 five
17023 "});
17024 cx.run_until_parked();
17025 cx.update_editor(|editor, window, cx| {
17026 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17027 });
17028 cx.assert_state_with_diff(
17029 indoc! { "
17030 one
17031 - two
17032 ˇthree
17033 - four
17034 five
17035 "}
17036 .to_string(),
17037 );
17038 cx.update_editor(|editor, window, cx| {
17039 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17040 });
17041
17042 cx.assert_state_with_diff(
17043 indoc! { "
17044 one
17045 ˇthree
17046 five
17047 "}
17048 .to_string(),
17049 );
17050
17051 cx.set_state(indoc! { "
17052 one
17053 ˇTWO
17054 three
17055 four
17056 five
17057 "});
17058 cx.run_until_parked();
17059 cx.update_editor(|editor, window, cx| {
17060 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17061 });
17062
17063 cx.assert_state_with_diff(
17064 indoc! { "
17065 one
17066 - two
17067 + ˇTWO
17068 three
17069 four
17070 five
17071 "}
17072 .to_string(),
17073 );
17074 cx.update_editor(|editor, window, cx| {
17075 editor.move_up(&Default::default(), window, cx);
17076 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17077 });
17078 cx.assert_state_with_diff(
17079 indoc! { "
17080 one
17081 ˇTWO
17082 three
17083 four
17084 five
17085 "}
17086 .to_string(),
17087 );
17088}
17089
17090#[gpui::test]
17091async fn test_edits_around_expanded_deletion_hunks(
17092 executor: BackgroundExecutor,
17093 cx: &mut TestAppContext,
17094) {
17095 init_test(cx, |_| {});
17096
17097 let mut cx = EditorTestContext::new(cx).await;
17098
17099 let diff_base = r#"
17100 use some::mod1;
17101 use some::mod2;
17102
17103 const A: u32 = 42;
17104 const B: u32 = 42;
17105 const C: u32 = 42;
17106
17107
17108 fn main() {
17109 println!("hello");
17110
17111 println!("world");
17112 }
17113 "#
17114 .unindent();
17115 executor.run_until_parked();
17116 cx.set_state(
17117 &r#"
17118 use some::mod1;
17119 use some::mod2;
17120
17121 ˇconst B: u32 = 42;
17122 const C: u32 = 42;
17123
17124
17125 fn main() {
17126 println!("hello");
17127
17128 println!("world");
17129 }
17130 "#
17131 .unindent(),
17132 );
17133
17134 cx.set_head_text(&diff_base);
17135 executor.run_until_parked();
17136
17137 cx.update_editor(|editor, window, cx| {
17138 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17139 });
17140 executor.run_until_parked();
17141
17142 cx.assert_state_with_diff(
17143 r#"
17144 use some::mod1;
17145 use some::mod2;
17146
17147 - const A: u32 = 42;
17148 ˇconst B: u32 = 42;
17149 const C: u32 = 42;
17150
17151
17152 fn main() {
17153 println!("hello");
17154
17155 println!("world");
17156 }
17157 "#
17158 .unindent(),
17159 );
17160
17161 cx.update_editor(|editor, window, cx| {
17162 editor.delete_line(&DeleteLine, window, cx);
17163 });
17164 executor.run_until_parked();
17165 cx.assert_state_with_diff(
17166 r#"
17167 use some::mod1;
17168 use some::mod2;
17169
17170 - const A: u32 = 42;
17171 - const B: u32 = 42;
17172 ˇconst C: u32 = 42;
17173
17174
17175 fn main() {
17176 println!("hello");
17177
17178 println!("world");
17179 }
17180 "#
17181 .unindent(),
17182 );
17183
17184 cx.update_editor(|editor, window, cx| {
17185 editor.delete_line(&DeleteLine, window, cx);
17186 });
17187 executor.run_until_parked();
17188 cx.assert_state_with_diff(
17189 r#"
17190 use some::mod1;
17191 use some::mod2;
17192
17193 - const A: u32 = 42;
17194 - const B: u32 = 42;
17195 - const C: u32 = 42;
17196 ˇ
17197
17198 fn main() {
17199 println!("hello");
17200
17201 println!("world");
17202 }
17203 "#
17204 .unindent(),
17205 );
17206
17207 cx.update_editor(|editor, window, cx| {
17208 editor.handle_input("replacement", window, cx);
17209 });
17210 executor.run_until_parked();
17211 cx.assert_state_with_diff(
17212 r#"
17213 use some::mod1;
17214 use some::mod2;
17215
17216 - const A: u32 = 42;
17217 - const B: u32 = 42;
17218 - const C: u32 = 42;
17219 -
17220 + replacementˇ
17221
17222 fn main() {
17223 println!("hello");
17224
17225 println!("world");
17226 }
17227 "#
17228 .unindent(),
17229 );
17230}
17231
17232#[gpui::test]
17233async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17234 init_test(cx, |_| {});
17235
17236 let mut cx = EditorTestContext::new(cx).await;
17237
17238 let base_text = r#"
17239 one
17240 two
17241 three
17242 four
17243 five
17244 "#
17245 .unindent();
17246 executor.run_until_parked();
17247 cx.set_state(
17248 &r#"
17249 one
17250 two
17251 fˇour
17252 five
17253 "#
17254 .unindent(),
17255 );
17256
17257 cx.set_head_text(&base_text);
17258 executor.run_until_parked();
17259
17260 cx.update_editor(|editor, window, cx| {
17261 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17262 });
17263 executor.run_until_parked();
17264
17265 cx.assert_state_with_diff(
17266 r#"
17267 one
17268 two
17269 - three
17270 fˇour
17271 five
17272 "#
17273 .unindent(),
17274 );
17275
17276 cx.update_editor(|editor, window, cx| {
17277 editor.backspace(&Backspace, window, cx);
17278 editor.backspace(&Backspace, window, cx);
17279 });
17280 executor.run_until_parked();
17281 cx.assert_state_with_diff(
17282 r#"
17283 one
17284 two
17285 - threeˇ
17286 - four
17287 + our
17288 five
17289 "#
17290 .unindent(),
17291 );
17292}
17293
17294#[gpui::test]
17295async fn test_edit_after_expanded_modification_hunk(
17296 executor: BackgroundExecutor,
17297 cx: &mut TestAppContext,
17298) {
17299 init_test(cx, |_| {});
17300
17301 let mut cx = EditorTestContext::new(cx).await;
17302
17303 let diff_base = r#"
17304 use some::mod1;
17305 use some::mod2;
17306
17307 const A: u32 = 42;
17308 const B: u32 = 42;
17309 const C: u32 = 42;
17310 const D: u32 = 42;
17311
17312
17313 fn main() {
17314 println!("hello");
17315
17316 println!("world");
17317 }"#
17318 .unindent();
17319
17320 cx.set_state(
17321 &r#"
17322 use some::mod1;
17323 use some::mod2;
17324
17325 const A: u32 = 42;
17326 const B: u32 = 42;
17327 const C: u32 = 43ˇ
17328 const D: u32 = 42;
17329
17330
17331 fn main() {
17332 println!("hello");
17333
17334 println!("world");
17335 }"#
17336 .unindent(),
17337 );
17338
17339 cx.set_head_text(&diff_base);
17340 executor.run_until_parked();
17341 cx.update_editor(|editor, window, cx| {
17342 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17343 });
17344 executor.run_until_parked();
17345
17346 cx.assert_state_with_diff(
17347 r#"
17348 use some::mod1;
17349 use some::mod2;
17350
17351 const A: u32 = 42;
17352 const B: u32 = 42;
17353 - const C: u32 = 42;
17354 + const C: u32 = 43ˇ
17355 const D: u32 = 42;
17356
17357
17358 fn main() {
17359 println!("hello");
17360
17361 println!("world");
17362 }"#
17363 .unindent(),
17364 );
17365
17366 cx.update_editor(|editor, window, cx| {
17367 editor.handle_input("\nnew_line\n", window, cx);
17368 });
17369 executor.run_until_parked();
17370
17371 cx.assert_state_with_diff(
17372 r#"
17373 use some::mod1;
17374 use some::mod2;
17375
17376 const A: u32 = 42;
17377 const B: u32 = 42;
17378 - const C: u32 = 42;
17379 + const C: u32 = 43
17380 + new_line
17381 + ˇ
17382 const D: u32 = 42;
17383
17384
17385 fn main() {
17386 println!("hello");
17387
17388 println!("world");
17389 }"#
17390 .unindent(),
17391 );
17392}
17393
17394#[gpui::test]
17395async fn test_stage_and_unstage_added_file_hunk(
17396 executor: BackgroundExecutor,
17397 cx: &mut TestAppContext,
17398) {
17399 init_test(cx, |_| {});
17400
17401 let mut cx = EditorTestContext::new(cx).await;
17402 cx.update_editor(|editor, _, cx| {
17403 editor.set_expand_all_diff_hunks(cx);
17404 });
17405
17406 let working_copy = r#"
17407 ˇfn main() {
17408 println!("hello, world!");
17409 }
17410 "#
17411 .unindent();
17412
17413 cx.set_state(&working_copy);
17414 executor.run_until_parked();
17415
17416 cx.assert_state_with_diff(
17417 r#"
17418 + ˇfn main() {
17419 + println!("hello, world!");
17420 + }
17421 "#
17422 .unindent(),
17423 );
17424 cx.assert_index_text(None);
17425
17426 cx.update_editor(|editor, window, cx| {
17427 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17428 });
17429 executor.run_until_parked();
17430 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17431 cx.assert_state_with_diff(
17432 r#"
17433 + ˇfn main() {
17434 + println!("hello, world!");
17435 + }
17436 "#
17437 .unindent(),
17438 );
17439
17440 cx.update_editor(|editor, window, cx| {
17441 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17442 });
17443 executor.run_until_parked();
17444 cx.assert_index_text(None);
17445}
17446
17447async fn setup_indent_guides_editor(
17448 text: &str,
17449 cx: &mut TestAppContext,
17450) -> (BufferId, EditorTestContext) {
17451 init_test(cx, |_| {});
17452
17453 let mut cx = EditorTestContext::new(cx).await;
17454
17455 let buffer_id = cx.update_editor(|editor, window, cx| {
17456 editor.set_text(text, window, cx);
17457 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17458
17459 buffer_ids[0]
17460 });
17461
17462 (buffer_id, cx)
17463}
17464
17465fn assert_indent_guides(
17466 range: Range<u32>,
17467 expected: Vec<IndentGuide>,
17468 active_indices: Option<Vec<usize>>,
17469 cx: &mut EditorTestContext,
17470) {
17471 let indent_guides = cx.update_editor(|editor, window, cx| {
17472 let snapshot = editor.snapshot(window, cx).display_snapshot;
17473 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17474 editor,
17475 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17476 true,
17477 &snapshot,
17478 cx,
17479 );
17480
17481 indent_guides.sort_by(|a, b| {
17482 a.depth.cmp(&b.depth).then(
17483 a.start_row
17484 .cmp(&b.start_row)
17485 .then(a.end_row.cmp(&b.end_row)),
17486 )
17487 });
17488 indent_guides
17489 });
17490
17491 if let Some(expected) = active_indices {
17492 let active_indices = cx.update_editor(|editor, window, cx| {
17493 let snapshot = editor.snapshot(window, cx).display_snapshot;
17494 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17495 });
17496
17497 assert_eq!(
17498 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17499 expected,
17500 "Active indent guide indices do not match"
17501 );
17502 }
17503
17504 assert_eq!(indent_guides, expected, "Indent guides do not match");
17505}
17506
17507fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17508 IndentGuide {
17509 buffer_id,
17510 start_row: MultiBufferRow(start_row),
17511 end_row: MultiBufferRow(end_row),
17512 depth,
17513 tab_size: 4,
17514 settings: IndentGuideSettings {
17515 enabled: true,
17516 line_width: 1,
17517 active_line_width: 1,
17518 ..Default::default()
17519 },
17520 }
17521}
17522
17523#[gpui::test]
17524async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17525 let (buffer_id, mut cx) = setup_indent_guides_editor(
17526 &"
17527 fn main() {
17528 let a = 1;
17529 }"
17530 .unindent(),
17531 cx,
17532 )
17533 .await;
17534
17535 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17536}
17537
17538#[gpui::test]
17539async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17540 let (buffer_id, mut cx) = setup_indent_guides_editor(
17541 &"
17542 fn main() {
17543 let a = 1;
17544 let b = 2;
17545 }"
17546 .unindent(),
17547 cx,
17548 )
17549 .await;
17550
17551 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17552}
17553
17554#[gpui::test]
17555async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17556 let (buffer_id, mut cx) = setup_indent_guides_editor(
17557 &"
17558 fn main() {
17559 let a = 1;
17560 if a == 3 {
17561 let b = 2;
17562 } else {
17563 let c = 3;
17564 }
17565 }"
17566 .unindent(),
17567 cx,
17568 )
17569 .await;
17570
17571 assert_indent_guides(
17572 0..8,
17573 vec![
17574 indent_guide(buffer_id, 1, 6, 0),
17575 indent_guide(buffer_id, 3, 3, 1),
17576 indent_guide(buffer_id, 5, 5, 1),
17577 ],
17578 None,
17579 &mut cx,
17580 );
17581}
17582
17583#[gpui::test]
17584async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17585 let (buffer_id, mut cx) = setup_indent_guides_editor(
17586 &"
17587 fn main() {
17588 let a = 1;
17589 let b = 2;
17590 let c = 3;
17591 }"
17592 .unindent(),
17593 cx,
17594 )
17595 .await;
17596
17597 assert_indent_guides(
17598 0..5,
17599 vec![
17600 indent_guide(buffer_id, 1, 3, 0),
17601 indent_guide(buffer_id, 2, 2, 1),
17602 ],
17603 None,
17604 &mut cx,
17605 );
17606}
17607
17608#[gpui::test]
17609async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17610 let (buffer_id, mut cx) = setup_indent_guides_editor(
17611 &"
17612 fn main() {
17613 let a = 1;
17614
17615 let c = 3;
17616 }"
17617 .unindent(),
17618 cx,
17619 )
17620 .await;
17621
17622 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17623}
17624
17625#[gpui::test]
17626async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17627 let (buffer_id, mut cx) = setup_indent_guides_editor(
17628 &"
17629 fn main() {
17630 let a = 1;
17631
17632 let c = 3;
17633
17634 if a == 3 {
17635 let b = 2;
17636 } else {
17637 let c = 3;
17638 }
17639 }"
17640 .unindent(),
17641 cx,
17642 )
17643 .await;
17644
17645 assert_indent_guides(
17646 0..11,
17647 vec![
17648 indent_guide(buffer_id, 1, 9, 0),
17649 indent_guide(buffer_id, 6, 6, 1),
17650 indent_guide(buffer_id, 8, 8, 1),
17651 ],
17652 None,
17653 &mut cx,
17654 );
17655}
17656
17657#[gpui::test]
17658async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17659 let (buffer_id, mut cx) = setup_indent_guides_editor(
17660 &"
17661 fn main() {
17662 let a = 1;
17663
17664 let c = 3;
17665
17666 if a == 3 {
17667 let b = 2;
17668 } else {
17669 let c = 3;
17670 }
17671 }"
17672 .unindent(),
17673 cx,
17674 )
17675 .await;
17676
17677 assert_indent_guides(
17678 1..11,
17679 vec![
17680 indent_guide(buffer_id, 1, 9, 0),
17681 indent_guide(buffer_id, 6, 6, 1),
17682 indent_guide(buffer_id, 8, 8, 1),
17683 ],
17684 None,
17685 &mut cx,
17686 );
17687}
17688
17689#[gpui::test]
17690async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17691 let (buffer_id, mut cx) = setup_indent_guides_editor(
17692 &"
17693 fn main() {
17694 let a = 1;
17695
17696 let c = 3;
17697
17698 if a == 3 {
17699 let b = 2;
17700 } else {
17701 let c = 3;
17702 }
17703 }"
17704 .unindent(),
17705 cx,
17706 )
17707 .await;
17708
17709 assert_indent_guides(
17710 1..10,
17711 vec![
17712 indent_guide(buffer_id, 1, 9, 0),
17713 indent_guide(buffer_id, 6, 6, 1),
17714 indent_guide(buffer_id, 8, 8, 1),
17715 ],
17716 None,
17717 &mut cx,
17718 );
17719}
17720
17721#[gpui::test]
17722async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17723 let (buffer_id, mut cx) = setup_indent_guides_editor(
17724 &"
17725 fn main() {
17726 if a {
17727 b(
17728 c,
17729 d,
17730 )
17731 } else {
17732 e(
17733 f
17734 )
17735 }
17736 }"
17737 .unindent(),
17738 cx,
17739 )
17740 .await;
17741
17742 assert_indent_guides(
17743 0..11,
17744 vec![
17745 indent_guide(buffer_id, 1, 10, 0),
17746 indent_guide(buffer_id, 2, 5, 1),
17747 indent_guide(buffer_id, 7, 9, 1),
17748 indent_guide(buffer_id, 3, 4, 2),
17749 indent_guide(buffer_id, 8, 8, 2),
17750 ],
17751 None,
17752 &mut cx,
17753 );
17754
17755 cx.update_editor(|editor, window, cx| {
17756 editor.fold_at(MultiBufferRow(2), window, cx);
17757 assert_eq!(
17758 editor.display_text(cx),
17759 "
17760 fn main() {
17761 if a {
17762 b(⋯
17763 )
17764 } else {
17765 e(
17766 f
17767 )
17768 }
17769 }"
17770 .unindent()
17771 );
17772 });
17773
17774 assert_indent_guides(
17775 0..11,
17776 vec![
17777 indent_guide(buffer_id, 1, 10, 0),
17778 indent_guide(buffer_id, 2, 5, 1),
17779 indent_guide(buffer_id, 7, 9, 1),
17780 indent_guide(buffer_id, 8, 8, 2),
17781 ],
17782 None,
17783 &mut cx,
17784 );
17785}
17786
17787#[gpui::test]
17788async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17789 let (buffer_id, mut cx) = setup_indent_guides_editor(
17790 &"
17791 block1
17792 block2
17793 block3
17794 block4
17795 block2
17796 block1
17797 block1"
17798 .unindent(),
17799 cx,
17800 )
17801 .await;
17802
17803 assert_indent_guides(
17804 1..10,
17805 vec![
17806 indent_guide(buffer_id, 1, 4, 0),
17807 indent_guide(buffer_id, 2, 3, 1),
17808 indent_guide(buffer_id, 3, 3, 2),
17809 ],
17810 None,
17811 &mut cx,
17812 );
17813}
17814
17815#[gpui::test]
17816async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17817 let (buffer_id, mut cx) = setup_indent_guides_editor(
17818 &"
17819 block1
17820 block2
17821 block3
17822
17823 block1
17824 block1"
17825 .unindent(),
17826 cx,
17827 )
17828 .await;
17829
17830 assert_indent_guides(
17831 0..6,
17832 vec![
17833 indent_guide(buffer_id, 1, 2, 0),
17834 indent_guide(buffer_id, 2, 2, 1),
17835 ],
17836 None,
17837 &mut cx,
17838 );
17839}
17840
17841#[gpui::test]
17842async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17843 let (buffer_id, mut cx) = setup_indent_guides_editor(
17844 &"
17845 function component() {
17846 \treturn (
17847 \t\t\t
17848 \t\t<div>
17849 \t\t\t<abc></abc>
17850 \t\t</div>
17851 \t)
17852 }"
17853 .unindent(),
17854 cx,
17855 )
17856 .await;
17857
17858 assert_indent_guides(
17859 0..8,
17860 vec![
17861 indent_guide(buffer_id, 1, 6, 0),
17862 indent_guide(buffer_id, 2, 5, 1),
17863 indent_guide(buffer_id, 4, 4, 2),
17864 ],
17865 None,
17866 &mut cx,
17867 );
17868}
17869
17870#[gpui::test]
17871async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17872 let (buffer_id, mut cx) = setup_indent_guides_editor(
17873 &"
17874 function component() {
17875 \treturn (
17876 \t
17877 \t\t<div>
17878 \t\t\t<abc></abc>
17879 \t\t</div>
17880 \t)
17881 }"
17882 .unindent(),
17883 cx,
17884 )
17885 .await;
17886
17887 assert_indent_guides(
17888 0..8,
17889 vec![
17890 indent_guide(buffer_id, 1, 6, 0),
17891 indent_guide(buffer_id, 2, 5, 1),
17892 indent_guide(buffer_id, 4, 4, 2),
17893 ],
17894 None,
17895 &mut cx,
17896 );
17897}
17898
17899#[gpui::test]
17900async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17901 let (buffer_id, mut cx) = setup_indent_guides_editor(
17902 &"
17903 block1
17904
17905
17906
17907 block2
17908 "
17909 .unindent(),
17910 cx,
17911 )
17912 .await;
17913
17914 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17915}
17916
17917#[gpui::test]
17918async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17919 let (buffer_id, mut cx) = setup_indent_guides_editor(
17920 &"
17921 def a:
17922 \tb = 3
17923 \tif True:
17924 \t\tc = 4
17925 \t\td = 5
17926 \tprint(b)
17927 "
17928 .unindent(),
17929 cx,
17930 )
17931 .await;
17932
17933 assert_indent_guides(
17934 0..6,
17935 vec![
17936 indent_guide(buffer_id, 1, 5, 0),
17937 indent_guide(buffer_id, 3, 4, 1),
17938 ],
17939 None,
17940 &mut cx,
17941 );
17942}
17943
17944#[gpui::test]
17945async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17946 let (buffer_id, mut cx) = setup_indent_guides_editor(
17947 &"
17948 fn main() {
17949 let a = 1;
17950 }"
17951 .unindent(),
17952 cx,
17953 )
17954 .await;
17955
17956 cx.update_editor(|editor, window, cx| {
17957 editor.change_selections(None, window, cx, |s| {
17958 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17959 });
17960 });
17961
17962 assert_indent_guides(
17963 0..3,
17964 vec![indent_guide(buffer_id, 1, 1, 0)],
17965 Some(vec![0]),
17966 &mut cx,
17967 );
17968}
17969
17970#[gpui::test]
17971async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17972 let (buffer_id, mut cx) = setup_indent_guides_editor(
17973 &"
17974 fn main() {
17975 if 1 == 2 {
17976 let a = 1;
17977 }
17978 }"
17979 .unindent(),
17980 cx,
17981 )
17982 .await;
17983
17984 cx.update_editor(|editor, window, cx| {
17985 editor.change_selections(None, window, cx, |s| {
17986 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17987 });
17988 });
17989
17990 assert_indent_guides(
17991 0..4,
17992 vec![
17993 indent_guide(buffer_id, 1, 3, 0),
17994 indent_guide(buffer_id, 2, 2, 1),
17995 ],
17996 Some(vec![1]),
17997 &mut cx,
17998 );
17999
18000 cx.update_editor(|editor, window, cx| {
18001 editor.change_selections(None, window, cx, |s| {
18002 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18003 });
18004 });
18005
18006 assert_indent_guides(
18007 0..4,
18008 vec![
18009 indent_guide(buffer_id, 1, 3, 0),
18010 indent_guide(buffer_id, 2, 2, 1),
18011 ],
18012 Some(vec![1]),
18013 &mut cx,
18014 );
18015
18016 cx.update_editor(|editor, window, cx| {
18017 editor.change_selections(None, window, cx, |s| {
18018 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18019 });
18020 });
18021
18022 assert_indent_guides(
18023 0..4,
18024 vec![
18025 indent_guide(buffer_id, 1, 3, 0),
18026 indent_guide(buffer_id, 2, 2, 1),
18027 ],
18028 Some(vec![0]),
18029 &mut cx,
18030 );
18031}
18032
18033#[gpui::test]
18034async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18035 let (buffer_id, mut cx) = setup_indent_guides_editor(
18036 &"
18037 fn main() {
18038 let a = 1;
18039
18040 let b = 2;
18041 }"
18042 .unindent(),
18043 cx,
18044 )
18045 .await;
18046
18047 cx.update_editor(|editor, window, cx| {
18048 editor.change_selections(None, window, cx, |s| {
18049 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18050 });
18051 });
18052
18053 assert_indent_guides(
18054 0..5,
18055 vec![indent_guide(buffer_id, 1, 3, 0)],
18056 Some(vec![0]),
18057 &mut cx,
18058 );
18059}
18060
18061#[gpui::test]
18062async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18063 let (buffer_id, mut cx) = setup_indent_guides_editor(
18064 &"
18065 def m:
18066 a = 1
18067 pass"
18068 .unindent(),
18069 cx,
18070 )
18071 .await;
18072
18073 cx.update_editor(|editor, window, cx| {
18074 editor.change_selections(None, window, cx, |s| {
18075 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18076 });
18077 });
18078
18079 assert_indent_guides(
18080 0..3,
18081 vec![indent_guide(buffer_id, 1, 2, 0)],
18082 Some(vec![0]),
18083 &mut cx,
18084 );
18085}
18086
18087#[gpui::test]
18088async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18089 init_test(cx, |_| {});
18090 let mut cx = EditorTestContext::new(cx).await;
18091 let text = indoc! {
18092 "
18093 impl A {
18094 fn b() {
18095 0;
18096 3;
18097 5;
18098 6;
18099 7;
18100 }
18101 }
18102 "
18103 };
18104 let base_text = indoc! {
18105 "
18106 impl A {
18107 fn b() {
18108 0;
18109 1;
18110 2;
18111 3;
18112 4;
18113 }
18114 fn c() {
18115 5;
18116 6;
18117 7;
18118 }
18119 }
18120 "
18121 };
18122
18123 cx.update_editor(|editor, window, cx| {
18124 editor.set_text(text, window, cx);
18125
18126 editor.buffer().update(cx, |multibuffer, cx| {
18127 let buffer = multibuffer.as_singleton().unwrap();
18128 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18129
18130 multibuffer.set_all_diff_hunks_expanded(cx);
18131 multibuffer.add_diff(diff, cx);
18132
18133 buffer.read(cx).remote_id()
18134 })
18135 });
18136 cx.run_until_parked();
18137
18138 cx.assert_state_with_diff(
18139 indoc! { "
18140 impl A {
18141 fn b() {
18142 0;
18143 - 1;
18144 - 2;
18145 3;
18146 - 4;
18147 - }
18148 - fn c() {
18149 5;
18150 6;
18151 7;
18152 }
18153 }
18154 ˇ"
18155 }
18156 .to_string(),
18157 );
18158
18159 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18160 editor
18161 .snapshot(window, cx)
18162 .buffer_snapshot
18163 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18164 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18165 .collect::<Vec<_>>()
18166 });
18167 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18168 assert_eq!(
18169 actual_guides,
18170 vec![
18171 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18172 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18173 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18174 ]
18175 );
18176}
18177
18178#[gpui::test]
18179async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18180 init_test(cx, |_| {});
18181 let mut cx = EditorTestContext::new(cx).await;
18182
18183 let diff_base = r#"
18184 a
18185 b
18186 c
18187 "#
18188 .unindent();
18189
18190 cx.set_state(
18191 &r#"
18192 ˇA
18193 b
18194 C
18195 "#
18196 .unindent(),
18197 );
18198 cx.set_head_text(&diff_base);
18199 cx.update_editor(|editor, window, cx| {
18200 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18201 });
18202 executor.run_until_parked();
18203
18204 let both_hunks_expanded = r#"
18205 - a
18206 + ˇA
18207 b
18208 - c
18209 + C
18210 "#
18211 .unindent();
18212
18213 cx.assert_state_with_diff(both_hunks_expanded.clone());
18214
18215 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18216 let snapshot = editor.snapshot(window, cx);
18217 let hunks = editor
18218 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18219 .collect::<Vec<_>>();
18220 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18221 let buffer_id = hunks[0].buffer_id;
18222 hunks
18223 .into_iter()
18224 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18225 .collect::<Vec<_>>()
18226 });
18227 assert_eq!(hunk_ranges.len(), 2);
18228
18229 cx.update_editor(|editor, _, cx| {
18230 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18231 });
18232 executor.run_until_parked();
18233
18234 let second_hunk_expanded = r#"
18235 ˇA
18236 b
18237 - c
18238 + C
18239 "#
18240 .unindent();
18241
18242 cx.assert_state_with_diff(second_hunk_expanded);
18243
18244 cx.update_editor(|editor, _, cx| {
18245 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18246 });
18247 executor.run_until_parked();
18248
18249 cx.assert_state_with_diff(both_hunks_expanded.clone());
18250
18251 cx.update_editor(|editor, _, cx| {
18252 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18253 });
18254 executor.run_until_parked();
18255
18256 let first_hunk_expanded = r#"
18257 - a
18258 + ˇA
18259 b
18260 C
18261 "#
18262 .unindent();
18263
18264 cx.assert_state_with_diff(first_hunk_expanded);
18265
18266 cx.update_editor(|editor, _, cx| {
18267 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18268 });
18269 executor.run_until_parked();
18270
18271 cx.assert_state_with_diff(both_hunks_expanded);
18272
18273 cx.set_state(
18274 &r#"
18275 ˇA
18276 b
18277 "#
18278 .unindent(),
18279 );
18280 cx.run_until_parked();
18281
18282 // TODO this cursor position seems bad
18283 cx.assert_state_with_diff(
18284 r#"
18285 - ˇa
18286 + A
18287 b
18288 "#
18289 .unindent(),
18290 );
18291
18292 cx.update_editor(|editor, window, cx| {
18293 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18294 });
18295
18296 cx.assert_state_with_diff(
18297 r#"
18298 - ˇa
18299 + A
18300 b
18301 - c
18302 "#
18303 .unindent(),
18304 );
18305
18306 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18307 let snapshot = editor.snapshot(window, cx);
18308 let hunks = editor
18309 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18310 .collect::<Vec<_>>();
18311 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18312 let buffer_id = hunks[0].buffer_id;
18313 hunks
18314 .into_iter()
18315 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18316 .collect::<Vec<_>>()
18317 });
18318 assert_eq!(hunk_ranges.len(), 2);
18319
18320 cx.update_editor(|editor, _, cx| {
18321 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18322 });
18323 executor.run_until_parked();
18324
18325 cx.assert_state_with_diff(
18326 r#"
18327 - ˇa
18328 + A
18329 b
18330 "#
18331 .unindent(),
18332 );
18333}
18334
18335#[gpui::test]
18336async fn test_toggle_deletion_hunk_at_start_of_file(
18337 executor: BackgroundExecutor,
18338 cx: &mut TestAppContext,
18339) {
18340 init_test(cx, |_| {});
18341 let mut cx = EditorTestContext::new(cx).await;
18342
18343 let diff_base = r#"
18344 a
18345 b
18346 c
18347 "#
18348 .unindent();
18349
18350 cx.set_state(
18351 &r#"
18352 ˇb
18353 c
18354 "#
18355 .unindent(),
18356 );
18357 cx.set_head_text(&diff_base);
18358 cx.update_editor(|editor, window, cx| {
18359 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18360 });
18361 executor.run_until_parked();
18362
18363 let hunk_expanded = r#"
18364 - a
18365 ˇb
18366 c
18367 "#
18368 .unindent();
18369
18370 cx.assert_state_with_diff(hunk_expanded.clone());
18371
18372 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18373 let snapshot = editor.snapshot(window, cx);
18374 let hunks = editor
18375 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18376 .collect::<Vec<_>>();
18377 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18378 let buffer_id = hunks[0].buffer_id;
18379 hunks
18380 .into_iter()
18381 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18382 .collect::<Vec<_>>()
18383 });
18384 assert_eq!(hunk_ranges.len(), 1);
18385
18386 cx.update_editor(|editor, _, cx| {
18387 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18388 });
18389 executor.run_until_parked();
18390
18391 let hunk_collapsed = r#"
18392 ˇb
18393 c
18394 "#
18395 .unindent();
18396
18397 cx.assert_state_with_diff(hunk_collapsed);
18398
18399 cx.update_editor(|editor, _, cx| {
18400 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18401 });
18402 executor.run_until_parked();
18403
18404 cx.assert_state_with_diff(hunk_expanded.clone());
18405}
18406
18407#[gpui::test]
18408async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18409 init_test(cx, |_| {});
18410
18411 let fs = FakeFs::new(cx.executor());
18412 fs.insert_tree(
18413 path!("/test"),
18414 json!({
18415 ".git": {},
18416 "file-1": "ONE\n",
18417 "file-2": "TWO\n",
18418 "file-3": "THREE\n",
18419 }),
18420 )
18421 .await;
18422
18423 fs.set_head_for_repo(
18424 path!("/test/.git").as_ref(),
18425 &[
18426 ("file-1".into(), "one\n".into()),
18427 ("file-2".into(), "two\n".into()),
18428 ("file-3".into(), "three\n".into()),
18429 ],
18430 "deadbeef",
18431 );
18432
18433 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18434 let mut buffers = vec![];
18435 for i in 1..=3 {
18436 let buffer = project
18437 .update(cx, |project, cx| {
18438 let path = format!(path!("/test/file-{}"), i);
18439 project.open_local_buffer(path, cx)
18440 })
18441 .await
18442 .unwrap();
18443 buffers.push(buffer);
18444 }
18445
18446 let multibuffer = cx.new(|cx| {
18447 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18448 multibuffer.set_all_diff_hunks_expanded(cx);
18449 for buffer in &buffers {
18450 let snapshot = buffer.read(cx).snapshot();
18451 multibuffer.set_excerpts_for_path(
18452 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18453 buffer.clone(),
18454 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18455 DEFAULT_MULTIBUFFER_CONTEXT,
18456 cx,
18457 );
18458 }
18459 multibuffer
18460 });
18461
18462 let editor = cx.add_window(|window, cx| {
18463 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18464 });
18465 cx.run_until_parked();
18466
18467 let snapshot = editor
18468 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18469 .unwrap();
18470 let hunks = snapshot
18471 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18472 .map(|hunk| match hunk {
18473 DisplayDiffHunk::Unfolded {
18474 display_row_range, ..
18475 } => display_row_range,
18476 DisplayDiffHunk::Folded { .. } => unreachable!(),
18477 })
18478 .collect::<Vec<_>>();
18479 assert_eq!(
18480 hunks,
18481 [
18482 DisplayRow(2)..DisplayRow(4),
18483 DisplayRow(7)..DisplayRow(9),
18484 DisplayRow(12)..DisplayRow(14),
18485 ]
18486 );
18487}
18488
18489#[gpui::test]
18490async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18491 init_test(cx, |_| {});
18492
18493 let mut cx = EditorTestContext::new(cx).await;
18494 cx.set_head_text(indoc! { "
18495 one
18496 two
18497 three
18498 four
18499 five
18500 "
18501 });
18502 cx.set_index_text(indoc! { "
18503 one
18504 two
18505 three
18506 four
18507 five
18508 "
18509 });
18510 cx.set_state(indoc! {"
18511 one
18512 TWO
18513 ˇTHREE
18514 FOUR
18515 five
18516 "});
18517 cx.run_until_parked();
18518 cx.update_editor(|editor, window, cx| {
18519 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18520 });
18521 cx.run_until_parked();
18522 cx.assert_index_text(Some(indoc! {"
18523 one
18524 TWO
18525 THREE
18526 FOUR
18527 five
18528 "}));
18529 cx.set_state(indoc! { "
18530 one
18531 TWO
18532 ˇTHREE-HUNDRED
18533 FOUR
18534 five
18535 "});
18536 cx.run_until_parked();
18537 cx.update_editor(|editor, window, cx| {
18538 let snapshot = editor.snapshot(window, cx);
18539 let hunks = editor
18540 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18541 .collect::<Vec<_>>();
18542 assert_eq!(hunks.len(), 1);
18543 assert_eq!(
18544 hunks[0].status(),
18545 DiffHunkStatus {
18546 kind: DiffHunkStatusKind::Modified,
18547 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18548 }
18549 );
18550
18551 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18552 });
18553 cx.run_until_parked();
18554 cx.assert_index_text(Some(indoc! {"
18555 one
18556 TWO
18557 THREE-HUNDRED
18558 FOUR
18559 five
18560 "}));
18561}
18562
18563#[gpui::test]
18564fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18565 init_test(cx, |_| {});
18566
18567 let editor = cx.add_window(|window, cx| {
18568 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18569 build_editor(buffer, window, cx)
18570 });
18571
18572 let render_args = Arc::new(Mutex::new(None));
18573 let snapshot = editor
18574 .update(cx, |editor, window, cx| {
18575 let snapshot = editor.buffer().read(cx).snapshot(cx);
18576 let range =
18577 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18578
18579 struct RenderArgs {
18580 row: MultiBufferRow,
18581 folded: bool,
18582 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18583 }
18584
18585 let crease = Crease::inline(
18586 range,
18587 FoldPlaceholder::test(),
18588 {
18589 let toggle_callback = render_args.clone();
18590 move |row, folded, callback, _window, _cx| {
18591 *toggle_callback.lock() = Some(RenderArgs {
18592 row,
18593 folded,
18594 callback,
18595 });
18596 div()
18597 }
18598 },
18599 |_row, _folded, _window, _cx| div(),
18600 );
18601
18602 editor.insert_creases(Some(crease), cx);
18603 let snapshot = editor.snapshot(window, cx);
18604 let _div = snapshot.render_crease_toggle(
18605 MultiBufferRow(1),
18606 false,
18607 cx.entity().clone(),
18608 window,
18609 cx,
18610 );
18611 snapshot
18612 })
18613 .unwrap();
18614
18615 let render_args = render_args.lock().take().unwrap();
18616 assert_eq!(render_args.row, MultiBufferRow(1));
18617 assert!(!render_args.folded);
18618 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18619
18620 cx.update_window(*editor, |_, window, cx| {
18621 (render_args.callback)(true, window, cx)
18622 })
18623 .unwrap();
18624 let snapshot = editor
18625 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18626 .unwrap();
18627 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18628
18629 cx.update_window(*editor, |_, window, cx| {
18630 (render_args.callback)(false, window, cx)
18631 })
18632 .unwrap();
18633 let snapshot = editor
18634 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18635 .unwrap();
18636 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18637}
18638
18639#[gpui::test]
18640async fn test_input_text(cx: &mut TestAppContext) {
18641 init_test(cx, |_| {});
18642 let mut cx = EditorTestContext::new(cx).await;
18643
18644 cx.set_state(
18645 &r#"ˇone
18646 two
18647
18648 three
18649 fourˇ
18650 five
18651
18652 siˇx"#
18653 .unindent(),
18654 );
18655
18656 cx.dispatch_action(HandleInput(String::new()));
18657 cx.assert_editor_state(
18658 &r#"ˇone
18659 two
18660
18661 three
18662 fourˇ
18663 five
18664
18665 siˇx"#
18666 .unindent(),
18667 );
18668
18669 cx.dispatch_action(HandleInput("AAAA".to_string()));
18670 cx.assert_editor_state(
18671 &r#"AAAAˇone
18672 two
18673
18674 three
18675 fourAAAAˇ
18676 five
18677
18678 siAAAAˇx"#
18679 .unindent(),
18680 );
18681}
18682
18683#[gpui::test]
18684async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18685 init_test(cx, |_| {});
18686
18687 let mut cx = EditorTestContext::new(cx).await;
18688 cx.set_state(
18689 r#"let foo = 1;
18690let foo = 2;
18691let foo = 3;
18692let fooˇ = 4;
18693let foo = 5;
18694let foo = 6;
18695let foo = 7;
18696let foo = 8;
18697let foo = 9;
18698let foo = 10;
18699let foo = 11;
18700let foo = 12;
18701let foo = 13;
18702let foo = 14;
18703let foo = 15;"#,
18704 );
18705
18706 cx.update_editor(|e, window, cx| {
18707 assert_eq!(
18708 e.next_scroll_position,
18709 NextScrollCursorCenterTopBottom::Center,
18710 "Default next scroll direction is center",
18711 );
18712
18713 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18714 assert_eq!(
18715 e.next_scroll_position,
18716 NextScrollCursorCenterTopBottom::Top,
18717 "After center, next scroll direction should be top",
18718 );
18719
18720 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18721 assert_eq!(
18722 e.next_scroll_position,
18723 NextScrollCursorCenterTopBottom::Bottom,
18724 "After top, next scroll direction should be bottom",
18725 );
18726
18727 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18728 assert_eq!(
18729 e.next_scroll_position,
18730 NextScrollCursorCenterTopBottom::Center,
18731 "After bottom, scrolling should start over",
18732 );
18733
18734 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18735 assert_eq!(
18736 e.next_scroll_position,
18737 NextScrollCursorCenterTopBottom::Top,
18738 "Scrolling continues if retriggered fast enough"
18739 );
18740 });
18741
18742 cx.executor()
18743 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18744 cx.executor().run_until_parked();
18745 cx.update_editor(|e, _, _| {
18746 assert_eq!(
18747 e.next_scroll_position,
18748 NextScrollCursorCenterTopBottom::Center,
18749 "If scrolling is not triggered fast enough, it should reset"
18750 );
18751 });
18752}
18753
18754#[gpui::test]
18755async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18756 init_test(cx, |_| {});
18757 let mut cx = EditorLspTestContext::new_rust(
18758 lsp::ServerCapabilities {
18759 definition_provider: Some(lsp::OneOf::Left(true)),
18760 references_provider: Some(lsp::OneOf::Left(true)),
18761 ..lsp::ServerCapabilities::default()
18762 },
18763 cx,
18764 )
18765 .await;
18766
18767 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18768 let go_to_definition = cx
18769 .lsp
18770 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18771 move |params, _| async move {
18772 if empty_go_to_definition {
18773 Ok(None)
18774 } else {
18775 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18776 uri: params.text_document_position_params.text_document.uri,
18777 range: lsp::Range::new(
18778 lsp::Position::new(4, 3),
18779 lsp::Position::new(4, 6),
18780 ),
18781 })))
18782 }
18783 },
18784 );
18785 let references = cx
18786 .lsp
18787 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18788 Ok(Some(vec![lsp::Location {
18789 uri: params.text_document_position.text_document.uri,
18790 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18791 }]))
18792 });
18793 (go_to_definition, references)
18794 };
18795
18796 cx.set_state(
18797 &r#"fn one() {
18798 let mut a = ˇtwo();
18799 }
18800
18801 fn two() {}"#
18802 .unindent(),
18803 );
18804 set_up_lsp_handlers(false, &mut cx);
18805 let navigated = cx
18806 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18807 .await
18808 .expect("Failed to navigate to definition");
18809 assert_eq!(
18810 navigated,
18811 Navigated::Yes,
18812 "Should have navigated to definition from the GetDefinition response"
18813 );
18814 cx.assert_editor_state(
18815 &r#"fn one() {
18816 let mut a = two();
18817 }
18818
18819 fn «twoˇ»() {}"#
18820 .unindent(),
18821 );
18822
18823 let editors = cx.update_workspace(|workspace, _, cx| {
18824 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18825 });
18826 cx.update_editor(|_, _, test_editor_cx| {
18827 assert_eq!(
18828 editors.len(),
18829 1,
18830 "Initially, only one, test, editor should be open in the workspace"
18831 );
18832 assert_eq!(
18833 test_editor_cx.entity(),
18834 editors.last().expect("Asserted len is 1").clone()
18835 );
18836 });
18837
18838 set_up_lsp_handlers(true, &mut cx);
18839 let navigated = cx
18840 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18841 .await
18842 .expect("Failed to navigate to lookup references");
18843 assert_eq!(
18844 navigated,
18845 Navigated::Yes,
18846 "Should have navigated to references as a fallback after empty GoToDefinition response"
18847 );
18848 // We should not change the selections in the existing file,
18849 // if opening another milti buffer with the references
18850 cx.assert_editor_state(
18851 &r#"fn one() {
18852 let mut a = two();
18853 }
18854
18855 fn «twoˇ»() {}"#
18856 .unindent(),
18857 );
18858 let editors = cx.update_workspace(|workspace, _, cx| {
18859 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18860 });
18861 cx.update_editor(|_, _, test_editor_cx| {
18862 assert_eq!(
18863 editors.len(),
18864 2,
18865 "After falling back to references search, we open a new editor with the results"
18866 );
18867 let references_fallback_text = editors
18868 .into_iter()
18869 .find(|new_editor| *new_editor != test_editor_cx.entity())
18870 .expect("Should have one non-test editor now")
18871 .read(test_editor_cx)
18872 .text(test_editor_cx);
18873 assert_eq!(
18874 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18875 "Should use the range from the references response and not the GoToDefinition one"
18876 );
18877 });
18878}
18879
18880#[gpui::test]
18881async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18882 init_test(cx, |_| {});
18883 cx.update(|cx| {
18884 let mut editor_settings = EditorSettings::get_global(cx).clone();
18885 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18886 EditorSettings::override_global(editor_settings, cx);
18887 });
18888 let mut cx = EditorLspTestContext::new_rust(
18889 lsp::ServerCapabilities {
18890 definition_provider: Some(lsp::OneOf::Left(true)),
18891 references_provider: Some(lsp::OneOf::Left(true)),
18892 ..lsp::ServerCapabilities::default()
18893 },
18894 cx,
18895 )
18896 .await;
18897 let original_state = r#"fn one() {
18898 let mut a = ˇtwo();
18899 }
18900
18901 fn two() {}"#
18902 .unindent();
18903 cx.set_state(&original_state);
18904
18905 let mut go_to_definition = cx
18906 .lsp
18907 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18908 move |_, _| async move { Ok(None) },
18909 );
18910 let _references = cx
18911 .lsp
18912 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18913 panic!("Should not call for references with no go to definition fallback")
18914 });
18915
18916 let navigated = cx
18917 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18918 .await
18919 .expect("Failed to navigate to lookup references");
18920 go_to_definition
18921 .next()
18922 .await
18923 .expect("Should have called the go_to_definition handler");
18924
18925 assert_eq!(
18926 navigated,
18927 Navigated::No,
18928 "Should have navigated to references as a fallback after empty GoToDefinition response"
18929 );
18930 cx.assert_editor_state(&original_state);
18931 let editors = cx.update_workspace(|workspace, _, cx| {
18932 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18933 });
18934 cx.update_editor(|_, _, _| {
18935 assert_eq!(
18936 editors.len(),
18937 1,
18938 "After unsuccessful fallback, no other editor should have been opened"
18939 );
18940 });
18941}
18942
18943#[gpui::test]
18944async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18945 init_test(cx, |_| {});
18946
18947 let language = Arc::new(Language::new(
18948 LanguageConfig::default(),
18949 Some(tree_sitter_rust::LANGUAGE.into()),
18950 ));
18951
18952 let text = r#"
18953 #[cfg(test)]
18954 mod tests() {
18955 #[test]
18956 fn runnable_1() {
18957 let a = 1;
18958 }
18959
18960 #[test]
18961 fn runnable_2() {
18962 let a = 1;
18963 let b = 2;
18964 }
18965 }
18966 "#
18967 .unindent();
18968
18969 let fs = FakeFs::new(cx.executor());
18970 fs.insert_file("/file.rs", Default::default()).await;
18971
18972 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18973 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18974 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18975 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18976 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18977
18978 let editor = cx.new_window_entity(|window, cx| {
18979 Editor::new(
18980 EditorMode::full(),
18981 multi_buffer,
18982 Some(project.clone()),
18983 window,
18984 cx,
18985 )
18986 });
18987
18988 editor.update_in(cx, |editor, window, cx| {
18989 let snapshot = editor.buffer().read(cx).snapshot(cx);
18990 editor.tasks.insert(
18991 (buffer.read(cx).remote_id(), 3),
18992 RunnableTasks {
18993 templates: vec![],
18994 offset: snapshot.anchor_before(43),
18995 column: 0,
18996 extra_variables: HashMap::default(),
18997 context_range: BufferOffset(43)..BufferOffset(85),
18998 },
18999 );
19000 editor.tasks.insert(
19001 (buffer.read(cx).remote_id(), 8),
19002 RunnableTasks {
19003 templates: vec![],
19004 offset: snapshot.anchor_before(86),
19005 column: 0,
19006 extra_variables: HashMap::default(),
19007 context_range: BufferOffset(86)..BufferOffset(191),
19008 },
19009 );
19010
19011 // Test finding task when cursor is inside function body
19012 editor.change_selections(None, window, cx, |s| {
19013 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19014 });
19015 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19016 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19017
19018 // Test finding task when cursor is on function name
19019 editor.change_selections(None, window, cx, |s| {
19020 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19021 });
19022 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19023 assert_eq!(row, 8, "Should find task when cursor is on function name");
19024 });
19025}
19026
19027#[gpui::test]
19028async fn test_folding_buffers(cx: &mut TestAppContext) {
19029 init_test(cx, |_| {});
19030
19031 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19032 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19033 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19034
19035 let fs = FakeFs::new(cx.executor());
19036 fs.insert_tree(
19037 path!("/a"),
19038 json!({
19039 "first.rs": sample_text_1,
19040 "second.rs": sample_text_2,
19041 "third.rs": sample_text_3,
19042 }),
19043 )
19044 .await;
19045 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19046 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19047 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19048 let worktree = project.update(cx, |project, cx| {
19049 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19050 assert_eq!(worktrees.len(), 1);
19051 worktrees.pop().unwrap()
19052 });
19053 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19054
19055 let buffer_1 = project
19056 .update(cx, |project, cx| {
19057 project.open_buffer((worktree_id, "first.rs"), cx)
19058 })
19059 .await
19060 .unwrap();
19061 let buffer_2 = project
19062 .update(cx, |project, cx| {
19063 project.open_buffer((worktree_id, "second.rs"), cx)
19064 })
19065 .await
19066 .unwrap();
19067 let buffer_3 = project
19068 .update(cx, |project, cx| {
19069 project.open_buffer((worktree_id, "third.rs"), cx)
19070 })
19071 .await
19072 .unwrap();
19073
19074 let multi_buffer = cx.new(|cx| {
19075 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19076 multi_buffer.push_excerpts(
19077 buffer_1.clone(),
19078 [
19079 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19080 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19081 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19082 ],
19083 cx,
19084 );
19085 multi_buffer.push_excerpts(
19086 buffer_2.clone(),
19087 [
19088 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19089 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19090 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19091 ],
19092 cx,
19093 );
19094 multi_buffer.push_excerpts(
19095 buffer_3.clone(),
19096 [
19097 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19098 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19099 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19100 ],
19101 cx,
19102 );
19103 multi_buffer
19104 });
19105 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19106 Editor::new(
19107 EditorMode::full(),
19108 multi_buffer.clone(),
19109 Some(project.clone()),
19110 window,
19111 cx,
19112 )
19113 });
19114
19115 assert_eq!(
19116 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19117 "\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",
19118 );
19119
19120 multi_buffer_editor.update(cx, |editor, cx| {
19121 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19122 });
19123 assert_eq!(
19124 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19125 "\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",
19126 "After folding the first buffer, its text should not be displayed"
19127 );
19128
19129 multi_buffer_editor.update(cx, |editor, cx| {
19130 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19131 });
19132 assert_eq!(
19133 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19134 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19135 "After folding the second buffer, its text should not be displayed"
19136 );
19137
19138 multi_buffer_editor.update(cx, |editor, cx| {
19139 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19140 });
19141 assert_eq!(
19142 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19143 "\n\n\n\n\n",
19144 "After folding the third buffer, its text should not be displayed"
19145 );
19146
19147 // Emulate selection inside the fold logic, that should work
19148 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19149 editor
19150 .snapshot(window, cx)
19151 .next_line_boundary(Point::new(0, 4));
19152 });
19153
19154 multi_buffer_editor.update(cx, |editor, cx| {
19155 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19156 });
19157 assert_eq!(
19158 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19159 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19160 "After unfolding the second buffer, its text should be displayed"
19161 );
19162
19163 // Typing inside of buffer 1 causes that buffer to be unfolded.
19164 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19165 assert_eq!(
19166 multi_buffer
19167 .read(cx)
19168 .snapshot(cx)
19169 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19170 .collect::<String>(),
19171 "bbbb"
19172 );
19173 editor.change_selections(None, window, cx, |selections| {
19174 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19175 });
19176 editor.handle_input("B", window, cx);
19177 });
19178
19179 assert_eq!(
19180 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19181 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19182 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19183 );
19184
19185 multi_buffer_editor.update(cx, |editor, cx| {
19186 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19187 });
19188 assert_eq!(
19189 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19190 "\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",
19191 "After unfolding the all buffers, all original text should be displayed"
19192 );
19193}
19194
19195#[gpui::test]
19196async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19197 init_test(cx, |_| {});
19198
19199 let sample_text_1 = "1111\n2222\n3333".to_string();
19200 let sample_text_2 = "4444\n5555\n6666".to_string();
19201 let sample_text_3 = "7777\n8888\n9999".to_string();
19202
19203 let fs = FakeFs::new(cx.executor());
19204 fs.insert_tree(
19205 path!("/a"),
19206 json!({
19207 "first.rs": sample_text_1,
19208 "second.rs": sample_text_2,
19209 "third.rs": sample_text_3,
19210 }),
19211 )
19212 .await;
19213 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19214 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19215 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19216 let worktree = project.update(cx, |project, cx| {
19217 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19218 assert_eq!(worktrees.len(), 1);
19219 worktrees.pop().unwrap()
19220 });
19221 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19222
19223 let buffer_1 = project
19224 .update(cx, |project, cx| {
19225 project.open_buffer((worktree_id, "first.rs"), cx)
19226 })
19227 .await
19228 .unwrap();
19229 let buffer_2 = project
19230 .update(cx, |project, cx| {
19231 project.open_buffer((worktree_id, "second.rs"), cx)
19232 })
19233 .await
19234 .unwrap();
19235 let buffer_3 = project
19236 .update(cx, |project, cx| {
19237 project.open_buffer((worktree_id, "third.rs"), cx)
19238 })
19239 .await
19240 .unwrap();
19241
19242 let multi_buffer = cx.new(|cx| {
19243 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19244 multi_buffer.push_excerpts(
19245 buffer_1.clone(),
19246 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19247 cx,
19248 );
19249 multi_buffer.push_excerpts(
19250 buffer_2.clone(),
19251 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19252 cx,
19253 );
19254 multi_buffer.push_excerpts(
19255 buffer_3.clone(),
19256 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19257 cx,
19258 );
19259 multi_buffer
19260 });
19261
19262 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19263 Editor::new(
19264 EditorMode::full(),
19265 multi_buffer,
19266 Some(project.clone()),
19267 window,
19268 cx,
19269 )
19270 });
19271
19272 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19273 assert_eq!(
19274 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19275 full_text,
19276 );
19277
19278 multi_buffer_editor.update(cx, |editor, cx| {
19279 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19280 });
19281 assert_eq!(
19282 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19283 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19284 "After folding the first buffer, its text should not be displayed"
19285 );
19286
19287 multi_buffer_editor.update(cx, |editor, cx| {
19288 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19289 });
19290
19291 assert_eq!(
19292 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19293 "\n\n\n\n\n\n7777\n8888\n9999",
19294 "After folding the second buffer, its text should not be displayed"
19295 );
19296
19297 multi_buffer_editor.update(cx, |editor, cx| {
19298 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19299 });
19300 assert_eq!(
19301 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19302 "\n\n\n\n\n",
19303 "After folding the third buffer, its text should not be displayed"
19304 );
19305
19306 multi_buffer_editor.update(cx, |editor, cx| {
19307 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19308 });
19309 assert_eq!(
19310 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19311 "\n\n\n\n4444\n5555\n6666\n\n",
19312 "After unfolding the second buffer, its text should be displayed"
19313 );
19314
19315 multi_buffer_editor.update(cx, |editor, cx| {
19316 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19317 });
19318 assert_eq!(
19319 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19320 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19321 "After unfolding the first buffer, its text should be displayed"
19322 );
19323
19324 multi_buffer_editor.update(cx, |editor, cx| {
19325 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19326 });
19327 assert_eq!(
19328 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19329 full_text,
19330 "After unfolding all buffers, all original text should be displayed"
19331 );
19332}
19333
19334#[gpui::test]
19335async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19336 init_test(cx, |_| {});
19337
19338 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19339
19340 let fs = FakeFs::new(cx.executor());
19341 fs.insert_tree(
19342 path!("/a"),
19343 json!({
19344 "main.rs": sample_text,
19345 }),
19346 )
19347 .await;
19348 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19349 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19350 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19351 let worktree = project.update(cx, |project, cx| {
19352 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19353 assert_eq!(worktrees.len(), 1);
19354 worktrees.pop().unwrap()
19355 });
19356 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19357
19358 let buffer_1 = project
19359 .update(cx, |project, cx| {
19360 project.open_buffer((worktree_id, "main.rs"), cx)
19361 })
19362 .await
19363 .unwrap();
19364
19365 let multi_buffer = cx.new(|cx| {
19366 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19367 multi_buffer.push_excerpts(
19368 buffer_1.clone(),
19369 [ExcerptRange::new(
19370 Point::new(0, 0)
19371 ..Point::new(
19372 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19373 0,
19374 ),
19375 )],
19376 cx,
19377 );
19378 multi_buffer
19379 });
19380 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19381 Editor::new(
19382 EditorMode::full(),
19383 multi_buffer,
19384 Some(project.clone()),
19385 window,
19386 cx,
19387 )
19388 });
19389
19390 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19391 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19392 enum TestHighlight {}
19393 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19394 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19395 editor.highlight_text::<TestHighlight>(
19396 vec![highlight_range.clone()],
19397 HighlightStyle::color(Hsla::green()),
19398 cx,
19399 );
19400 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19401 });
19402
19403 let full_text = format!("\n\n{sample_text}");
19404 assert_eq!(
19405 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19406 full_text,
19407 );
19408}
19409
19410#[gpui::test]
19411async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19412 init_test(cx, |_| {});
19413 cx.update(|cx| {
19414 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19415 "keymaps/default-linux.json",
19416 cx,
19417 )
19418 .unwrap();
19419 cx.bind_keys(default_key_bindings);
19420 });
19421
19422 let (editor, cx) = cx.add_window_view(|window, cx| {
19423 let multi_buffer = MultiBuffer::build_multi(
19424 [
19425 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19426 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19427 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19428 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19429 ],
19430 cx,
19431 );
19432 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19433
19434 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19435 // fold all but the second buffer, so that we test navigating between two
19436 // adjacent folded buffers, as well as folded buffers at the start and
19437 // end the multibuffer
19438 editor.fold_buffer(buffer_ids[0], cx);
19439 editor.fold_buffer(buffer_ids[2], cx);
19440 editor.fold_buffer(buffer_ids[3], cx);
19441
19442 editor
19443 });
19444 cx.simulate_resize(size(px(1000.), px(1000.)));
19445
19446 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19447 cx.assert_excerpts_with_selections(indoc! {"
19448 [EXCERPT]
19449 ˇ[FOLDED]
19450 [EXCERPT]
19451 a1
19452 b1
19453 [EXCERPT]
19454 [FOLDED]
19455 [EXCERPT]
19456 [FOLDED]
19457 "
19458 });
19459 cx.simulate_keystroke("down");
19460 cx.assert_excerpts_with_selections(indoc! {"
19461 [EXCERPT]
19462 [FOLDED]
19463 [EXCERPT]
19464 ˇa1
19465 b1
19466 [EXCERPT]
19467 [FOLDED]
19468 [EXCERPT]
19469 [FOLDED]
19470 "
19471 });
19472 cx.simulate_keystroke("down");
19473 cx.assert_excerpts_with_selections(indoc! {"
19474 [EXCERPT]
19475 [FOLDED]
19476 [EXCERPT]
19477 a1
19478 ˇb1
19479 [EXCERPT]
19480 [FOLDED]
19481 [EXCERPT]
19482 [FOLDED]
19483 "
19484 });
19485 cx.simulate_keystroke("down");
19486 cx.assert_excerpts_with_selections(indoc! {"
19487 [EXCERPT]
19488 [FOLDED]
19489 [EXCERPT]
19490 a1
19491 b1
19492 ˇ[EXCERPT]
19493 [FOLDED]
19494 [EXCERPT]
19495 [FOLDED]
19496 "
19497 });
19498 cx.simulate_keystroke("down");
19499 cx.assert_excerpts_with_selections(indoc! {"
19500 [EXCERPT]
19501 [FOLDED]
19502 [EXCERPT]
19503 a1
19504 b1
19505 [EXCERPT]
19506 ˇ[FOLDED]
19507 [EXCERPT]
19508 [FOLDED]
19509 "
19510 });
19511 for _ in 0..5 {
19512 cx.simulate_keystroke("down");
19513 cx.assert_excerpts_with_selections(indoc! {"
19514 [EXCERPT]
19515 [FOLDED]
19516 [EXCERPT]
19517 a1
19518 b1
19519 [EXCERPT]
19520 [FOLDED]
19521 [EXCERPT]
19522 ˇ[FOLDED]
19523 "
19524 });
19525 }
19526
19527 cx.simulate_keystroke("up");
19528 cx.assert_excerpts_with_selections(indoc! {"
19529 [EXCERPT]
19530 [FOLDED]
19531 [EXCERPT]
19532 a1
19533 b1
19534 [EXCERPT]
19535 ˇ[FOLDED]
19536 [EXCERPT]
19537 [FOLDED]
19538 "
19539 });
19540 cx.simulate_keystroke("up");
19541 cx.assert_excerpts_with_selections(indoc! {"
19542 [EXCERPT]
19543 [FOLDED]
19544 [EXCERPT]
19545 a1
19546 b1
19547 ˇ[EXCERPT]
19548 [FOLDED]
19549 [EXCERPT]
19550 [FOLDED]
19551 "
19552 });
19553 cx.simulate_keystroke("up");
19554 cx.assert_excerpts_with_selections(indoc! {"
19555 [EXCERPT]
19556 [FOLDED]
19557 [EXCERPT]
19558 a1
19559 ˇb1
19560 [EXCERPT]
19561 [FOLDED]
19562 [EXCERPT]
19563 [FOLDED]
19564 "
19565 });
19566 cx.simulate_keystroke("up");
19567 cx.assert_excerpts_with_selections(indoc! {"
19568 [EXCERPT]
19569 [FOLDED]
19570 [EXCERPT]
19571 ˇa1
19572 b1
19573 [EXCERPT]
19574 [FOLDED]
19575 [EXCERPT]
19576 [FOLDED]
19577 "
19578 });
19579 for _ in 0..5 {
19580 cx.simulate_keystroke("up");
19581 cx.assert_excerpts_with_selections(indoc! {"
19582 [EXCERPT]
19583 ˇ[FOLDED]
19584 [EXCERPT]
19585 a1
19586 b1
19587 [EXCERPT]
19588 [FOLDED]
19589 [EXCERPT]
19590 [FOLDED]
19591 "
19592 });
19593 }
19594}
19595
19596#[gpui::test]
19597async fn test_inline_completion_text(cx: &mut TestAppContext) {
19598 init_test(cx, |_| {});
19599
19600 // Simple insertion
19601 assert_highlighted_edits(
19602 "Hello, world!",
19603 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19604 true,
19605 cx,
19606 |highlighted_edits, cx| {
19607 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19608 assert_eq!(highlighted_edits.highlights.len(), 1);
19609 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19610 assert_eq!(
19611 highlighted_edits.highlights[0].1.background_color,
19612 Some(cx.theme().status().created_background)
19613 );
19614 },
19615 )
19616 .await;
19617
19618 // Replacement
19619 assert_highlighted_edits(
19620 "This is a test.",
19621 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19622 false,
19623 cx,
19624 |highlighted_edits, cx| {
19625 assert_eq!(highlighted_edits.text, "That is a test.");
19626 assert_eq!(highlighted_edits.highlights.len(), 1);
19627 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19628 assert_eq!(
19629 highlighted_edits.highlights[0].1.background_color,
19630 Some(cx.theme().status().created_background)
19631 );
19632 },
19633 )
19634 .await;
19635
19636 // Multiple edits
19637 assert_highlighted_edits(
19638 "Hello, world!",
19639 vec![
19640 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19641 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19642 ],
19643 false,
19644 cx,
19645 |highlighted_edits, cx| {
19646 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19647 assert_eq!(highlighted_edits.highlights.len(), 2);
19648 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19649 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19650 assert_eq!(
19651 highlighted_edits.highlights[0].1.background_color,
19652 Some(cx.theme().status().created_background)
19653 );
19654 assert_eq!(
19655 highlighted_edits.highlights[1].1.background_color,
19656 Some(cx.theme().status().created_background)
19657 );
19658 },
19659 )
19660 .await;
19661
19662 // Multiple lines with edits
19663 assert_highlighted_edits(
19664 "First line\nSecond line\nThird line\nFourth line",
19665 vec![
19666 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19667 (
19668 Point::new(2, 0)..Point::new(2, 10),
19669 "New third line".to_string(),
19670 ),
19671 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19672 ],
19673 false,
19674 cx,
19675 |highlighted_edits, cx| {
19676 assert_eq!(
19677 highlighted_edits.text,
19678 "Second modified\nNew third line\nFourth updated line"
19679 );
19680 assert_eq!(highlighted_edits.highlights.len(), 3);
19681 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19682 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19683 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19684 for highlight in &highlighted_edits.highlights {
19685 assert_eq!(
19686 highlight.1.background_color,
19687 Some(cx.theme().status().created_background)
19688 );
19689 }
19690 },
19691 )
19692 .await;
19693}
19694
19695#[gpui::test]
19696async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19697 init_test(cx, |_| {});
19698
19699 // Deletion
19700 assert_highlighted_edits(
19701 "Hello, world!",
19702 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19703 true,
19704 cx,
19705 |highlighted_edits, cx| {
19706 assert_eq!(highlighted_edits.text, "Hello, world!");
19707 assert_eq!(highlighted_edits.highlights.len(), 1);
19708 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19709 assert_eq!(
19710 highlighted_edits.highlights[0].1.background_color,
19711 Some(cx.theme().status().deleted_background)
19712 );
19713 },
19714 )
19715 .await;
19716
19717 // Insertion
19718 assert_highlighted_edits(
19719 "Hello, world!",
19720 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19721 true,
19722 cx,
19723 |highlighted_edits, cx| {
19724 assert_eq!(highlighted_edits.highlights.len(), 1);
19725 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19726 assert_eq!(
19727 highlighted_edits.highlights[0].1.background_color,
19728 Some(cx.theme().status().created_background)
19729 );
19730 },
19731 )
19732 .await;
19733}
19734
19735async fn assert_highlighted_edits(
19736 text: &str,
19737 edits: Vec<(Range<Point>, String)>,
19738 include_deletions: bool,
19739 cx: &mut TestAppContext,
19740 assertion_fn: impl Fn(HighlightedText, &App),
19741) {
19742 let window = cx.add_window(|window, cx| {
19743 let buffer = MultiBuffer::build_simple(text, cx);
19744 Editor::new(EditorMode::full(), buffer, None, window, cx)
19745 });
19746 let cx = &mut VisualTestContext::from_window(*window, cx);
19747
19748 let (buffer, snapshot) = window
19749 .update(cx, |editor, _window, cx| {
19750 (
19751 editor.buffer().clone(),
19752 editor.buffer().read(cx).snapshot(cx),
19753 )
19754 })
19755 .unwrap();
19756
19757 let edits = edits
19758 .into_iter()
19759 .map(|(range, edit)| {
19760 (
19761 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19762 edit,
19763 )
19764 })
19765 .collect::<Vec<_>>();
19766
19767 let text_anchor_edits = edits
19768 .clone()
19769 .into_iter()
19770 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19771 .collect::<Vec<_>>();
19772
19773 let edit_preview = window
19774 .update(cx, |_, _window, cx| {
19775 buffer
19776 .read(cx)
19777 .as_singleton()
19778 .unwrap()
19779 .read(cx)
19780 .preview_edits(text_anchor_edits.into(), cx)
19781 })
19782 .unwrap()
19783 .await;
19784
19785 cx.update(|_window, cx| {
19786 let highlighted_edits = inline_completion_edit_text(
19787 &snapshot.as_singleton().unwrap().2,
19788 &edits,
19789 &edit_preview,
19790 include_deletions,
19791 cx,
19792 );
19793 assertion_fn(highlighted_edits, cx)
19794 });
19795}
19796
19797#[track_caller]
19798fn assert_breakpoint(
19799 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19800 path: &Arc<Path>,
19801 expected: Vec<(u32, Breakpoint)>,
19802) {
19803 if expected.len() == 0usize {
19804 assert!(!breakpoints.contains_key(path), "{}", path.display());
19805 } else {
19806 let mut breakpoint = breakpoints
19807 .get(path)
19808 .unwrap()
19809 .into_iter()
19810 .map(|breakpoint| {
19811 (
19812 breakpoint.row,
19813 Breakpoint {
19814 message: breakpoint.message.clone(),
19815 state: breakpoint.state,
19816 condition: breakpoint.condition.clone(),
19817 hit_condition: breakpoint.hit_condition.clone(),
19818 },
19819 )
19820 })
19821 .collect::<Vec<_>>();
19822
19823 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19824
19825 assert_eq!(expected, breakpoint);
19826 }
19827}
19828
19829fn add_log_breakpoint_at_cursor(
19830 editor: &mut Editor,
19831 log_message: &str,
19832 window: &mut Window,
19833 cx: &mut Context<Editor>,
19834) {
19835 let (anchor, bp) = editor
19836 .breakpoints_at_cursors(window, cx)
19837 .first()
19838 .and_then(|(anchor, bp)| {
19839 if let Some(bp) = bp {
19840 Some((*anchor, bp.clone()))
19841 } else {
19842 None
19843 }
19844 })
19845 .unwrap_or_else(|| {
19846 let cursor_position: Point = editor.selections.newest(cx).head();
19847
19848 let breakpoint_position = editor
19849 .snapshot(window, cx)
19850 .display_snapshot
19851 .buffer_snapshot
19852 .anchor_before(Point::new(cursor_position.row, 0));
19853
19854 (breakpoint_position, Breakpoint::new_log(&log_message))
19855 });
19856
19857 editor.edit_breakpoint_at_anchor(
19858 anchor,
19859 bp,
19860 BreakpointEditAction::EditLogMessage(log_message.into()),
19861 cx,
19862 );
19863}
19864
19865#[gpui::test]
19866async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19867 init_test(cx, |_| {});
19868
19869 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19870 let fs = FakeFs::new(cx.executor());
19871 fs.insert_tree(
19872 path!("/a"),
19873 json!({
19874 "main.rs": sample_text,
19875 }),
19876 )
19877 .await;
19878 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19879 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19880 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19881
19882 let fs = FakeFs::new(cx.executor());
19883 fs.insert_tree(
19884 path!("/a"),
19885 json!({
19886 "main.rs": sample_text,
19887 }),
19888 )
19889 .await;
19890 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19891 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19892 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19893 let worktree_id = workspace
19894 .update(cx, |workspace, _window, cx| {
19895 workspace.project().update(cx, |project, cx| {
19896 project.worktrees(cx).next().unwrap().read(cx).id()
19897 })
19898 })
19899 .unwrap();
19900
19901 let buffer = project
19902 .update(cx, |project, cx| {
19903 project.open_buffer((worktree_id, "main.rs"), cx)
19904 })
19905 .await
19906 .unwrap();
19907
19908 let (editor, cx) = cx.add_window_view(|window, cx| {
19909 Editor::new(
19910 EditorMode::full(),
19911 MultiBuffer::build_from_buffer(buffer, cx),
19912 Some(project.clone()),
19913 window,
19914 cx,
19915 )
19916 });
19917
19918 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19919 let abs_path = project.read_with(cx, |project, cx| {
19920 project
19921 .absolute_path(&project_path, cx)
19922 .map(|path_buf| Arc::from(path_buf.to_owned()))
19923 .unwrap()
19924 });
19925
19926 // assert we can add breakpoint on the first line
19927 editor.update_in(cx, |editor, window, cx| {
19928 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19929 editor.move_to_end(&MoveToEnd, window, cx);
19930 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19931 });
19932
19933 let breakpoints = editor.update(cx, |editor, cx| {
19934 editor
19935 .breakpoint_store()
19936 .as_ref()
19937 .unwrap()
19938 .read(cx)
19939 .all_source_breakpoints(cx)
19940 .clone()
19941 });
19942
19943 assert_eq!(1, breakpoints.len());
19944 assert_breakpoint(
19945 &breakpoints,
19946 &abs_path,
19947 vec![
19948 (0, Breakpoint::new_standard()),
19949 (3, Breakpoint::new_standard()),
19950 ],
19951 );
19952
19953 editor.update_in(cx, |editor, window, cx| {
19954 editor.move_to_beginning(&MoveToBeginning, window, cx);
19955 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19956 });
19957
19958 let breakpoints = editor.update(cx, |editor, cx| {
19959 editor
19960 .breakpoint_store()
19961 .as_ref()
19962 .unwrap()
19963 .read(cx)
19964 .all_source_breakpoints(cx)
19965 .clone()
19966 });
19967
19968 assert_eq!(1, breakpoints.len());
19969 assert_breakpoint(
19970 &breakpoints,
19971 &abs_path,
19972 vec![(3, Breakpoint::new_standard())],
19973 );
19974
19975 editor.update_in(cx, |editor, window, cx| {
19976 editor.move_to_end(&MoveToEnd, window, cx);
19977 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19978 });
19979
19980 let breakpoints = editor.update(cx, |editor, cx| {
19981 editor
19982 .breakpoint_store()
19983 .as_ref()
19984 .unwrap()
19985 .read(cx)
19986 .all_source_breakpoints(cx)
19987 .clone()
19988 });
19989
19990 assert_eq!(0, breakpoints.len());
19991 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19992}
19993
19994#[gpui::test]
19995async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19996 init_test(cx, |_| {});
19997
19998 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19999
20000 let fs = FakeFs::new(cx.executor());
20001 fs.insert_tree(
20002 path!("/a"),
20003 json!({
20004 "main.rs": sample_text,
20005 }),
20006 )
20007 .await;
20008 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20009 let (workspace, cx) =
20010 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20011
20012 let worktree_id = workspace.update(cx, |workspace, cx| {
20013 workspace.project().update(cx, |project, cx| {
20014 project.worktrees(cx).next().unwrap().read(cx).id()
20015 })
20016 });
20017
20018 let buffer = project
20019 .update(cx, |project, cx| {
20020 project.open_buffer((worktree_id, "main.rs"), cx)
20021 })
20022 .await
20023 .unwrap();
20024
20025 let (editor, cx) = cx.add_window_view(|window, cx| {
20026 Editor::new(
20027 EditorMode::full(),
20028 MultiBuffer::build_from_buffer(buffer, cx),
20029 Some(project.clone()),
20030 window,
20031 cx,
20032 )
20033 });
20034
20035 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20036 let abs_path = project.read_with(cx, |project, cx| {
20037 project
20038 .absolute_path(&project_path, cx)
20039 .map(|path_buf| Arc::from(path_buf.to_owned()))
20040 .unwrap()
20041 });
20042
20043 editor.update_in(cx, |editor, window, cx| {
20044 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20045 });
20046
20047 let breakpoints = editor.update(cx, |editor, cx| {
20048 editor
20049 .breakpoint_store()
20050 .as_ref()
20051 .unwrap()
20052 .read(cx)
20053 .all_source_breakpoints(cx)
20054 .clone()
20055 });
20056
20057 assert_breakpoint(
20058 &breakpoints,
20059 &abs_path,
20060 vec![(0, Breakpoint::new_log("hello world"))],
20061 );
20062
20063 // Removing a log message from a log breakpoint should remove it
20064 editor.update_in(cx, |editor, window, cx| {
20065 add_log_breakpoint_at_cursor(editor, "", window, cx);
20066 });
20067
20068 let breakpoints = editor.update(cx, |editor, cx| {
20069 editor
20070 .breakpoint_store()
20071 .as_ref()
20072 .unwrap()
20073 .read(cx)
20074 .all_source_breakpoints(cx)
20075 .clone()
20076 });
20077
20078 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20079
20080 editor.update_in(cx, |editor, window, cx| {
20081 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20082 editor.move_to_end(&MoveToEnd, window, cx);
20083 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20084 // Not adding a log message to a standard breakpoint shouldn't remove it
20085 add_log_breakpoint_at_cursor(editor, "", window, cx);
20086 });
20087
20088 let breakpoints = editor.update(cx, |editor, cx| {
20089 editor
20090 .breakpoint_store()
20091 .as_ref()
20092 .unwrap()
20093 .read(cx)
20094 .all_source_breakpoints(cx)
20095 .clone()
20096 });
20097
20098 assert_breakpoint(
20099 &breakpoints,
20100 &abs_path,
20101 vec![
20102 (0, Breakpoint::new_standard()),
20103 (3, Breakpoint::new_standard()),
20104 ],
20105 );
20106
20107 editor.update_in(cx, |editor, window, cx| {
20108 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20109 });
20110
20111 let breakpoints = editor.update(cx, |editor, cx| {
20112 editor
20113 .breakpoint_store()
20114 .as_ref()
20115 .unwrap()
20116 .read(cx)
20117 .all_source_breakpoints(cx)
20118 .clone()
20119 });
20120
20121 assert_breakpoint(
20122 &breakpoints,
20123 &abs_path,
20124 vec![
20125 (0, Breakpoint::new_standard()),
20126 (3, Breakpoint::new_log("hello world")),
20127 ],
20128 );
20129
20130 editor.update_in(cx, |editor, window, cx| {
20131 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20132 });
20133
20134 let breakpoints = editor.update(cx, |editor, cx| {
20135 editor
20136 .breakpoint_store()
20137 .as_ref()
20138 .unwrap()
20139 .read(cx)
20140 .all_source_breakpoints(cx)
20141 .clone()
20142 });
20143
20144 assert_breakpoint(
20145 &breakpoints,
20146 &abs_path,
20147 vec![
20148 (0, Breakpoint::new_standard()),
20149 (3, Breakpoint::new_log("hello Earth!!")),
20150 ],
20151 );
20152}
20153
20154/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20155/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20156/// or when breakpoints were placed out of order. This tests for a regression too
20157#[gpui::test]
20158async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20159 init_test(cx, |_| {});
20160
20161 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20162 let fs = FakeFs::new(cx.executor());
20163 fs.insert_tree(
20164 path!("/a"),
20165 json!({
20166 "main.rs": sample_text,
20167 }),
20168 )
20169 .await;
20170 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20171 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20172 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20173
20174 let fs = FakeFs::new(cx.executor());
20175 fs.insert_tree(
20176 path!("/a"),
20177 json!({
20178 "main.rs": sample_text,
20179 }),
20180 )
20181 .await;
20182 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20183 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20184 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20185 let worktree_id = workspace
20186 .update(cx, |workspace, _window, cx| {
20187 workspace.project().update(cx, |project, cx| {
20188 project.worktrees(cx).next().unwrap().read(cx).id()
20189 })
20190 })
20191 .unwrap();
20192
20193 let buffer = project
20194 .update(cx, |project, cx| {
20195 project.open_buffer((worktree_id, "main.rs"), cx)
20196 })
20197 .await
20198 .unwrap();
20199
20200 let (editor, cx) = cx.add_window_view(|window, cx| {
20201 Editor::new(
20202 EditorMode::full(),
20203 MultiBuffer::build_from_buffer(buffer, cx),
20204 Some(project.clone()),
20205 window,
20206 cx,
20207 )
20208 });
20209
20210 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20211 let abs_path = project.read_with(cx, |project, cx| {
20212 project
20213 .absolute_path(&project_path, cx)
20214 .map(|path_buf| Arc::from(path_buf.to_owned()))
20215 .unwrap()
20216 });
20217
20218 // assert we can add breakpoint on the first line
20219 editor.update_in(cx, |editor, window, cx| {
20220 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20221 editor.move_to_end(&MoveToEnd, window, cx);
20222 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20223 editor.move_up(&MoveUp, window, cx);
20224 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20225 });
20226
20227 let breakpoints = editor.update(cx, |editor, cx| {
20228 editor
20229 .breakpoint_store()
20230 .as_ref()
20231 .unwrap()
20232 .read(cx)
20233 .all_source_breakpoints(cx)
20234 .clone()
20235 });
20236
20237 assert_eq!(1, breakpoints.len());
20238 assert_breakpoint(
20239 &breakpoints,
20240 &abs_path,
20241 vec![
20242 (0, Breakpoint::new_standard()),
20243 (2, Breakpoint::new_standard()),
20244 (3, Breakpoint::new_standard()),
20245 ],
20246 );
20247
20248 editor.update_in(cx, |editor, window, cx| {
20249 editor.move_to_beginning(&MoveToBeginning, window, cx);
20250 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20251 editor.move_to_end(&MoveToEnd, window, cx);
20252 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20253 // Disabling a breakpoint that doesn't exist should do nothing
20254 editor.move_up(&MoveUp, window, cx);
20255 editor.move_up(&MoveUp, window, cx);
20256 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20257 });
20258
20259 let breakpoints = editor.update(cx, |editor, cx| {
20260 editor
20261 .breakpoint_store()
20262 .as_ref()
20263 .unwrap()
20264 .read(cx)
20265 .all_source_breakpoints(cx)
20266 .clone()
20267 });
20268
20269 let disable_breakpoint = {
20270 let mut bp = Breakpoint::new_standard();
20271 bp.state = BreakpointState::Disabled;
20272 bp
20273 };
20274
20275 assert_eq!(1, breakpoints.len());
20276 assert_breakpoint(
20277 &breakpoints,
20278 &abs_path,
20279 vec![
20280 (0, disable_breakpoint.clone()),
20281 (2, Breakpoint::new_standard()),
20282 (3, disable_breakpoint.clone()),
20283 ],
20284 );
20285
20286 editor.update_in(cx, |editor, window, cx| {
20287 editor.move_to_beginning(&MoveToBeginning, window, cx);
20288 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20289 editor.move_to_end(&MoveToEnd, window, cx);
20290 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20291 editor.move_up(&MoveUp, window, cx);
20292 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20293 });
20294
20295 let breakpoints = editor.update(cx, |editor, cx| {
20296 editor
20297 .breakpoint_store()
20298 .as_ref()
20299 .unwrap()
20300 .read(cx)
20301 .all_source_breakpoints(cx)
20302 .clone()
20303 });
20304
20305 assert_eq!(1, breakpoints.len());
20306 assert_breakpoint(
20307 &breakpoints,
20308 &abs_path,
20309 vec![
20310 (0, Breakpoint::new_standard()),
20311 (2, disable_breakpoint),
20312 (3, Breakpoint::new_standard()),
20313 ],
20314 );
20315}
20316
20317#[gpui::test]
20318async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20319 init_test(cx, |_| {});
20320 let capabilities = lsp::ServerCapabilities {
20321 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20322 prepare_provider: Some(true),
20323 work_done_progress_options: Default::default(),
20324 })),
20325 ..Default::default()
20326 };
20327 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20328
20329 cx.set_state(indoc! {"
20330 struct Fˇoo {}
20331 "});
20332
20333 cx.update_editor(|editor, _, cx| {
20334 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20335 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20336 editor.highlight_background::<DocumentHighlightRead>(
20337 &[highlight_range],
20338 |theme| theme.colors().editor_document_highlight_read_background,
20339 cx,
20340 );
20341 });
20342
20343 let mut prepare_rename_handler = cx
20344 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20345 move |_, _, _| async move {
20346 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20347 start: lsp::Position {
20348 line: 0,
20349 character: 7,
20350 },
20351 end: lsp::Position {
20352 line: 0,
20353 character: 10,
20354 },
20355 })))
20356 },
20357 );
20358 let prepare_rename_task = cx
20359 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20360 .expect("Prepare rename was not started");
20361 prepare_rename_handler.next().await.unwrap();
20362 prepare_rename_task.await.expect("Prepare rename failed");
20363
20364 let mut rename_handler =
20365 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20366 let edit = lsp::TextEdit {
20367 range: lsp::Range {
20368 start: lsp::Position {
20369 line: 0,
20370 character: 7,
20371 },
20372 end: lsp::Position {
20373 line: 0,
20374 character: 10,
20375 },
20376 },
20377 new_text: "FooRenamed".to_string(),
20378 };
20379 Ok(Some(lsp::WorkspaceEdit::new(
20380 // Specify the same edit twice
20381 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20382 )))
20383 });
20384 let rename_task = cx
20385 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20386 .expect("Confirm rename was not started");
20387 rename_handler.next().await.unwrap();
20388 rename_task.await.expect("Confirm rename failed");
20389 cx.run_until_parked();
20390
20391 // Despite two edits, only one is actually applied as those are identical
20392 cx.assert_editor_state(indoc! {"
20393 struct FooRenamedˇ {}
20394 "});
20395}
20396
20397#[gpui::test]
20398async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20399 init_test(cx, |_| {});
20400 // These capabilities indicate that the server does not support prepare rename.
20401 let capabilities = lsp::ServerCapabilities {
20402 rename_provider: Some(lsp::OneOf::Left(true)),
20403 ..Default::default()
20404 };
20405 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20406
20407 cx.set_state(indoc! {"
20408 struct Fˇoo {}
20409 "});
20410
20411 cx.update_editor(|editor, _window, cx| {
20412 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20413 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20414 editor.highlight_background::<DocumentHighlightRead>(
20415 &[highlight_range],
20416 |theme| theme.colors().editor_document_highlight_read_background,
20417 cx,
20418 );
20419 });
20420
20421 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20422 .expect("Prepare rename was not started")
20423 .await
20424 .expect("Prepare rename failed");
20425
20426 let mut rename_handler =
20427 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20428 let edit = lsp::TextEdit {
20429 range: lsp::Range {
20430 start: lsp::Position {
20431 line: 0,
20432 character: 7,
20433 },
20434 end: lsp::Position {
20435 line: 0,
20436 character: 10,
20437 },
20438 },
20439 new_text: "FooRenamed".to_string(),
20440 };
20441 Ok(Some(lsp::WorkspaceEdit::new(
20442 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20443 )))
20444 });
20445 let rename_task = cx
20446 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20447 .expect("Confirm rename was not started");
20448 rename_handler.next().await.unwrap();
20449 rename_task.await.expect("Confirm rename failed");
20450 cx.run_until_parked();
20451
20452 // Correct range is renamed, as `surrounding_word` is used to find it.
20453 cx.assert_editor_state(indoc! {"
20454 struct FooRenamedˇ {}
20455 "});
20456}
20457
20458#[gpui::test]
20459async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20460 init_test(cx, |_| {});
20461 let mut cx = EditorTestContext::new(cx).await;
20462
20463 let language = Arc::new(
20464 Language::new(
20465 LanguageConfig::default(),
20466 Some(tree_sitter_html::LANGUAGE.into()),
20467 )
20468 .with_brackets_query(
20469 r#"
20470 ("<" @open "/>" @close)
20471 ("</" @open ">" @close)
20472 ("<" @open ">" @close)
20473 ("\"" @open "\"" @close)
20474 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20475 "#,
20476 )
20477 .unwrap(),
20478 );
20479 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20480
20481 cx.set_state(indoc! {"
20482 <span>ˇ</span>
20483 "});
20484 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20485 cx.assert_editor_state(indoc! {"
20486 <span>
20487 ˇ
20488 </span>
20489 "});
20490
20491 cx.set_state(indoc! {"
20492 <span><span></span>ˇ</span>
20493 "});
20494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20495 cx.assert_editor_state(indoc! {"
20496 <span><span></span>
20497 ˇ</span>
20498 "});
20499
20500 cx.set_state(indoc! {"
20501 <span>ˇ
20502 </span>
20503 "});
20504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20505 cx.assert_editor_state(indoc! {"
20506 <span>
20507 ˇ
20508 </span>
20509 "});
20510}
20511
20512#[gpui::test(iterations = 10)]
20513async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20514 init_test(cx, |_| {});
20515
20516 let fs = FakeFs::new(cx.executor());
20517 fs.insert_tree(
20518 path!("/dir"),
20519 json!({
20520 "a.ts": "a",
20521 }),
20522 )
20523 .await;
20524
20525 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20526 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20527 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20528
20529 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20530 language_registry.add(Arc::new(Language::new(
20531 LanguageConfig {
20532 name: "TypeScript".into(),
20533 matcher: LanguageMatcher {
20534 path_suffixes: vec!["ts".to_string()],
20535 ..Default::default()
20536 },
20537 ..Default::default()
20538 },
20539 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20540 )));
20541 let mut fake_language_servers = language_registry.register_fake_lsp(
20542 "TypeScript",
20543 FakeLspAdapter {
20544 capabilities: lsp::ServerCapabilities {
20545 code_lens_provider: Some(lsp::CodeLensOptions {
20546 resolve_provider: Some(true),
20547 }),
20548 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20549 commands: vec!["_the/command".to_string()],
20550 ..lsp::ExecuteCommandOptions::default()
20551 }),
20552 ..lsp::ServerCapabilities::default()
20553 },
20554 ..FakeLspAdapter::default()
20555 },
20556 );
20557
20558 let (buffer, _handle) = project
20559 .update(cx, |p, cx| {
20560 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20561 })
20562 .await
20563 .unwrap();
20564 cx.executor().run_until_parked();
20565
20566 let fake_server = fake_language_servers.next().await.unwrap();
20567
20568 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20569 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20570 drop(buffer_snapshot);
20571 let actions = cx
20572 .update_window(*workspace, |_, window, cx| {
20573 project.code_actions(&buffer, anchor..anchor, window, cx)
20574 })
20575 .unwrap();
20576
20577 fake_server
20578 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20579 Ok(Some(vec![
20580 lsp::CodeLens {
20581 range: lsp::Range::default(),
20582 command: Some(lsp::Command {
20583 title: "Code lens command".to_owned(),
20584 command: "_the/command".to_owned(),
20585 arguments: None,
20586 }),
20587 data: None,
20588 },
20589 lsp::CodeLens {
20590 range: lsp::Range::default(),
20591 command: Some(lsp::Command {
20592 title: "Command not in capabilities".to_owned(),
20593 command: "not in capabilities".to_owned(),
20594 arguments: None,
20595 }),
20596 data: None,
20597 },
20598 lsp::CodeLens {
20599 range: lsp::Range {
20600 start: lsp::Position {
20601 line: 1,
20602 character: 1,
20603 },
20604 end: lsp::Position {
20605 line: 1,
20606 character: 1,
20607 },
20608 },
20609 command: Some(lsp::Command {
20610 title: "Command not in range".to_owned(),
20611 command: "_the/command".to_owned(),
20612 arguments: None,
20613 }),
20614 data: None,
20615 },
20616 ]))
20617 })
20618 .next()
20619 .await;
20620
20621 let actions = actions.await.unwrap();
20622 assert_eq!(
20623 actions.len(),
20624 1,
20625 "Should have only one valid action for the 0..0 range"
20626 );
20627 let action = actions[0].clone();
20628 let apply = project.update(cx, |project, cx| {
20629 project.apply_code_action(buffer.clone(), action, true, cx)
20630 });
20631
20632 // Resolving the code action does not populate its edits. In absence of
20633 // edits, we must execute the given command.
20634 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20635 |mut lens, _| async move {
20636 let lens_command = lens.command.as_mut().expect("should have a command");
20637 assert_eq!(lens_command.title, "Code lens command");
20638 lens_command.arguments = Some(vec![json!("the-argument")]);
20639 Ok(lens)
20640 },
20641 );
20642
20643 // While executing the command, the language server sends the editor
20644 // a `workspaceEdit` request.
20645 fake_server
20646 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20647 let fake = fake_server.clone();
20648 move |params, _| {
20649 assert_eq!(params.command, "_the/command");
20650 let fake = fake.clone();
20651 async move {
20652 fake.server
20653 .request::<lsp::request::ApplyWorkspaceEdit>(
20654 lsp::ApplyWorkspaceEditParams {
20655 label: None,
20656 edit: lsp::WorkspaceEdit {
20657 changes: Some(
20658 [(
20659 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20660 vec![lsp::TextEdit {
20661 range: lsp::Range::new(
20662 lsp::Position::new(0, 0),
20663 lsp::Position::new(0, 0),
20664 ),
20665 new_text: "X".into(),
20666 }],
20667 )]
20668 .into_iter()
20669 .collect(),
20670 ),
20671 ..Default::default()
20672 },
20673 },
20674 )
20675 .await
20676 .into_response()
20677 .unwrap();
20678 Ok(Some(json!(null)))
20679 }
20680 }
20681 })
20682 .next()
20683 .await;
20684
20685 // Applying the code lens command returns a project transaction containing the edits
20686 // sent by the language server in its `workspaceEdit` request.
20687 let transaction = apply.await.unwrap();
20688 assert!(transaction.0.contains_key(&buffer));
20689 buffer.update(cx, |buffer, cx| {
20690 assert_eq!(buffer.text(), "Xa");
20691 buffer.undo(cx);
20692 assert_eq!(buffer.text(), "a");
20693 });
20694}
20695
20696#[gpui::test]
20697async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20698 init_test(cx, |_| {});
20699
20700 let fs = FakeFs::new(cx.executor());
20701 let main_text = r#"fn main() {
20702println!("1");
20703println!("2");
20704println!("3");
20705println!("4");
20706println!("5");
20707}"#;
20708 let lib_text = "mod foo {}";
20709 fs.insert_tree(
20710 path!("/a"),
20711 json!({
20712 "lib.rs": lib_text,
20713 "main.rs": main_text,
20714 }),
20715 )
20716 .await;
20717
20718 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20719 let (workspace, cx) =
20720 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20721 let worktree_id = workspace.update(cx, |workspace, cx| {
20722 workspace.project().update(cx, |project, cx| {
20723 project.worktrees(cx).next().unwrap().read(cx).id()
20724 })
20725 });
20726
20727 let expected_ranges = vec![
20728 Point::new(0, 0)..Point::new(0, 0),
20729 Point::new(1, 0)..Point::new(1, 1),
20730 Point::new(2, 0)..Point::new(2, 2),
20731 Point::new(3, 0)..Point::new(3, 3),
20732 ];
20733
20734 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20735 let editor_1 = workspace
20736 .update_in(cx, |workspace, window, cx| {
20737 workspace.open_path(
20738 (worktree_id, "main.rs"),
20739 Some(pane_1.downgrade()),
20740 true,
20741 window,
20742 cx,
20743 )
20744 })
20745 .unwrap()
20746 .await
20747 .downcast::<Editor>()
20748 .unwrap();
20749 pane_1.update(cx, |pane, cx| {
20750 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20751 open_editor.update(cx, |editor, cx| {
20752 assert_eq!(
20753 editor.display_text(cx),
20754 main_text,
20755 "Original main.rs text on initial open",
20756 );
20757 assert_eq!(
20758 editor
20759 .selections
20760 .all::<Point>(cx)
20761 .into_iter()
20762 .map(|s| s.range())
20763 .collect::<Vec<_>>(),
20764 vec![Point::zero()..Point::zero()],
20765 "Default selections on initial open",
20766 );
20767 })
20768 });
20769 editor_1.update_in(cx, |editor, window, cx| {
20770 editor.change_selections(None, window, cx, |s| {
20771 s.select_ranges(expected_ranges.clone());
20772 });
20773 });
20774
20775 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20776 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20777 });
20778 let editor_2 = workspace
20779 .update_in(cx, |workspace, window, cx| {
20780 workspace.open_path(
20781 (worktree_id, "main.rs"),
20782 Some(pane_2.downgrade()),
20783 true,
20784 window,
20785 cx,
20786 )
20787 })
20788 .unwrap()
20789 .await
20790 .downcast::<Editor>()
20791 .unwrap();
20792 pane_2.update(cx, |pane, cx| {
20793 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20794 open_editor.update(cx, |editor, cx| {
20795 assert_eq!(
20796 editor.display_text(cx),
20797 main_text,
20798 "Original main.rs text on initial open in another panel",
20799 );
20800 assert_eq!(
20801 editor
20802 .selections
20803 .all::<Point>(cx)
20804 .into_iter()
20805 .map(|s| s.range())
20806 .collect::<Vec<_>>(),
20807 vec![Point::zero()..Point::zero()],
20808 "Default selections on initial open in another panel",
20809 );
20810 })
20811 });
20812
20813 editor_2.update_in(cx, |editor, window, cx| {
20814 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20815 });
20816
20817 let _other_editor_1 = workspace
20818 .update_in(cx, |workspace, window, cx| {
20819 workspace.open_path(
20820 (worktree_id, "lib.rs"),
20821 Some(pane_1.downgrade()),
20822 true,
20823 window,
20824 cx,
20825 )
20826 })
20827 .unwrap()
20828 .await
20829 .downcast::<Editor>()
20830 .unwrap();
20831 pane_1
20832 .update_in(cx, |pane, window, cx| {
20833 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20834 })
20835 .await
20836 .unwrap();
20837 drop(editor_1);
20838 pane_1.update(cx, |pane, cx| {
20839 pane.active_item()
20840 .unwrap()
20841 .downcast::<Editor>()
20842 .unwrap()
20843 .update(cx, |editor, cx| {
20844 assert_eq!(
20845 editor.display_text(cx),
20846 lib_text,
20847 "Other file should be open and active",
20848 );
20849 });
20850 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20851 });
20852
20853 let _other_editor_2 = workspace
20854 .update_in(cx, |workspace, window, cx| {
20855 workspace.open_path(
20856 (worktree_id, "lib.rs"),
20857 Some(pane_2.downgrade()),
20858 true,
20859 window,
20860 cx,
20861 )
20862 })
20863 .unwrap()
20864 .await
20865 .downcast::<Editor>()
20866 .unwrap();
20867 pane_2
20868 .update_in(cx, |pane, window, cx| {
20869 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20870 })
20871 .await
20872 .unwrap();
20873 drop(editor_2);
20874 pane_2.update(cx, |pane, cx| {
20875 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20876 open_editor.update(cx, |editor, cx| {
20877 assert_eq!(
20878 editor.display_text(cx),
20879 lib_text,
20880 "Other file should be open and active in another panel too",
20881 );
20882 });
20883 assert_eq!(
20884 pane.items().count(),
20885 1,
20886 "No other editors should be open in another pane",
20887 );
20888 });
20889
20890 let _editor_1_reopened = workspace
20891 .update_in(cx, |workspace, window, cx| {
20892 workspace.open_path(
20893 (worktree_id, "main.rs"),
20894 Some(pane_1.downgrade()),
20895 true,
20896 window,
20897 cx,
20898 )
20899 })
20900 .unwrap()
20901 .await
20902 .downcast::<Editor>()
20903 .unwrap();
20904 let _editor_2_reopened = workspace
20905 .update_in(cx, |workspace, window, cx| {
20906 workspace.open_path(
20907 (worktree_id, "main.rs"),
20908 Some(pane_2.downgrade()),
20909 true,
20910 window,
20911 cx,
20912 )
20913 })
20914 .unwrap()
20915 .await
20916 .downcast::<Editor>()
20917 .unwrap();
20918 pane_1.update(cx, |pane, cx| {
20919 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20920 open_editor.update(cx, |editor, cx| {
20921 assert_eq!(
20922 editor.display_text(cx),
20923 main_text,
20924 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20925 );
20926 assert_eq!(
20927 editor
20928 .selections
20929 .all::<Point>(cx)
20930 .into_iter()
20931 .map(|s| s.range())
20932 .collect::<Vec<_>>(),
20933 expected_ranges,
20934 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20935 );
20936 })
20937 });
20938 pane_2.update(cx, |pane, cx| {
20939 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20940 open_editor.update(cx, |editor, cx| {
20941 assert_eq!(
20942 editor.display_text(cx),
20943 r#"fn main() {
20944⋯rintln!("1");
20945⋯intln!("2");
20946⋯ntln!("3");
20947println!("4");
20948println!("5");
20949}"#,
20950 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20951 );
20952 assert_eq!(
20953 editor
20954 .selections
20955 .all::<Point>(cx)
20956 .into_iter()
20957 .map(|s| s.range())
20958 .collect::<Vec<_>>(),
20959 vec![Point::zero()..Point::zero()],
20960 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20961 );
20962 })
20963 });
20964}
20965
20966#[gpui::test]
20967async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20968 init_test(cx, |_| {});
20969
20970 let fs = FakeFs::new(cx.executor());
20971 let main_text = r#"fn main() {
20972println!("1");
20973println!("2");
20974println!("3");
20975println!("4");
20976println!("5");
20977}"#;
20978 let lib_text = "mod foo {}";
20979 fs.insert_tree(
20980 path!("/a"),
20981 json!({
20982 "lib.rs": lib_text,
20983 "main.rs": main_text,
20984 }),
20985 )
20986 .await;
20987
20988 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20989 let (workspace, cx) =
20990 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20991 let worktree_id = workspace.update(cx, |workspace, cx| {
20992 workspace.project().update(cx, |project, cx| {
20993 project.worktrees(cx).next().unwrap().read(cx).id()
20994 })
20995 });
20996
20997 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20998 let editor = workspace
20999 .update_in(cx, |workspace, window, cx| {
21000 workspace.open_path(
21001 (worktree_id, "main.rs"),
21002 Some(pane.downgrade()),
21003 true,
21004 window,
21005 cx,
21006 )
21007 })
21008 .unwrap()
21009 .await
21010 .downcast::<Editor>()
21011 .unwrap();
21012 pane.update(cx, |pane, cx| {
21013 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21014 open_editor.update(cx, |editor, cx| {
21015 assert_eq!(
21016 editor.display_text(cx),
21017 main_text,
21018 "Original main.rs text on initial open",
21019 );
21020 })
21021 });
21022 editor.update_in(cx, |editor, window, cx| {
21023 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21024 });
21025
21026 cx.update_global(|store: &mut SettingsStore, cx| {
21027 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21028 s.restore_on_file_reopen = Some(false);
21029 });
21030 });
21031 editor.update_in(cx, |editor, window, cx| {
21032 editor.fold_ranges(
21033 vec![
21034 Point::new(1, 0)..Point::new(1, 1),
21035 Point::new(2, 0)..Point::new(2, 2),
21036 Point::new(3, 0)..Point::new(3, 3),
21037 ],
21038 false,
21039 window,
21040 cx,
21041 );
21042 });
21043 pane.update_in(cx, |pane, window, cx| {
21044 pane.close_all_items(&CloseAllItems::default(), window, cx)
21045 })
21046 .await
21047 .unwrap();
21048 pane.update(cx, |pane, _| {
21049 assert!(pane.active_item().is_none());
21050 });
21051 cx.update_global(|store: &mut SettingsStore, cx| {
21052 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21053 s.restore_on_file_reopen = Some(true);
21054 });
21055 });
21056
21057 let _editor_reopened = workspace
21058 .update_in(cx, |workspace, window, cx| {
21059 workspace.open_path(
21060 (worktree_id, "main.rs"),
21061 Some(pane.downgrade()),
21062 true,
21063 window,
21064 cx,
21065 )
21066 })
21067 .unwrap()
21068 .await
21069 .downcast::<Editor>()
21070 .unwrap();
21071 pane.update(cx, |pane, cx| {
21072 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21073 open_editor.update(cx, |editor, cx| {
21074 assert_eq!(
21075 editor.display_text(cx),
21076 main_text,
21077 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21078 );
21079 })
21080 });
21081}
21082
21083#[gpui::test]
21084async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21085 struct EmptyModalView {
21086 focus_handle: gpui::FocusHandle,
21087 }
21088 impl EventEmitter<DismissEvent> for EmptyModalView {}
21089 impl Render for EmptyModalView {
21090 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21091 div()
21092 }
21093 }
21094 impl Focusable for EmptyModalView {
21095 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21096 self.focus_handle.clone()
21097 }
21098 }
21099 impl workspace::ModalView for EmptyModalView {}
21100 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21101 EmptyModalView {
21102 focus_handle: cx.focus_handle(),
21103 }
21104 }
21105
21106 init_test(cx, |_| {});
21107
21108 let fs = FakeFs::new(cx.executor());
21109 let project = Project::test(fs, [], cx).await;
21110 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21111 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21112 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21113 let editor = cx.new_window_entity(|window, cx| {
21114 Editor::new(
21115 EditorMode::full(),
21116 buffer,
21117 Some(project.clone()),
21118 window,
21119 cx,
21120 )
21121 });
21122 workspace
21123 .update(cx, |workspace, window, cx| {
21124 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21125 })
21126 .unwrap();
21127 editor.update_in(cx, |editor, window, cx| {
21128 editor.open_context_menu(&OpenContextMenu, window, cx);
21129 assert!(editor.mouse_context_menu.is_some());
21130 });
21131 workspace
21132 .update(cx, |workspace, window, cx| {
21133 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21134 })
21135 .unwrap();
21136 cx.read(|cx| {
21137 assert!(editor.read(cx).mouse_context_menu.is_none());
21138 });
21139}
21140
21141#[gpui::test]
21142async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21143 init_test(cx, |_| {});
21144
21145 let fs = FakeFs::new(cx.executor());
21146 fs.insert_file(path!("/file.html"), Default::default())
21147 .await;
21148
21149 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21150
21151 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21152 let html_language = Arc::new(Language::new(
21153 LanguageConfig {
21154 name: "HTML".into(),
21155 matcher: LanguageMatcher {
21156 path_suffixes: vec!["html".to_string()],
21157 ..LanguageMatcher::default()
21158 },
21159 brackets: BracketPairConfig {
21160 pairs: vec![BracketPair {
21161 start: "<".into(),
21162 end: ">".into(),
21163 close: true,
21164 ..Default::default()
21165 }],
21166 ..Default::default()
21167 },
21168 ..Default::default()
21169 },
21170 Some(tree_sitter_html::LANGUAGE.into()),
21171 ));
21172 language_registry.add(html_language);
21173 let mut fake_servers = language_registry.register_fake_lsp(
21174 "HTML",
21175 FakeLspAdapter {
21176 capabilities: lsp::ServerCapabilities {
21177 completion_provider: Some(lsp::CompletionOptions {
21178 resolve_provider: Some(true),
21179 ..Default::default()
21180 }),
21181 ..Default::default()
21182 },
21183 ..Default::default()
21184 },
21185 );
21186
21187 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21188 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21189
21190 let worktree_id = workspace
21191 .update(cx, |workspace, _window, cx| {
21192 workspace.project().update(cx, |project, cx| {
21193 project.worktrees(cx).next().unwrap().read(cx).id()
21194 })
21195 })
21196 .unwrap();
21197 project
21198 .update(cx, |project, cx| {
21199 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21200 })
21201 .await
21202 .unwrap();
21203 let editor = workspace
21204 .update(cx, |workspace, window, cx| {
21205 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21206 })
21207 .unwrap()
21208 .await
21209 .unwrap()
21210 .downcast::<Editor>()
21211 .unwrap();
21212
21213 let fake_server = fake_servers.next().await.unwrap();
21214 editor.update_in(cx, |editor, window, cx| {
21215 editor.set_text("<ad></ad>", window, cx);
21216 editor.change_selections(None, window, cx, |selections| {
21217 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21218 });
21219 let Some((buffer, _)) = editor
21220 .buffer
21221 .read(cx)
21222 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21223 else {
21224 panic!("Failed to get buffer for selection position");
21225 };
21226 let buffer = buffer.read(cx);
21227 let buffer_id = buffer.remote_id();
21228 let opening_range =
21229 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21230 let closing_range =
21231 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21232 let mut linked_ranges = HashMap::default();
21233 linked_ranges.insert(
21234 buffer_id,
21235 vec![(opening_range.clone(), vec![closing_range.clone()])],
21236 );
21237 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21238 });
21239 let mut completion_handle =
21240 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21241 Ok(Some(lsp::CompletionResponse::Array(vec![
21242 lsp::CompletionItem {
21243 label: "head".to_string(),
21244 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21245 lsp::InsertReplaceEdit {
21246 new_text: "head".to_string(),
21247 insert: lsp::Range::new(
21248 lsp::Position::new(0, 1),
21249 lsp::Position::new(0, 3),
21250 ),
21251 replace: lsp::Range::new(
21252 lsp::Position::new(0, 1),
21253 lsp::Position::new(0, 3),
21254 ),
21255 },
21256 )),
21257 ..Default::default()
21258 },
21259 ])))
21260 });
21261 editor.update_in(cx, |editor, window, cx| {
21262 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21263 });
21264 cx.run_until_parked();
21265 completion_handle.next().await.unwrap();
21266 editor.update(cx, |editor, _| {
21267 assert!(
21268 editor.context_menu_visible(),
21269 "Completion menu should be visible"
21270 );
21271 });
21272 editor.update_in(cx, |editor, window, cx| {
21273 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21274 });
21275 cx.executor().run_until_parked();
21276 editor.update(cx, |editor, cx| {
21277 assert_eq!(editor.text(cx), "<head></head>");
21278 });
21279}
21280
21281#[gpui::test]
21282async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21283 init_test(cx, |_| {});
21284
21285 let fs = FakeFs::new(cx.executor());
21286 fs.insert_tree(
21287 path!("/root"),
21288 json!({
21289 "a": {
21290 "main.rs": "fn main() {}",
21291 },
21292 "foo": {
21293 "bar": {
21294 "external_file.rs": "pub mod external {}",
21295 }
21296 }
21297 }),
21298 )
21299 .await;
21300
21301 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21302 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21303 language_registry.add(rust_lang());
21304 let _fake_servers = language_registry.register_fake_lsp(
21305 "Rust",
21306 FakeLspAdapter {
21307 ..FakeLspAdapter::default()
21308 },
21309 );
21310 let (workspace, cx) =
21311 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21312 let worktree_id = workspace.update(cx, |workspace, cx| {
21313 workspace.project().update(cx, |project, cx| {
21314 project.worktrees(cx).next().unwrap().read(cx).id()
21315 })
21316 });
21317
21318 let assert_language_servers_count =
21319 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21320 project.update(cx, |project, cx| {
21321 let current = project
21322 .lsp_store()
21323 .read(cx)
21324 .as_local()
21325 .unwrap()
21326 .language_servers
21327 .len();
21328 assert_eq!(expected, current, "{context}");
21329 });
21330 };
21331
21332 assert_language_servers_count(
21333 0,
21334 "No servers should be running before any file is open",
21335 cx,
21336 );
21337 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21338 let main_editor = workspace
21339 .update_in(cx, |workspace, window, cx| {
21340 workspace.open_path(
21341 (worktree_id, "main.rs"),
21342 Some(pane.downgrade()),
21343 true,
21344 window,
21345 cx,
21346 )
21347 })
21348 .unwrap()
21349 .await
21350 .downcast::<Editor>()
21351 .unwrap();
21352 pane.update(cx, |pane, cx| {
21353 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21354 open_editor.update(cx, |editor, cx| {
21355 assert_eq!(
21356 editor.display_text(cx),
21357 "fn main() {}",
21358 "Original main.rs text on initial open",
21359 );
21360 });
21361 assert_eq!(open_editor, main_editor);
21362 });
21363 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21364
21365 let external_editor = workspace
21366 .update_in(cx, |workspace, window, cx| {
21367 workspace.open_abs_path(
21368 PathBuf::from("/root/foo/bar/external_file.rs"),
21369 OpenOptions::default(),
21370 window,
21371 cx,
21372 )
21373 })
21374 .await
21375 .expect("opening external file")
21376 .downcast::<Editor>()
21377 .expect("downcasted external file's open element to editor");
21378 pane.update(cx, |pane, cx| {
21379 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21380 open_editor.update(cx, |editor, cx| {
21381 assert_eq!(
21382 editor.display_text(cx),
21383 "pub mod external {}",
21384 "External file is open now",
21385 );
21386 });
21387 assert_eq!(open_editor, external_editor);
21388 });
21389 assert_language_servers_count(
21390 1,
21391 "Second, external, *.rs file should join the existing server",
21392 cx,
21393 );
21394
21395 pane.update_in(cx, |pane, window, cx| {
21396 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21397 })
21398 .await
21399 .unwrap();
21400 pane.update_in(cx, |pane, window, cx| {
21401 pane.navigate_backward(window, cx);
21402 });
21403 cx.run_until_parked();
21404 pane.update(cx, |pane, cx| {
21405 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21406 open_editor.update(cx, |editor, cx| {
21407 assert_eq!(
21408 editor.display_text(cx),
21409 "pub mod external {}",
21410 "External file is open now",
21411 );
21412 });
21413 });
21414 assert_language_servers_count(
21415 1,
21416 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21417 cx,
21418 );
21419
21420 cx.update(|_, cx| {
21421 workspace::reload(&workspace::Reload::default(), cx);
21422 });
21423 assert_language_servers_count(
21424 1,
21425 "After reloading the worktree with local and external files opened, only one project should be started",
21426 cx,
21427 );
21428}
21429
21430#[gpui::test]
21431async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21432 init_test(cx, |_| {});
21433
21434 let mut cx = EditorTestContext::new(cx).await;
21435 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21436 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21437
21438 // test cursor move to start of each line on tab
21439 // for `if`, `elif`, `else`, `while`, `with` and `for`
21440 cx.set_state(indoc! {"
21441 def main():
21442 ˇ for item in items:
21443 ˇ while item.active:
21444 ˇ if item.value > 10:
21445 ˇ continue
21446 ˇ elif item.value < 0:
21447 ˇ break
21448 ˇ else:
21449 ˇ with item.context() as ctx:
21450 ˇ yield count
21451 ˇ else:
21452 ˇ log('while else')
21453 ˇ else:
21454 ˇ log('for else')
21455 "});
21456 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21457 cx.assert_editor_state(indoc! {"
21458 def main():
21459 ˇfor item in items:
21460 ˇwhile item.active:
21461 ˇif item.value > 10:
21462 ˇcontinue
21463 ˇelif item.value < 0:
21464 ˇbreak
21465 ˇelse:
21466 ˇwith item.context() as ctx:
21467 ˇyield count
21468 ˇelse:
21469 ˇlog('while else')
21470 ˇelse:
21471 ˇlog('for else')
21472 "});
21473 // test relative indent is preserved when tab
21474 // for `if`, `elif`, `else`, `while`, `with` and `for`
21475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21476 cx.assert_editor_state(indoc! {"
21477 def main():
21478 ˇfor item in items:
21479 ˇwhile item.active:
21480 ˇif item.value > 10:
21481 ˇcontinue
21482 ˇelif item.value < 0:
21483 ˇbreak
21484 ˇelse:
21485 ˇwith item.context() as ctx:
21486 ˇyield count
21487 ˇelse:
21488 ˇlog('while else')
21489 ˇelse:
21490 ˇlog('for else')
21491 "});
21492
21493 // test cursor move to start of each line on tab
21494 // for `try`, `except`, `else`, `finally`, `match` and `def`
21495 cx.set_state(indoc! {"
21496 def main():
21497 ˇ try:
21498 ˇ fetch()
21499 ˇ except ValueError:
21500 ˇ handle_error()
21501 ˇ else:
21502 ˇ match value:
21503 ˇ case _:
21504 ˇ finally:
21505 ˇ def status():
21506 ˇ return 0
21507 "});
21508 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21509 cx.assert_editor_state(indoc! {"
21510 def main():
21511 ˇtry:
21512 ˇfetch()
21513 ˇexcept ValueError:
21514 ˇhandle_error()
21515 ˇelse:
21516 ˇmatch value:
21517 ˇcase _:
21518 ˇfinally:
21519 ˇdef status():
21520 ˇreturn 0
21521 "});
21522 // test relative indent is preserved when tab
21523 // for `try`, `except`, `else`, `finally`, `match` and `def`
21524 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21525 cx.assert_editor_state(indoc! {"
21526 def main():
21527 ˇtry:
21528 ˇfetch()
21529 ˇexcept ValueError:
21530 ˇhandle_error()
21531 ˇelse:
21532 ˇmatch value:
21533 ˇcase _:
21534 ˇfinally:
21535 ˇdef status():
21536 ˇreturn 0
21537 "});
21538}
21539
21540#[gpui::test]
21541async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21542 init_test(cx, |_| {});
21543
21544 let mut cx = EditorTestContext::new(cx).await;
21545 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21546 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21547
21548 // test `else` auto outdents when typed inside `if` block
21549 cx.set_state(indoc! {"
21550 def main():
21551 if i == 2:
21552 return
21553 ˇ
21554 "});
21555 cx.update_editor(|editor, window, cx| {
21556 editor.handle_input("else:", window, cx);
21557 });
21558 cx.assert_editor_state(indoc! {"
21559 def main():
21560 if i == 2:
21561 return
21562 else:ˇ
21563 "});
21564
21565 // test `except` auto outdents when typed inside `try` block
21566 cx.set_state(indoc! {"
21567 def main():
21568 try:
21569 i = 2
21570 ˇ
21571 "});
21572 cx.update_editor(|editor, window, cx| {
21573 editor.handle_input("except:", window, cx);
21574 });
21575 cx.assert_editor_state(indoc! {"
21576 def main():
21577 try:
21578 i = 2
21579 except:ˇ
21580 "});
21581
21582 // test `else` auto outdents when typed inside `except` block
21583 cx.set_state(indoc! {"
21584 def main():
21585 try:
21586 i = 2
21587 except:
21588 j = 2
21589 ˇ
21590 "});
21591 cx.update_editor(|editor, window, cx| {
21592 editor.handle_input("else:", window, cx);
21593 });
21594 cx.assert_editor_state(indoc! {"
21595 def main():
21596 try:
21597 i = 2
21598 except:
21599 j = 2
21600 else:ˇ
21601 "});
21602
21603 // test `finally` auto outdents when typed inside `else` block
21604 cx.set_state(indoc! {"
21605 def main():
21606 try:
21607 i = 2
21608 except:
21609 j = 2
21610 else:
21611 k = 2
21612 ˇ
21613 "});
21614 cx.update_editor(|editor, window, cx| {
21615 editor.handle_input("finally:", window, cx);
21616 });
21617 cx.assert_editor_state(indoc! {"
21618 def main():
21619 try:
21620 i = 2
21621 except:
21622 j = 2
21623 else:
21624 k = 2
21625 finally:ˇ
21626 "});
21627
21628 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21629 // cx.set_state(indoc! {"
21630 // def main():
21631 // try:
21632 // for i in range(n):
21633 // pass
21634 // ˇ
21635 // "});
21636 // cx.update_editor(|editor, window, cx| {
21637 // editor.handle_input("except:", window, cx);
21638 // });
21639 // cx.assert_editor_state(indoc! {"
21640 // def main():
21641 // try:
21642 // for i in range(n):
21643 // pass
21644 // except:ˇ
21645 // "});
21646
21647 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21648 // cx.set_state(indoc! {"
21649 // def main():
21650 // try:
21651 // i = 2
21652 // except:
21653 // for i in range(n):
21654 // pass
21655 // ˇ
21656 // "});
21657 // cx.update_editor(|editor, window, cx| {
21658 // editor.handle_input("else:", window, cx);
21659 // });
21660 // cx.assert_editor_state(indoc! {"
21661 // def main():
21662 // try:
21663 // i = 2
21664 // except:
21665 // for i in range(n):
21666 // pass
21667 // else:ˇ
21668 // "});
21669
21670 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21671 // cx.set_state(indoc! {"
21672 // def main():
21673 // try:
21674 // i = 2
21675 // except:
21676 // j = 2
21677 // else:
21678 // for i in range(n):
21679 // pass
21680 // ˇ
21681 // "});
21682 // cx.update_editor(|editor, window, cx| {
21683 // editor.handle_input("finally:", window, cx);
21684 // });
21685 // cx.assert_editor_state(indoc! {"
21686 // def main():
21687 // try:
21688 // i = 2
21689 // except:
21690 // j = 2
21691 // else:
21692 // for i in range(n):
21693 // pass
21694 // finally:ˇ
21695 // "});
21696
21697 // test `else` stays at correct indent when typed after `for` block
21698 cx.set_state(indoc! {"
21699 def main():
21700 for i in range(10):
21701 if i == 3:
21702 break
21703 ˇ
21704 "});
21705 cx.update_editor(|editor, window, cx| {
21706 editor.handle_input("else:", window, cx);
21707 });
21708 cx.assert_editor_state(indoc! {"
21709 def main():
21710 for i in range(10):
21711 if i == 3:
21712 break
21713 else:ˇ
21714 "});
21715
21716 // test does not outdent on typing after line with square brackets
21717 cx.set_state(indoc! {"
21718 def f() -> list[str]:
21719 ˇ
21720 "});
21721 cx.update_editor(|editor, window, cx| {
21722 editor.handle_input("a", window, cx);
21723 });
21724 cx.assert_editor_state(indoc! {"
21725 def f() -> list[str]:
21726 aˇ
21727 "});
21728}
21729
21730#[gpui::test]
21731async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21732 init_test(cx, |_| {});
21733 update_test_language_settings(cx, |settings| {
21734 settings.defaults.extend_comment_on_newline = Some(false);
21735 });
21736 let mut cx = EditorTestContext::new(cx).await;
21737 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21738 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21739
21740 // test correct indent after newline on comment
21741 cx.set_state(indoc! {"
21742 # COMMENT:ˇ
21743 "});
21744 cx.update_editor(|editor, window, cx| {
21745 editor.newline(&Newline, window, cx);
21746 });
21747 cx.assert_editor_state(indoc! {"
21748 # COMMENT:
21749 ˇ
21750 "});
21751
21752 // test correct indent after newline in brackets
21753 cx.set_state(indoc! {"
21754 {ˇ}
21755 "});
21756 cx.update_editor(|editor, window, cx| {
21757 editor.newline(&Newline, window, cx);
21758 });
21759 cx.run_until_parked();
21760 cx.assert_editor_state(indoc! {"
21761 {
21762 ˇ
21763 }
21764 "});
21765
21766 cx.set_state(indoc! {"
21767 (ˇ)
21768 "});
21769 cx.update_editor(|editor, window, cx| {
21770 editor.newline(&Newline, window, cx);
21771 });
21772 cx.run_until_parked();
21773 cx.assert_editor_state(indoc! {"
21774 (
21775 ˇ
21776 )
21777 "});
21778
21779 // do not indent after empty lists or dictionaries
21780 cx.set_state(indoc! {"
21781 a = []ˇ
21782 "});
21783 cx.update_editor(|editor, window, cx| {
21784 editor.newline(&Newline, window, cx);
21785 });
21786 cx.run_until_parked();
21787 cx.assert_editor_state(indoc! {"
21788 a = []
21789 ˇ
21790 "});
21791}
21792
21793fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21794 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21795 point..point
21796}
21797
21798#[track_caller]
21799fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21800 let (text, ranges) = marked_text_ranges(marked_text, true);
21801 assert_eq!(editor.text(cx), text);
21802 assert_eq!(
21803 editor.selections.ranges(cx),
21804 ranges,
21805 "Assert selections are {}",
21806 marked_text
21807 );
21808}
21809
21810pub fn handle_signature_help_request(
21811 cx: &mut EditorLspTestContext,
21812 mocked_response: lsp::SignatureHelp,
21813) -> impl Future<Output = ()> + use<> {
21814 let mut request =
21815 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21816 let mocked_response = mocked_response.clone();
21817 async move { Ok(Some(mocked_response)) }
21818 });
21819
21820 async move {
21821 request.next().await;
21822 }
21823}
21824
21825#[track_caller]
21826pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21827 cx.update_editor(|editor, _, _| {
21828 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21829 let entries = menu.entries.borrow();
21830 let entries = entries
21831 .iter()
21832 .map(|entry| entry.string.as_str())
21833 .collect::<Vec<_>>();
21834 assert_eq!(entries, expected);
21835 } else {
21836 panic!("Expected completions menu");
21837 }
21838 });
21839}
21840
21841/// Handle completion request passing a marked string specifying where the completion
21842/// should be triggered from using '|' character, what range should be replaced, and what completions
21843/// should be returned using '<' and '>' to delimit the range.
21844///
21845/// Also see `handle_completion_request_with_insert_and_replace`.
21846#[track_caller]
21847pub fn handle_completion_request(
21848 marked_string: &str,
21849 completions: Vec<&'static str>,
21850 is_incomplete: bool,
21851 counter: Arc<AtomicUsize>,
21852 cx: &mut EditorLspTestContext,
21853) -> impl Future<Output = ()> {
21854 let complete_from_marker: TextRangeMarker = '|'.into();
21855 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21856 let (_, mut marked_ranges) = marked_text_ranges_by(
21857 marked_string,
21858 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21859 );
21860
21861 let complete_from_position =
21862 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21863 let replace_range =
21864 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21865
21866 let mut request =
21867 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21868 let completions = completions.clone();
21869 counter.fetch_add(1, atomic::Ordering::Release);
21870 async move {
21871 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21872 assert_eq!(
21873 params.text_document_position.position,
21874 complete_from_position
21875 );
21876 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21877 is_incomplete: is_incomplete,
21878 item_defaults: None,
21879 items: completions
21880 .iter()
21881 .map(|completion_text| lsp::CompletionItem {
21882 label: completion_text.to_string(),
21883 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21884 range: replace_range,
21885 new_text: completion_text.to_string(),
21886 })),
21887 ..Default::default()
21888 })
21889 .collect(),
21890 })))
21891 }
21892 });
21893
21894 async move {
21895 request.next().await;
21896 }
21897}
21898
21899/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21900/// given instead, which also contains an `insert` range.
21901///
21902/// This function uses markers to define ranges:
21903/// - `|` marks the cursor position
21904/// - `<>` marks the replace range
21905/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21906pub fn handle_completion_request_with_insert_and_replace(
21907 cx: &mut EditorLspTestContext,
21908 marked_string: &str,
21909 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21910 counter: Arc<AtomicUsize>,
21911) -> impl Future<Output = ()> {
21912 let complete_from_marker: TextRangeMarker = '|'.into();
21913 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21914 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21915
21916 let (_, mut marked_ranges) = marked_text_ranges_by(
21917 marked_string,
21918 vec![
21919 complete_from_marker.clone(),
21920 replace_range_marker.clone(),
21921 insert_range_marker.clone(),
21922 ],
21923 );
21924
21925 let complete_from_position =
21926 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21927 let replace_range =
21928 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21929
21930 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21931 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21932 _ => lsp::Range {
21933 start: replace_range.start,
21934 end: complete_from_position,
21935 },
21936 };
21937
21938 let mut request =
21939 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21940 let completions = completions.clone();
21941 counter.fetch_add(1, atomic::Ordering::Release);
21942 async move {
21943 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21944 assert_eq!(
21945 params.text_document_position.position, complete_from_position,
21946 "marker `|` position doesn't match",
21947 );
21948 Ok(Some(lsp::CompletionResponse::Array(
21949 completions
21950 .iter()
21951 .map(|(label, new_text)| lsp::CompletionItem {
21952 label: label.to_string(),
21953 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21954 lsp::InsertReplaceEdit {
21955 insert: insert_range,
21956 replace: replace_range,
21957 new_text: new_text.to_string(),
21958 },
21959 )),
21960 ..Default::default()
21961 })
21962 .collect(),
21963 )))
21964 }
21965 });
21966
21967 async move {
21968 request.next().await;
21969 }
21970}
21971
21972fn handle_resolve_completion_request(
21973 cx: &mut EditorLspTestContext,
21974 edits: Option<Vec<(&'static str, &'static str)>>,
21975) -> impl Future<Output = ()> {
21976 let edits = edits.map(|edits| {
21977 edits
21978 .iter()
21979 .map(|(marked_string, new_text)| {
21980 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21981 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21982 lsp::TextEdit::new(replace_range, new_text.to_string())
21983 })
21984 .collect::<Vec<_>>()
21985 });
21986
21987 let mut request =
21988 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21989 let edits = edits.clone();
21990 async move {
21991 Ok(lsp::CompletionItem {
21992 additional_text_edits: edits,
21993 ..Default::default()
21994 })
21995 }
21996 });
21997
21998 async move {
21999 request.next().await;
22000 }
22001}
22002
22003pub(crate) fn update_test_language_settings(
22004 cx: &mut TestAppContext,
22005 f: impl Fn(&mut AllLanguageSettingsContent),
22006) {
22007 cx.update(|cx| {
22008 SettingsStore::update_global(cx, |store, cx| {
22009 store.update_user_settings::<AllLanguageSettings>(cx, f);
22010 });
22011 });
22012}
22013
22014pub(crate) fn update_test_project_settings(
22015 cx: &mut TestAppContext,
22016 f: impl Fn(&mut ProjectSettings),
22017) {
22018 cx.update(|cx| {
22019 SettingsStore::update_global(cx, |store, cx| {
22020 store.update_user_settings::<ProjectSettings>(cx, f);
22021 });
22022 });
22023}
22024
22025pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22026 cx.update(|cx| {
22027 assets::Assets.load_test_fonts(cx);
22028 let store = SettingsStore::test(cx);
22029 cx.set_global(store);
22030 theme::init(theme::LoadThemes::JustBase, cx);
22031 release_channel::init(SemanticVersion::default(), cx);
22032 client::init_settings(cx);
22033 language::init(cx);
22034 Project::init_settings(cx);
22035 workspace::init_settings(cx);
22036 crate::init(cx);
22037 });
22038
22039 update_test_language_settings(cx, f);
22040}
22041
22042#[track_caller]
22043fn assert_hunk_revert(
22044 not_reverted_text_with_selections: &str,
22045 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22046 expected_reverted_text_with_selections: &str,
22047 base_text: &str,
22048 cx: &mut EditorLspTestContext,
22049) {
22050 cx.set_state(not_reverted_text_with_selections);
22051 cx.set_head_text(base_text);
22052 cx.executor().run_until_parked();
22053
22054 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22055 let snapshot = editor.snapshot(window, cx);
22056 let reverted_hunk_statuses = snapshot
22057 .buffer_snapshot
22058 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22059 .map(|hunk| hunk.status().kind)
22060 .collect::<Vec<_>>();
22061
22062 editor.git_restore(&Default::default(), window, cx);
22063 reverted_hunk_statuses
22064 });
22065 cx.executor().run_until_parked();
22066 cx.assert_editor_state(expected_reverted_text_with_selections);
22067 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22068}
22069
22070#[gpui::test(iterations = 10)]
22071async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22072 init_test(cx, |_| {});
22073
22074 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22075 let counter = diagnostic_requests.clone();
22076
22077 let fs = FakeFs::new(cx.executor());
22078 fs.insert_tree(
22079 path!("/a"),
22080 json!({
22081 "first.rs": "fn main() { let a = 5; }",
22082 "second.rs": "// Test file",
22083 }),
22084 )
22085 .await;
22086
22087 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22088 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22089 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22090
22091 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22092 language_registry.add(rust_lang());
22093 let mut fake_servers = language_registry.register_fake_lsp(
22094 "Rust",
22095 FakeLspAdapter {
22096 capabilities: lsp::ServerCapabilities {
22097 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22098 lsp::DiagnosticOptions {
22099 identifier: None,
22100 inter_file_dependencies: true,
22101 workspace_diagnostics: true,
22102 work_done_progress_options: Default::default(),
22103 },
22104 )),
22105 ..Default::default()
22106 },
22107 ..Default::default()
22108 },
22109 );
22110
22111 let editor = workspace
22112 .update(cx, |workspace, window, cx| {
22113 workspace.open_abs_path(
22114 PathBuf::from(path!("/a/first.rs")),
22115 OpenOptions::default(),
22116 window,
22117 cx,
22118 )
22119 })
22120 .unwrap()
22121 .await
22122 .unwrap()
22123 .downcast::<Editor>()
22124 .unwrap();
22125 let fake_server = fake_servers.next().await.unwrap();
22126 let server_id = fake_server.server.server_id();
22127 let mut first_request = fake_server
22128 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22129 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22130 let result_id = Some(new_result_id.to_string());
22131 assert_eq!(
22132 params.text_document.uri,
22133 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22134 );
22135 async move {
22136 Ok(lsp::DocumentDiagnosticReportResult::Report(
22137 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22138 related_documents: None,
22139 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22140 items: Vec::new(),
22141 result_id,
22142 },
22143 }),
22144 ))
22145 }
22146 });
22147
22148 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22149 project.update(cx, |project, cx| {
22150 let buffer_id = editor
22151 .read(cx)
22152 .buffer()
22153 .read(cx)
22154 .as_singleton()
22155 .expect("created a singleton buffer")
22156 .read(cx)
22157 .remote_id();
22158 let buffer_result_id = project
22159 .lsp_store()
22160 .read(cx)
22161 .result_id(server_id, buffer_id, cx);
22162 assert_eq!(expected, buffer_result_id);
22163 });
22164 };
22165
22166 ensure_result_id(None, cx);
22167 cx.executor().advance_clock(Duration::from_millis(60));
22168 cx.executor().run_until_parked();
22169 assert_eq!(
22170 diagnostic_requests.load(atomic::Ordering::Acquire),
22171 1,
22172 "Opening file should trigger diagnostic request"
22173 );
22174 first_request
22175 .next()
22176 .await
22177 .expect("should have sent the first diagnostics pull request");
22178 ensure_result_id(Some("1".to_string()), cx);
22179
22180 // Editing should trigger diagnostics
22181 editor.update_in(cx, |editor, window, cx| {
22182 editor.handle_input("2", window, cx)
22183 });
22184 cx.executor().advance_clock(Duration::from_millis(60));
22185 cx.executor().run_until_parked();
22186 assert_eq!(
22187 diagnostic_requests.load(atomic::Ordering::Acquire),
22188 2,
22189 "Editing should trigger diagnostic request"
22190 );
22191 ensure_result_id(Some("2".to_string()), cx);
22192
22193 // Moving cursor should not trigger diagnostic request
22194 editor.update_in(cx, |editor, window, cx| {
22195 editor.change_selections(None, window, cx, |s| {
22196 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22197 });
22198 });
22199 cx.executor().advance_clock(Duration::from_millis(60));
22200 cx.executor().run_until_parked();
22201 assert_eq!(
22202 diagnostic_requests.load(atomic::Ordering::Acquire),
22203 2,
22204 "Cursor movement should not trigger diagnostic request"
22205 );
22206 ensure_result_id(Some("2".to_string()), cx);
22207 // Multiple rapid edits should be debounced
22208 for _ in 0..5 {
22209 editor.update_in(cx, |editor, window, cx| {
22210 editor.handle_input("x", window, cx)
22211 });
22212 }
22213 cx.executor().advance_clock(Duration::from_millis(60));
22214 cx.executor().run_until_parked();
22215
22216 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22217 assert!(
22218 final_requests <= 4,
22219 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22220 );
22221 ensure_result_id(Some(final_requests.to_string()), cx);
22222}
22223
22224#[gpui::test]
22225async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22226 // Regression test for issue #11671
22227 // Previously, adding a cursor after moving multiple cursors would reset
22228 // the cursor count instead of adding to the existing cursors.
22229 init_test(cx, |_| {});
22230 let mut cx = EditorTestContext::new(cx).await;
22231
22232 // Create a simple buffer with cursor at start
22233 cx.set_state(indoc! {"
22234 ˇaaaa
22235 bbbb
22236 cccc
22237 dddd
22238 eeee
22239 ffff
22240 gggg
22241 hhhh"});
22242
22243 // Add 2 cursors below (so we have 3 total)
22244 cx.update_editor(|editor, window, cx| {
22245 editor.add_selection_below(&Default::default(), window, cx);
22246 editor.add_selection_below(&Default::default(), window, cx);
22247 });
22248
22249 // Verify we have 3 cursors
22250 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22251 assert_eq!(
22252 initial_count, 3,
22253 "Should have 3 cursors after adding 2 below"
22254 );
22255
22256 // Move down one line
22257 cx.update_editor(|editor, window, cx| {
22258 editor.move_down(&MoveDown, window, cx);
22259 });
22260
22261 // Add another cursor below
22262 cx.update_editor(|editor, window, cx| {
22263 editor.add_selection_below(&Default::default(), window, cx);
22264 });
22265
22266 // Should now have 4 cursors (3 original + 1 new)
22267 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22268 assert_eq!(
22269 final_count, 4,
22270 "Should have 4 cursors after moving and adding another"
22271 );
22272}
22273
22274#[gpui::test(iterations = 10)]
22275async fn test_document_colors(cx: &mut TestAppContext) {
22276 let expected_color = Rgba {
22277 r: 0.33,
22278 g: 0.33,
22279 b: 0.33,
22280 a: 0.33,
22281 };
22282
22283 init_test(cx, |_| {});
22284
22285 let fs = FakeFs::new(cx.executor());
22286 fs.insert_tree(
22287 path!("/a"),
22288 json!({
22289 "first.rs": "fn main() { let a = 5; }",
22290 }),
22291 )
22292 .await;
22293
22294 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22295 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22296 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22297
22298 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22299 language_registry.add(rust_lang());
22300 let mut fake_servers = language_registry.register_fake_lsp(
22301 "Rust",
22302 FakeLspAdapter {
22303 capabilities: lsp::ServerCapabilities {
22304 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22305 ..lsp::ServerCapabilities::default()
22306 },
22307 name: "rust-analyzer",
22308 ..FakeLspAdapter::default()
22309 },
22310 );
22311 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22312 "Rust",
22313 FakeLspAdapter {
22314 capabilities: lsp::ServerCapabilities {
22315 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22316 ..lsp::ServerCapabilities::default()
22317 },
22318 name: "not-rust-analyzer",
22319 ..FakeLspAdapter::default()
22320 },
22321 );
22322
22323 let editor = workspace
22324 .update(cx, |workspace, window, cx| {
22325 workspace.open_abs_path(
22326 PathBuf::from(path!("/a/first.rs")),
22327 OpenOptions::default(),
22328 window,
22329 cx,
22330 )
22331 })
22332 .unwrap()
22333 .await
22334 .unwrap()
22335 .downcast::<Editor>()
22336 .unwrap();
22337 let fake_language_server = fake_servers.next().await.unwrap();
22338 let fake_language_server_without_capabilities =
22339 fake_servers_without_capabilities.next().await.unwrap();
22340 let requests_made = Arc::new(AtomicUsize::new(0));
22341 let closure_requests_made = Arc::clone(&requests_made);
22342 let mut color_request_handle = fake_language_server
22343 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22344 let requests_made = Arc::clone(&closure_requests_made);
22345 async move {
22346 assert_eq!(
22347 params.text_document.uri,
22348 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22349 );
22350 requests_made.fetch_add(1, atomic::Ordering::Release);
22351 Ok(vec![
22352 lsp::ColorInformation {
22353 range: lsp::Range {
22354 start: lsp::Position {
22355 line: 0,
22356 character: 0,
22357 },
22358 end: lsp::Position {
22359 line: 0,
22360 character: 1,
22361 },
22362 },
22363 color: lsp::Color {
22364 red: 0.33,
22365 green: 0.33,
22366 blue: 0.33,
22367 alpha: 0.33,
22368 },
22369 },
22370 lsp::ColorInformation {
22371 range: lsp::Range {
22372 start: lsp::Position {
22373 line: 0,
22374 character: 0,
22375 },
22376 end: lsp::Position {
22377 line: 0,
22378 character: 1,
22379 },
22380 },
22381 color: lsp::Color {
22382 red: 0.33,
22383 green: 0.33,
22384 blue: 0.33,
22385 alpha: 0.33,
22386 },
22387 },
22388 ])
22389 }
22390 });
22391
22392 let _handle = fake_language_server_without_capabilities
22393 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22394 panic!("Should not be called");
22395 });
22396 cx.executor().advance_clock(Duration::from_millis(100));
22397 color_request_handle.next().await.unwrap();
22398 cx.run_until_parked();
22399 assert_eq!(
22400 1,
22401 requests_made.load(atomic::Ordering::Acquire),
22402 "Should query for colors once per editor open"
22403 );
22404 editor.update_in(cx, |editor, _, cx| {
22405 assert_eq!(
22406 vec![expected_color],
22407 extract_color_inlays(editor, cx),
22408 "Should have an initial inlay"
22409 );
22410 });
22411
22412 // opening another file in a split should not influence the LSP query counter
22413 workspace
22414 .update(cx, |workspace, window, cx| {
22415 assert_eq!(
22416 workspace.panes().len(),
22417 1,
22418 "Should have one pane with one editor"
22419 );
22420 workspace.activate_pane_at_index(&ActivatePane(1), window, cx);
22421 })
22422 .unwrap();
22423 cx.run_until_parked();
22424 workspace
22425 .update(cx, |workspace, _, cx| {
22426 let panes = workspace.panes();
22427 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
22428 for pane in panes {
22429 let editor = pane
22430 .read(cx)
22431 .active_item()
22432 .and_then(|item| item.downcast::<Editor>())
22433 .expect("Should have opened an editor in each split");
22434 let editor_file = editor
22435 .read(cx)
22436 .buffer()
22437 .read(cx)
22438 .as_singleton()
22439 .expect("test deals with singleton buffers")
22440 .read(cx)
22441 .file()
22442 .expect("test buffese should have a file")
22443 .path();
22444 assert_eq!(
22445 editor_file.as_ref(),
22446 Path::new("first.rs"),
22447 "Both editors should be opened for the same file"
22448 )
22449 }
22450 })
22451 .unwrap();
22452
22453 cx.executor().advance_clock(Duration::from_millis(500));
22454 let save = editor.update_in(cx, |editor, window, cx| {
22455 editor.move_to_end(&MoveToEnd, window, cx);
22456 editor.handle_input("dirty", window, cx);
22457 editor.save(
22458 SaveOptions {
22459 format: true,
22460 autosave: true,
22461 },
22462 project.clone(),
22463 window,
22464 cx,
22465 )
22466 });
22467 save.await.unwrap();
22468
22469 color_request_handle.next().await.unwrap();
22470 cx.run_until_parked();
22471 assert_eq!(
22472 3,
22473 requests_made.load(atomic::Ordering::Acquire),
22474 "Should query for colors once per save and once per formatting after save"
22475 );
22476
22477 drop(editor);
22478 let close = workspace
22479 .update(cx, |workspace, window, cx| {
22480 workspace.active_pane().update(cx, |pane, cx| {
22481 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22482 })
22483 })
22484 .unwrap();
22485 close.await.unwrap();
22486 let close = workspace
22487 .update(cx, |workspace, window, cx| {
22488 workspace.active_pane().update(cx, |pane, cx| {
22489 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22490 })
22491 })
22492 .unwrap();
22493 close.await.unwrap();
22494 assert_eq!(
22495 3,
22496 requests_made.load(atomic::Ordering::Acquire),
22497 "After saving and closing all editors, no extra requests should be made"
22498 );
22499 workspace
22500 .update(cx, |workspace, _, cx| {
22501 assert!(
22502 workspace.active_item(cx).is_none(),
22503 "Should close all editors"
22504 )
22505 })
22506 .unwrap();
22507
22508 workspace
22509 .update(cx, |workspace, window, cx| {
22510 workspace.active_pane().update(cx, |pane, cx| {
22511 pane.navigate_backward(window, cx);
22512 })
22513 })
22514 .unwrap();
22515 cx.executor().advance_clock(Duration::from_millis(100));
22516 cx.run_until_parked();
22517 let editor = workspace
22518 .update(cx, |workspace, _, cx| {
22519 workspace
22520 .active_item(cx)
22521 .expect("Should have reopened the editor again after navigating back")
22522 .downcast::<Editor>()
22523 .expect("Should be an editor")
22524 })
22525 .unwrap();
22526 color_request_handle.next().await.unwrap();
22527 assert_eq!(
22528 3,
22529 requests_made.load(atomic::Ordering::Acquire),
22530 "Cache should be reused on buffer close and reopen"
22531 );
22532 editor.update(cx, |editor, cx| {
22533 assert_eq!(
22534 vec![expected_color],
22535 extract_color_inlays(editor, cx),
22536 "Should have an initial inlay"
22537 );
22538 });
22539}
22540
22541#[track_caller]
22542fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22543 editor
22544 .all_inlays(cx)
22545 .into_iter()
22546 .filter_map(|inlay| inlay.get_color())
22547 .map(Rgba::from)
22548 .collect()
22549}