1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use languages::rust_lang;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::build_editor_with_project;
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_item_view::InvalidItemView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(
224 editor.selections.ranges(&editor.display_snapshot(cx)),
225 vec![4..4]
226 );
227
228 editor.start_transaction_at(now, window, cx);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([4..5])
231 });
232 editor.insert("e", window, cx);
233 editor.end_transaction_at(now, cx);
234 assert_eq!(editor.text(cx), "12cde6");
235 assert_eq!(
236 editor.selections.ranges(&editor.display_snapshot(cx)),
237 vec![5..5]
238 );
239
240 now += group_interval + Duration::from_millis(1);
241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
242 s.select_ranges([2..2])
243 });
244
245 // Simulate an edit in another editor
246 buffer.update(cx, |buffer, cx| {
247 buffer.start_transaction_at(now, cx);
248 buffer.edit([(0..1, "a")], None, cx);
249 buffer.edit([(1..1, "b")], None, cx);
250 buffer.end_transaction_at(now, cx);
251 });
252
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(
255 editor.selections.ranges(&editor.display_snapshot(cx)),
256 vec![3..3]
257 );
258
259 // Last transaction happened past the group interval in a different editor.
260 // Undo it individually and don't restore selections.
261 editor.undo(&Undo, window, cx);
262 assert_eq!(editor.text(cx), "12cde6");
263 assert_eq!(
264 editor.selections.ranges(&editor.display_snapshot(cx)),
265 vec![2..2]
266 );
267
268 // First two transactions happened within the group interval in this editor.
269 // Undo them together and restore selections.
270 editor.undo(&Undo, window, cx);
271 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
272 assert_eq!(editor.text(cx), "123456");
273 assert_eq!(
274 editor.selections.ranges(&editor.display_snapshot(cx)),
275 vec![0..0]
276 );
277
278 // Redo the first two transactions together.
279 editor.redo(&Redo, window, cx);
280 assert_eq!(editor.text(cx), "12cde6");
281 assert_eq!(
282 editor.selections.ranges(&editor.display_snapshot(cx)),
283 vec![5..5]
284 );
285
286 // Redo the last transaction on its own.
287 editor.redo(&Redo, window, cx);
288 assert_eq!(editor.text(cx), "ab2cde6");
289 assert_eq!(
290 editor.selections.ranges(&editor.display_snapshot(cx)),
291 vec![6..6]
292 );
293
294 // Test empty transactions.
295 editor.start_transaction_at(now, window, cx);
296 editor.end_transaction_at(now, cx);
297 editor.undo(&Undo, window, cx);
298 assert_eq!(editor.text(cx), "12cde6");
299 });
300}
301
302#[gpui::test]
303fn test_ime_composition(cx: &mut TestAppContext) {
304 init_test(cx, |_| {});
305
306 let buffer = cx.new(|cx| {
307 let mut buffer = language::Buffer::local("abcde", cx);
308 // Ensure automatic grouping doesn't occur.
309 buffer.set_group_interval(Duration::ZERO);
310 buffer
311 });
312
313 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
314 cx.add_window(|window, cx| {
315 let mut editor = build_editor(buffer.clone(), window, cx);
316
317 // Start a new IME composition.
318 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
319 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
320 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
321 assert_eq!(editor.text(cx), "äbcde");
322 assert_eq!(
323 editor.marked_text_ranges(cx),
324 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
325 );
326
327 // Finalize IME composition.
328 editor.replace_text_in_range(None, "ā", window, cx);
329 assert_eq!(editor.text(cx), "ābcde");
330 assert_eq!(editor.marked_text_ranges(cx), None);
331
332 // IME composition edits are grouped and are undone/redone at once.
333 editor.undo(&Default::default(), window, cx);
334 assert_eq!(editor.text(cx), "abcde");
335 assert_eq!(editor.marked_text_ranges(cx), None);
336 editor.redo(&Default::default(), window, cx);
337 assert_eq!(editor.text(cx), "ābcde");
338 assert_eq!(editor.marked_text_ranges(cx), None);
339
340 // Start a new IME composition.
341 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
342 assert_eq!(
343 editor.marked_text_ranges(cx),
344 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
345 );
346
347 // Undoing during an IME composition cancels it.
348 editor.undo(&Default::default(), window, cx);
349 assert_eq!(editor.text(cx), "ābcde");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
353 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
354 assert_eq!(editor.text(cx), "ābcdè");
355 assert_eq!(
356 editor.marked_text_ranges(cx),
357 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
358 );
359
360 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
361 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
362 assert_eq!(editor.text(cx), "ābcdę");
363 assert_eq!(editor.marked_text_ranges(cx), None);
364
365 // Start a new IME composition with multiple cursors.
366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
367 s.select_ranges([
368 OffsetUtf16(1)..OffsetUtf16(1),
369 OffsetUtf16(3)..OffsetUtf16(3),
370 OffsetUtf16(5)..OffsetUtf16(5),
371 ])
372 });
373 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
374 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
375 assert_eq!(
376 editor.marked_text_ranges(cx),
377 Some(vec![
378 OffsetUtf16(0)..OffsetUtf16(3),
379 OffsetUtf16(4)..OffsetUtf16(7),
380 OffsetUtf16(8)..OffsetUtf16(11)
381 ])
382 );
383
384 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
385 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
386 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
387 assert_eq!(
388 editor.marked_text_ranges(cx),
389 Some(vec![
390 OffsetUtf16(1)..OffsetUtf16(2),
391 OffsetUtf16(5)..OffsetUtf16(6),
392 OffsetUtf16(9)..OffsetUtf16(10)
393 ])
394 );
395
396 // Finalize IME composition with multiple cursors.
397 editor.replace_text_in_range(Some(9..10), "2", window, cx);
398 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
399 assert_eq!(editor.marked_text_ranges(cx), None);
400
401 editor
402 });
403}
404
405#[gpui::test]
406fn test_selection_with_mouse(cx: &mut TestAppContext) {
407 init_test(cx, |_| {});
408
409 let editor = cx.add_window(|window, cx| {
410 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
411 build_editor(buffer, window, cx)
412 });
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
416 });
417 assert_eq!(
418 editor
419 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
420 .unwrap(),
421 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
422 );
423
424 _ = editor.update(cx, |editor, window, cx| {
425 editor.update_selection(
426 DisplayPoint::new(DisplayRow(3), 3),
427 0,
428 gpui::Point::<f32>::default(),
429 window,
430 cx,
431 );
432 });
433
434 assert_eq!(
435 editor
436 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
437 .unwrap(),
438 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
439 );
440
441 _ = editor.update(cx, |editor, window, cx| {
442 editor.update_selection(
443 DisplayPoint::new(DisplayRow(1), 1),
444 0,
445 gpui::Point::<f32>::default(),
446 window,
447 cx,
448 );
449 });
450
451 assert_eq!(
452 editor
453 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
454 .unwrap(),
455 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
456 );
457
458 _ = editor.update(cx, |editor, window, cx| {
459 editor.end_selection(window, cx);
460 editor.update_selection(
461 DisplayPoint::new(DisplayRow(3), 3),
462 0,
463 gpui::Point::<f32>::default(),
464 window,
465 cx,
466 );
467 });
468
469 assert_eq!(
470 editor
471 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
472 .unwrap(),
473 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
478 editor.update_selection(
479 DisplayPoint::new(DisplayRow(0), 0),
480 0,
481 gpui::Point::<f32>::default(),
482 window,
483 cx,
484 );
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
493 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.end_selection(window, cx);
499 });
500
501 assert_eq!(
502 editor
503 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
504 .unwrap(),
505 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
506 );
507}
508
509#[gpui::test]
510fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
511 init_test(cx, |_| {});
512
513 let editor = cx.add_window(|window, cx| {
514 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
515 build_editor(buffer, window, cx)
516 });
517
518 _ = editor.update(cx, |editor, window, cx| {
519 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.end_selection(window, cx);
532 });
533
534 assert_eq!(
535 editor
536 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
537 .unwrap(),
538 [
539 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
540 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
541 ]
542 );
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.end_selection(window, cx);
550 });
551
552 assert_eq!(
553 editor
554 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
555 .unwrap(),
556 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
557 );
558}
559
560#[gpui::test]
561fn test_canceling_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575 });
576
577 _ = editor.update(cx, |editor, window, cx| {
578 editor.update_selection(
579 DisplayPoint::new(DisplayRow(3), 3),
580 0,
581 gpui::Point::<f32>::default(),
582 window,
583 cx,
584 );
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
588 );
589 });
590
591 _ = editor.update(cx, |editor, window, cx| {
592 editor.cancel(&Cancel, window, cx);
593 editor.update_selection(
594 DisplayPoint::new(DisplayRow(1), 1),
595 0,
596 gpui::Point::<f32>::default(),
597 window,
598 cx,
599 );
600 assert_eq!(
601 editor.selections.display_ranges(cx),
602 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
603 );
604 });
605}
606
607#[gpui::test]
608fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
609 init_test(cx, |_| {});
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
618 assert_eq!(
619 editor.selections.display_ranges(cx),
620 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
621 );
622
623 editor.move_down(&Default::default(), window, cx);
624 assert_eq!(
625 editor.selections.display_ranges(cx),
626 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
627 );
628
629 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
630 assert_eq!(
631 editor.selections.display_ranges(cx),
632 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
633 );
634
635 editor.move_up(&Default::default(), window, cx);
636 assert_eq!(
637 editor.selections.display_ranges(cx),
638 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
639 );
640 });
641}
642
643#[gpui::test]
644fn test_extending_selection(cx: &mut TestAppContext) {
645 init_test(cx, |_| {});
646
647 let editor = cx.add_window(|window, cx| {
648 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
649 build_editor(buffer, window, cx)
650 });
651
652 _ = editor.update(cx, |editor, window, cx| {
653 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
654 editor.end_selection(window, cx);
655 assert_eq!(
656 editor.selections.display_ranges(cx),
657 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
658 );
659
660 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 editor.selections.display_ranges(cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
670 assert_eq!(
671 editor.selections.display_ranges(cx),
672 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
673 );
674
675 editor.update_selection(
676 DisplayPoint::new(DisplayRow(0), 1),
677 0,
678 gpui::Point::<f32>::default(),
679 window,
680 cx,
681 );
682 editor.end_selection(window, cx);
683 assert_eq!(
684 editor.selections.display_ranges(cx),
685 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
686 );
687
688 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
689 editor.end_selection(window, cx);
690 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
691 editor.end_selection(window, cx);
692 assert_eq!(
693 editor.selections.display_ranges(cx),
694 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
695 );
696
697 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
698 assert_eq!(
699 editor.selections.display_ranges(cx),
700 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
701 );
702
703 editor.update_selection(
704 DisplayPoint::new(DisplayRow(0), 6),
705 0,
706 gpui::Point::<f32>::default(),
707 window,
708 cx,
709 );
710 assert_eq!(
711 editor.selections.display_ranges(cx),
712 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
713 );
714
715 editor.update_selection(
716 DisplayPoint::new(DisplayRow(0), 1),
717 0,
718 gpui::Point::<f32>::default(),
719 window,
720 cx,
721 );
722 editor.end_selection(window, cx);
723 assert_eq!(
724 editor.selections.display_ranges(cx),
725 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
726 );
727 });
728}
729
730#[gpui::test]
731fn test_clone(cx: &mut TestAppContext) {
732 init_test(cx, |_| {});
733
734 let (text, selection_ranges) = marked_text_ranges(
735 indoc! {"
736 one
737 two
738 threeˇ
739 four
740 fiveˇ
741 "},
742 true,
743 );
744
745 let editor = cx.add_window(|window, cx| {
746 let buffer = MultiBuffer::build_simple(&text, cx);
747 build_editor(buffer, window, cx)
748 });
749
750 _ = editor.update(cx, |editor, window, cx| {
751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
752 s.select_ranges(selection_ranges.clone())
753 });
754 editor.fold_creases(
755 vec![
756 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
757 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
758 ],
759 true,
760 window,
761 cx,
762 );
763 });
764
765 let cloned_editor = editor
766 .update(cx, |editor, _, cx| {
767 cx.open_window(Default::default(), |window, cx| {
768 cx.new(|cx| editor.clone(window, cx))
769 })
770 })
771 .unwrap()
772 .unwrap();
773
774 let snapshot = editor
775 .update(cx, |e, window, cx| e.snapshot(window, cx))
776 .unwrap();
777 let cloned_snapshot = cloned_editor
778 .update(cx, |e, window, cx| e.snapshot(window, cx))
779 .unwrap();
780
781 assert_eq!(
782 cloned_editor
783 .update(cx, |e, _, cx| e.display_text(cx))
784 .unwrap(),
785 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
786 );
787 assert_eq!(
788 cloned_snapshot
789 .folds_in_range(0..text.len())
790 .collect::<Vec<_>>(),
791 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
792 );
793 assert_set_eq!(
794 cloned_editor
795 .update(cx, |editor, _, cx| editor
796 .selections
797 .ranges::<Point>(&editor.display_snapshot(cx)))
798 .unwrap(),
799 editor
800 .update(cx, |editor, _, cx| editor
801 .selections
802 .ranges(&editor.display_snapshot(cx)))
803 .unwrap()
804 );
805 assert_set_eq!(
806 cloned_editor
807 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
808 .unwrap(),
809 editor
810 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
811 .unwrap()
812 );
813}
814
815#[gpui::test]
816async fn test_navigation_history(cx: &mut TestAppContext) {
817 init_test(cx, |_| {});
818
819 use workspace::item::Item;
820
821 let fs = FakeFs::new(cx.executor());
822 let project = Project::test(fs, [], cx).await;
823 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
824 let pane = workspace
825 .update(cx, |workspace, _, _| workspace.active_pane().clone())
826 .unwrap();
827
828 _ = workspace.update(cx, |_v, window, cx| {
829 cx.new(|cx| {
830 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
831 let mut editor = build_editor(buffer, window, cx);
832 let handle = cx.entity();
833 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
834
835 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
836 editor.nav_history.as_mut().unwrap().pop_backward(cx)
837 }
838
839 // Move the cursor a small distance.
840 // Nothing is added to the navigation history.
841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
842 s.select_display_ranges([
843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
844 ])
845 });
846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
847 s.select_display_ranges([
848 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
849 ])
850 });
851 assert!(pop_history(&mut editor, cx).is_none());
852
853 // Move the cursor a large distance.
854 // The history can jump back to the previous position.
855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
856 s.select_display_ranges([
857 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
858 ])
859 });
860 let nav_entry = pop_history(&mut editor, cx).unwrap();
861 editor.navigate(nav_entry.data.unwrap(), window, cx);
862 assert_eq!(nav_entry.item.id(), cx.entity_id());
863 assert_eq!(
864 editor.selections.display_ranges(cx),
865 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
866 );
867 assert!(pop_history(&mut editor, cx).is_none());
868
869 // Move the cursor a small distance via the mouse.
870 // Nothing is added to the navigation history.
871 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
872 editor.end_selection(window, cx);
873 assert_eq!(
874 editor.selections.display_ranges(cx),
875 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
876 );
877 assert!(pop_history(&mut editor, cx).is_none());
878
879 // Move the cursor a large distance via the mouse.
880 // The history can jump back to the previous position.
881 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
882 editor.end_selection(window, cx);
883 assert_eq!(
884 editor.selections.display_ranges(cx),
885 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
886 );
887 let nav_entry = pop_history(&mut editor, cx).unwrap();
888 editor.navigate(nav_entry.data.unwrap(), window, cx);
889 assert_eq!(nav_entry.item.id(), cx.entity_id());
890 assert_eq!(
891 editor.selections.display_ranges(cx),
892 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
893 );
894 assert!(pop_history(&mut editor, cx).is_none());
895
896 // Set scroll position to check later
897 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
898 let original_scroll_position = editor.scroll_manager.anchor();
899
900 // Jump to the end of the document and adjust scroll
901 editor.move_to_end(&MoveToEnd, window, cx);
902 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
903 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
904
905 let nav_entry = pop_history(&mut editor, cx).unwrap();
906 editor.navigate(nav_entry.data.unwrap(), window, cx);
907 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
908
909 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
910 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
911 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
912 let invalid_point = Point::new(9999, 0);
913 editor.navigate(
914 Box::new(NavigationData {
915 cursor_anchor: invalid_anchor,
916 cursor_position: invalid_point,
917 scroll_anchor: ScrollAnchor {
918 anchor: invalid_anchor,
919 offset: Default::default(),
920 },
921 scroll_top_row: invalid_point.row,
922 }),
923 window,
924 cx,
925 );
926 assert_eq!(
927 editor.selections.display_ranges(cx),
928 &[editor.max_point(cx)..editor.max_point(cx)]
929 );
930 assert_eq!(
931 editor.scroll_position(cx),
932 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
933 );
934
935 editor
936 })
937 });
938}
939
940#[gpui::test]
941fn test_cancel(cx: &mut TestAppContext) {
942 init_test(cx, |_| {});
943
944 let editor = cx.add_window(|window, cx| {
945 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
946 build_editor(buffer, window, cx)
947 });
948
949 _ = editor.update(cx, |editor, window, cx| {
950 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
951 editor.update_selection(
952 DisplayPoint::new(DisplayRow(1), 1),
953 0,
954 gpui::Point::<f32>::default(),
955 window,
956 cx,
957 );
958 editor.end_selection(window, cx);
959
960 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
961 editor.update_selection(
962 DisplayPoint::new(DisplayRow(0), 3),
963 0,
964 gpui::Point::<f32>::default(),
965 window,
966 cx,
967 );
968 editor.end_selection(window, cx);
969 assert_eq!(
970 editor.selections.display_ranges(cx),
971 [
972 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
973 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
974 ]
975 );
976 });
977
978 _ = editor.update(cx, |editor, window, cx| {
979 editor.cancel(&Cancel, window, cx);
980 assert_eq!(
981 editor.selections.display_ranges(cx),
982 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
983 );
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.cancel(&Cancel, window, cx);
988 assert_eq!(
989 editor.selections.display_ranges(cx),
990 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
991 );
992 });
993}
994
995#[gpui::test]
996fn test_fold_action(cx: &mut TestAppContext) {
997 init_test(cx, |_| {});
998
999 let editor = cx.add_window(|window, cx| {
1000 let buffer = MultiBuffer::build_simple(
1001 &"
1002 impl Foo {
1003 // Hello!
1004
1005 fn a() {
1006 1
1007 }
1008
1009 fn b() {
1010 2
1011 }
1012
1013 fn c() {
1014 3
1015 }
1016 }
1017 "
1018 .unindent(),
1019 cx,
1020 );
1021 build_editor(buffer, window, cx)
1022 });
1023
1024 _ = editor.update(cx, |editor, window, cx| {
1025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1026 s.select_display_ranges([
1027 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1028 ]);
1029 });
1030 editor.fold(&Fold, window, cx);
1031 assert_eq!(
1032 editor.display_text(cx),
1033 "
1034 impl Foo {
1035 // Hello!
1036
1037 fn a() {
1038 1
1039 }
1040
1041 fn b() {⋯
1042 }
1043
1044 fn c() {⋯
1045 }
1046 }
1047 "
1048 .unindent(),
1049 );
1050
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {⋯
1056 }
1057 "
1058 .unindent(),
1059 );
1060
1061 editor.unfold_lines(&UnfoldLines, window, cx);
1062 assert_eq!(
1063 editor.display_text(cx),
1064 "
1065 impl Foo {
1066 // Hello!
1067
1068 fn a() {
1069 1
1070 }
1071
1072 fn b() {⋯
1073 }
1074
1075 fn c() {⋯
1076 }
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 editor.buffer.read(cx).read(cx).text()
1086 );
1087 });
1088}
1089
1090#[gpui::test]
1091fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1092 init_test(cx, |_| {});
1093
1094 let editor = cx.add_window(|window, cx| {
1095 let buffer = MultiBuffer::build_simple(
1096 &"
1097 class Foo:
1098 # Hello!
1099
1100 def a():
1101 print(1)
1102
1103 def b():
1104 print(2)
1105
1106 def c():
1107 print(3)
1108 "
1109 .unindent(),
1110 cx,
1111 );
1112 build_editor(buffer, window, cx)
1113 });
1114
1115 _ = editor.update(cx, |editor, window, cx| {
1116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1117 s.select_display_ranges([
1118 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1119 ]);
1120 });
1121 editor.fold(&Fold, window, cx);
1122 assert_eq!(
1123 editor.display_text(cx),
1124 "
1125 class Foo:
1126 # Hello!
1127
1128 def a():
1129 print(1)
1130
1131 def b():⋯
1132
1133 def c():⋯
1134 "
1135 .unindent(),
1136 );
1137
1138 editor.fold(&Fold, window, cx);
1139 assert_eq!(
1140 editor.display_text(cx),
1141 "
1142 class Foo:⋯
1143 "
1144 .unindent(),
1145 );
1146
1147 editor.unfold_lines(&UnfoldLines, window, cx);
1148 assert_eq!(
1149 editor.display_text(cx),
1150 "
1151 class Foo:
1152 # Hello!
1153
1154 def a():
1155 print(1)
1156
1157 def b():⋯
1158
1159 def c():⋯
1160 "
1161 .unindent(),
1162 );
1163
1164 editor.unfold_lines(&UnfoldLines, window, cx);
1165 assert_eq!(
1166 editor.display_text(cx),
1167 editor.buffer.read(cx).read(cx).text()
1168 );
1169 });
1170}
1171
1172#[gpui::test]
1173fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1174 init_test(cx, |_| {});
1175
1176 let editor = cx.add_window(|window, cx| {
1177 let buffer = MultiBuffer::build_simple(
1178 &"
1179 class Foo:
1180 # Hello!
1181
1182 def a():
1183 print(1)
1184
1185 def b():
1186 print(2)
1187
1188
1189 def c():
1190 print(3)
1191
1192
1193 "
1194 .unindent(),
1195 cx,
1196 );
1197 build_editor(buffer, window, cx)
1198 });
1199
1200 _ = editor.update(cx, |editor, window, cx| {
1201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1202 s.select_display_ranges([
1203 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1204 ]);
1205 });
1206 editor.fold(&Fold, window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():⋯
1217
1218
1219 def c():⋯
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.fold(&Fold, window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:⋯
1231
1232
1233 "
1234 .unindent(),
1235 );
1236
1237 editor.unfold_lines(&UnfoldLines, window, cx);
1238 assert_eq!(
1239 editor.display_text(cx),
1240 "
1241 class Foo:
1242 # Hello!
1243
1244 def a():
1245 print(1)
1246
1247 def b():⋯
1248
1249
1250 def c():⋯
1251
1252
1253 "
1254 .unindent(),
1255 );
1256
1257 editor.unfold_lines(&UnfoldLines, window, cx);
1258 assert_eq!(
1259 editor.display_text(cx),
1260 editor.buffer.read(cx).read(cx).text()
1261 );
1262 });
1263}
1264
1265#[gpui::test]
1266fn test_fold_at_level(cx: &mut TestAppContext) {
1267 init_test(cx, |_| {});
1268
1269 let editor = cx.add_window(|window, cx| {
1270 let buffer = MultiBuffer::build_simple(
1271 &"
1272 class Foo:
1273 # Hello!
1274
1275 def a():
1276 print(1)
1277
1278 def b():
1279 print(2)
1280
1281
1282 class Bar:
1283 # World!
1284
1285 def a():
1286 print(1)
1287
1288 def b():
1289 print(2)
1290
1291
1292 "
1293 .unindent(),
1294 cx,
1295 );
1296 build_editor(buffer, window, cx)
1297 });
1298
1299 _ = editor.update(cx, |editor, window, cx| {
1300 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1301 assert_eq!(
1302 editor.display_text(cx),
1303 "
1304 class Foo:
1305 # Hello!
1306
1307 def a():⋯
1308
1309 def b():⋯
1310
1311
1312 class Bar:
1313 # World!
1314
1315 def a():⋯
1316
1317 def b():⋯
1318
1319
1320 "
1321 .unindent(),
1322 );
1323
1324 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1325 assert_eq!(
1326 editor.display_text(cx),
1327 "
1328 class Foo:⋯
1329
1330
1331 class Bar:⋯
1332
1333
1334 "
1335 .unindent(),
1336 );
1337
1338 editor.unfold_all(&UnfoldAll, window, cx);
1339 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1340 assert_eq!(
1341 editor.display_text(cx),
1342 "
1343 class Foo:
1344 # Hello!
1345
1346 def a():
1347 print(1)
1348
1349 def b():
1350 print(2)
1351
1352
1353 class Bar:
1354 # World!
1355
1356 def a():
1357 print(1)
1358
1359 def b():
1360 print(2)
1361
1362
1363 "
1364 .unindent(),
1365 );
1366
1367 assert_eq!(
1368 editor.display_text(cx),
1369 editor.buffer.read(cx).read(cx).text()
1370 );
1371 let (_, positions) = marked_text_ranges(
1372 &"
1373 class Foo:
1374 # Hello!
1375
1376 def a():
1377 print(1)
1378
1379 def b():
1380 p«riˇ»nt(2)
1381
1382
1383 class Bar:
1384 # World!
1385
1386 def a():
1387 «ˇprint(1)
1388
1389 def b():
1390 print(2)»
1391
1392
1393 "
1394 .unindent(),
1395 true,
1396 );
1397
1398 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1399 s.select_ranges(positions)
1400 });
1401
1402 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1403 assert_eq!(
1404 editor.display_text(cx),
1405 "
1406 class Foo:
1407 # Hello!
1408
1409 def a():⋯
1410
1411 def b():
1412 print(2)
1413
1414
1415 class Bar:
1416 # World!
1417
1418 def a():
1419 print(1)
1420
1421 def b():
1422 print(2)
1423
1424
1425 "
1426 .unindent(),
1427 );
1428 });
1429}
1430
1431#[gpui::test]
1432fn test_move_cursor(cx: &mut TestAppContext) {
1433 init_test(cx, |_| {});
1434
1435 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1436 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1437
1438 buffer.update(cx, |buffer, cx| {
1439 buffer.edit(
1440 vec![
1441 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1442 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1443 ],
1444 None,
1445 cx,
1446 );
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1458 );
1459
1460 editor.move_right(&MoveRight, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1464 );
1465
1466 editor.move_left(&MoveLeft, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1470 );
1471
1472 editor.move_up(&MoveUp, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1476 );
1477
1478 editor.move_to_end(&MoveToEnd, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1482 );
1483
1484 editor.move_to_beginning(&MoveToBeginning, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1488 );
1489
1490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1491 s.select_display_ranges([
1492 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1493 ]);
1494 });
1495 editor.select_to_beginning(&SelectToBeginning, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1499 );
1500
1501 editor.select_to_end(&SelectToEnd, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1505 );
1506 });
1507}
1508
1509#[gpui::test]
1510fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1511 init_test(cx, |_| {});
1512
1513 let editor = cx.add_window(|window, cx| {
1514 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1515 build_editor(buffer, window, cx)
1516 });
1517
1518 assert_eq!('🟥'.len_utf8(), 4);
1519 assert_eq!('α'.len_utf8(), 2);
1520
1521 _ = editor.update(cx, |editor, window, cx| {
1522 editor.fold_creases(
1523 vec![
1524 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1525 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1526 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1527 ],
1528 true,
1529 window,
1530 cx,
1531 );
1532 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1533
1534 editor.move_right(&MoveRight, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(0, "🟥".len())]
1538 );
1539 editor.move_right(&MoveRight, window, cx);
1540 assert_eq!(
1541 editor.selections.display_ranges(cx),
1542 &[empty_range(0, "🟥🟧".len())]
1543 );
1544 editor.move_right(&MoveRight, window, cx);
1545 assert_eq!(
1546 editor.selections.display_ranges(cx),
1547 &[empty_range(0, "🟥🟧⋯".len())]
1548 );
1549
1550 editor.move_down(&MoveDown, window, cx);
1551 assert_eq!(
1552 editor.selections.display_ranges(cx),
1553 &[empty_range(1, "ab⋯e".len())]
1554 );
1555 editor.move_left(&MoveLeft, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[empty_range(1, "ab⋯".len())]
1559 );
1560 editor.move_left(&MoveLeft, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(1, "ab".len())]
1564 );
1565 editor.move_left(&MoveLeft, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[empty_range(1, "a".len())]
1569 );
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[empty_range(2, "α".len())]
1575 );
1576 editor.move_right(&MoveRight, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[empty_range(2, "αβ".len())]
1580 );
1581 editor.move_right(&MoveRight, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[empty_range(2, "αβ⋯".len())]
1585 );
1586 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(
1588 editor.selections.display_ranges(cx),
1589 &[empty_range(2, "αβ⋯ε".len())]
1590 );
1591
1592 editor.move_up(&MoveUp, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[empty_range(1, "ab⋯e".len())]
1596 );
1597 editor.move_down(&MoveDown, window, cx);
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[empty_range(2, "αβ⋯ε".len())]
1601 );
1602 editor.move_up(&MoveUp, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[empty_range(1, "ab⋯e".len())]
1606 );
1607
1608 editor.move_up(&MoveUp, window, cx);
1609 assert_eq!(
1610 editor.selections.display_ranges(cx),
1611 &[empty_range(0, "🟥🟧".len())]
1612 );
1613 editor.move_left(&MoveLeft, window, cx);
1614 assert_eq!(
1615 editor.selections.display_ranges(cx),
1616 &[empty_range(0, "🟥".len())]
1617 );
1618 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(
1620 editor.selections.display_ranges(cx),
1621 &[empty_range(0, "".len())]
1622 );
1623 });
1624}
1625
1626#[gpui::test]
1627fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1628 init_test(cx, |_| {});
1629
1630 let editor = cx.add_window(|window, cx| {
1631 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1632 build_editor(buffer, window, cx)
1633 });
1634 _ = editor.update(cx, |editor, window, cx| {
1635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1636 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1637 });
1638
1639 // moving above start of document should move selection to start of document,
1640 // but the next move down should still be at the original goal_x
1641 editor.move_up(&MoveUp, window, cx);
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[empty_range(0, "".len())]
1645 );
1646
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[empty_range(1, "abcd".len())]
1651 );
1652
1653 editor.move_down(&MoveDown, window, cx);
1654 assert_eq!(
1655 editor.selections.display_ranges(cx),
1656 &[empty_range(2, "αβγ".len())]
1657 );
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[empty_range(3, "abcd".len())]
1663 );
1664
1665 editor.move_down(&MoveDown, window, cx);
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1669 );
1670
1671 // moving past end of document should not change goal_x
1672 editor.move_down(&MoveDown, window, cx);
1673 assert_eq!(
1674 editor.selections.display_ranges(cx),
1675 &[empty_range(5, "".len())]
1676 );
1677
1678 editor.move_down(&MoveDown, window, cx);
1679 assert_eq!(
1680 editor.selections.display_ranges(cx),
1681 &[empty_range(5, "".len())]
1682 );
1683
1684 editor.move_up(&MoveUp, window, cx);
1685 assert_eq!(
1686 editor.selections.display_ranges(cx),
1687 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1688 );
1689
1690 editor.move_up(&MoveUp, window, cx);
1691 assert_eq!(
1692 editor.selections.display_ranges(cx),
1693 &[empty_range(3, "abcd".len())]
1694 );
1695
1696 editor.move_up(&MoveUp, window, cx);
1697 assert_eq!(
1698 editor.selections.display_ranges(cx),
1699 &[empty_range(2, "αβγ".len())]
1700 );
1701 });
1702}
1703
1704#[gpui::test]
1705fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1706 init_test(cx, |_| {});
1707 let move_to_beg = MoveToBeginningOfLine {
1708 stop_at_soft_wraps: true,
1709 stop_at_indent: true,
1710 };
1711
1712 let delete_to_beg = DeleteToBeginningOfLine {
1713 stop_at_indent: false,
1714 };
1715
1716 let move_to_end = MoveToEndOfLine {
1717 stop_at_soft_wraps: true,
1718 };
1719
1720 let editor = cx.add_window(|window, cx| {
1721 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1722 build_editor(buffer, window, cx)
1723 });
1724 _ = editor.update(cx, |editor, window, cx| {
1725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1726 s.select_display_ranges([
1727 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1728 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1729 ]);
1730 });
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1735 assert_eq!(
1736 editor.selections.display_ranges(cx),
1737 &[
1738 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1740 ]
1741 );
1742 });
1743
1744 _ = editor.update(cx, |editor, window, cx| {
1745 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1746 assert_eq!(
1747 editor.selections.display_ranges(cx),
1748 &[
1749 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1750 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1751 ]
1752 );
1753 });
1754
1755 _ = editor.update(cx, |editor, window, cx| {
1756 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1757 assert_eq!(
1758 editor.selections.display_ranges(cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.move_to_end_of_line(&move_to_end, window, cx);
1768 assert_eq!(
1769 editor.selections.display_ranges(cx),
1770 &[
1771 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1772 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1773 ]
1774 );
1775 });
1776
1777 // Moving to the end of line again is a no-op.
1778 _ = editor.update(cx, |editor, window, cx| {
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 editor.selections.display_ranges(cx),
1782 &[
1783 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1784 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1785 ]
1786 );
1787 });
1788
1789 _ = editor.update(cx, |editor, window, cx| {
1790 editor.move_left(&MoveLeft, window, cx);
1791 editor.select_to_beginning_of_line(
1792 &SelectToBeginningOfLine {
1793 stop_at_soft_wraps: true,
1794 stop_at_indent: true,
1795 },
1796 window,
1797 cx,
1798 );
1799 assert_eq!(
1800 editor.selections.display_ranges(cx),
1801 &[
1802 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1803 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1804 ]
1805 );
1806 });
1807
1808 _ = editor.update(cx, |editor, window, cx| {
1809 editor.select_to_beginning_of_line(
1810 &SelectToBeginningOfLine {
1811 stop_at_soft_wraps: true,
1812 stop_at_indent: true,
1813 },
1814 window,
1815 cx,
1816 );
1817 assert_eq!(
1818 editor.selections.display_ranges(cx),
1819 &[
1820 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1822 ]
1823 );
1824 });
1825
1826 _ = editor.update(cx, |editor, window, cx| {
1827 editor.select_to_beginning_of_line(
1828 &SelectToBeginningOfLine {
1829 stop_at_soft_wraps: true,
1830 stop_at_indent: true,
1831 },
1832 window,
1833 cx,
1834 );
1835 assert_eq!(
1836 editor.selections.display_ranges(cx),
1837 &[
1838 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1839 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1840 ]
1841 );
1842 });
1843
1844 _ = editor.update(cx, |editor, window, cx| {
1845 editor.select_to_end_of_line(
1846 &SelectToEndOfLine {
1847 stop_at_soft_wraps: true,
1848 },
1849 window,
1850 cx,
1851 );
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1856 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1857 ]
1858 );
1859 });
1860
1861 _ = editor.update(cx, |editor, window, cx| {
1862 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1863 assert_eq!(editor.display_text(cx), "ab\n de");
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1869 ]
1870 );
1871 });
1872
1873 _ = editor.update(cx, |editor, window, cx| {
1874 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1875 assert_eq!(editor.display_text(cx), "\n");
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883 });
1884}
1885
1886#[gpui::test]
1887fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1888 init_test(cx, |_| {});
1889 let move_to_beg = MoveToBeginningOfLine {
1890 stop_at_soft_wraps: false,
1891 stop_at_indent: false,
1892 };
1893
1894 let move_to_end = MoveToEndOfLine {
1895 stop_at_soft_wraps: false,
1896 };
1897
1898 let editor = cx.add_window(|window, cx| {
1899 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1900 build_editor(buffer, window, cx)
1901 });
1902
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.set_wrap_width(Some(140.0.into()), cx);
1905
1906 // We expect the following lines after wrapping
1907 // ```
1908 // thequickbrownfox
1909 // jumpedoverthelazydo
1910 // gs
1911 // ```
1912 // The final `gs` was soft-wrapped onto a new line.
1913 assert_eq!(
1914 "thequickbrownfox\njumpedoverthelaz\nydogs",
1915 editor.display_text(cx),
1916 );
1917
1918 // First, let's assert behavior on the first line, that was not soft-wrapped.
1919 // Start the cursor at the `k` on the first line
1920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1921 s.select_display_ranges([
1922 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1923 ]);
1924 });
1925
1926 // Moving to the beginning of the line should put us at the beginning of the line.
1927 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1928 assert_eq!(
1929 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1930 editor.selections.display_ranges(cx)
1931 );
1932
1933 // Moving to the end of the line should put us at the end of the line.
1934 editor.move_to_end_of_line(&move_to_end, window, cx);
1935 assert_eq!(
1936 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1937 editor.selections.display_ranges(cx)
1938 );
1939
1940 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1941 // Start the cursor at the last line (`y` that was wrapped to a new line)
1942 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1943 s.select_display_ranges([
1944 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1945 ]);
1946 });
1947
1948 // Moving to the beginning of the line should put us at the start of the second line of
1949 // display text, i.e., the `j`.
1950 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1951 assert_eq!(
1952 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1953 editor.selections.display_ranges(cx)
1954 );
1955
1956 // Moving to the beginning of the line again should be a no-op.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1960 editor.selections.display_ranges(cx)
1961 );
1962
1963 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1964 // next display line.
1965 editor.move_to_end_of_line(&move_to_end, window, cx);
1966 assert_eq!(
1967 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1968 editor.selections.display_ranges(cx)
1969 );
1970
1971 // Moving to the end of the line again should be a no-op.
1972 editor.move_to_end_of_line(&move_to_end, window, cx);
1973 assert_eq!(
1974 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1975 editor.selections.display_ranges(cx)
1976 );
1977 });
1978}
1979
1980#[gpui::test]
1981fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1982 init_test(cx, |_| {});
1983
1984 let move_to_beg = MoveToBeginningOfLine {
1985 stop_at_soft_wraps: true,
1986 stop_at_indent: true,
1987 };
1988
1989 let select_to_beg = SelectToBeginningOfLine {
1990 stop_at_soft_wraps: true,
1991 stop_at_indent: true,
1992 };
1993
1994 let delete_to_beg = DeleteToBeginningOfLine {
1995 stop_at_indent: true,
1996 };
1997
1998 let move_to_end = MoveToEndOfLine {
1999 stop_at_soft_wraps: false,
2000 };
2001
2002 let editor = cx.add_window(|window, cx| {
2003 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2004 build_editor(buffer, window, cx)
2005 });
2006
2007 _ = editor.update(cx, |editor, window, cx| {
2008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2009 s.select_display_ranges([
2010 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2011 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2012 ]);
2013 });
2014
2015 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2016 // and the second cursor at the first non-whitespace character in the line.
2017 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2018 assert_eq!(
2019 editor.selections.display_ranges(cx),
2020 &[
2021 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2022 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2023 ]
2024 );
2025
2026 // Moving to the beginning of the line again should be a no-op for the first cursor,
2027 // and should move the second cursor to the beginning of the line.
2028 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2029 assert_eq!(
2030 editor.selections.display_ranges(cx),
2031 &[
2032 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2033 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2034 ]
2035 );
2036
2037 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2038 // and should move the second cursor back to the first non-whitespace character in the line.
2039 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2040 assert_eq!(
2041 editor.selections.display_ranges(cx),
2042 &[
2043 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2044 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2045 ]
2046 );
2047
2048 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2049 // and to the first non-whitespace character in the line for the second cursor.
2050 editor.move_to_end_of_line(&move_to_end, window, cx);
2051 editor.move_left(&MoveLeft, window, cx);
2052 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2053 assert_eq!(
2054 editor.selections.display_ranges(cx),
2055 &[
2056 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2057 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2058 ]
2059 );
2060
2061 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2062 // and should select to the beginning of the line for the second cursor.
2063 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2064 assert_eq!(
2065 editor.selections.display_ranges(cx),
2066 &[
2067 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2068 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2069 ]
2070 );
2071
2072 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2073 // and should delete to the first non-whitespace character in the line for the second cursor.
2074 editor.move_to_end_of_line(&move_to_end, window, cx);
2075 editor.move_left(&MoveLeft, window, cx);
2076 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2077 assert_eq!(editor.text(cx), "c\n f");
2078 });
2079}
2080
2081#[gpui::test]
2082fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2083 init_test(cx, |_| {});
2084
2085 let move_to_beg = MoveToBeginningOfLine {
2086 stop_at_soft_wraps: true,
2087 stop_at_indent: true,
2088 };
2089
2090 let editor = cx.add_window(|window, cx| {
2091 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2092 build_editor(buffer, window, cx)
2093 });
2094
2095 _ = editor.update(cx, |editor, window, cx| {
2096 // test cursor between line_start and indent_start
2097 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2098 s.select_display_ranges([
2099 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2100 ]);
2101 });
2102
2103 // cursor should move to line_start
2104 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2105 assert_eq!(
2106 editor.selections.display_ranges(cx),
2107 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2108 );
2109
2110 // cursor should move to indent_start
2111 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2112 assert_eq!(
2113 editor.selections.display_ranges(cx),
2114 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2115 );
2116
2117 // cursor should move to back to line_start
2118 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2119 assert_eq!(
2120 editor.selections.display_ranges(cx),
2121 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2122 );
2123 });
2124}
2125
2126#[gpui::test]
2127fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2128 init_test(cx, |_| {});
2129
2130 let editor = cx.add_window(|window, cx| {
2131 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2132 build_editor(buffer, window, cx)
2133 });
2134 _ = editor.update(cx, |editor, window, cx| {
2135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2136 s.select_display_ranges([
2137 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2138 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2139 ])
2140 });
2141 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2142 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2143
2144 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2145 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2146
2147 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2148 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2149
2150 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2151 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2152
2153 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2154 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2155
2156 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2157 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2158
2159 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2160 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2161
2162 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2163 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2164
2165 editor.move_right(&MoveRight, window, cx);
2166 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2167 assert_selection_ranges(
2168 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2169 editor,
2170 cx,
2171 );
2172
2173 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2174 assert_selection_ranges(
2175 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2176 editor,
2177 cx,
2178 );
2179
2180 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2181 assert_selection_ranges(
2182 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2183 editor,
2184 cx,
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192
2193 let editor = cx.add_window(|window, cx| {
2194 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2195 build_editor(buffer, window, cx)
2196 });
2197
2198 _ = editor.update(cx, |editor, window, cx| {
2199 editor.set_wrap_width(Some(140.0.into()), cx);
2200 assert_eq!(
2201 editor.display_text(cx),
2202 "use one::{\n two::three::\n four::five\n};"
2203 );
2204
2205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2206 s.select_display_ranges([
2207 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2208 ]);
2209 });
2210
2211 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2212 assert_eq!(
2213 editor.selections.display_ranges(cx),
2214 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2215 );
2216
2217 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2218 assert_eq!(
2219 editor.selections.display_ranges(cx),
2220 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2221 );
2222
2223 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2224 assert_eq!(
2225 editor.selections.display_ranges(cx),
2226 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2227 );
2228
2229 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2230 assert_eq!(
2231 editor.selections.display_ranges(cx),
2232 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2233 );
2234
2235 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2236 assert_eq!(
2237 editor.selections.display_ranges(cx),
2238 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2239 );
2240
2241 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2242 assert_eq!(
2243 editor.selections.display_ranges(cx),
2244 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2245 );
2246 });
2247}
2248
2249#[gpui::test]
2250async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2251 init_test(cx, |_| {});
2252 let mut cx = EditorTestContext::new(cx).await;
2253
2254 let line_height = cx.editor(|editor, window, _| {
2255 editor
2256 .style()
2257 .unwrap()
2258 .text
2259 .line_height_in_pixels(window.rem_size())
2260 });
2261 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2262
2263 cx.set_state(
2264 &r#"ˇone
2265 two
2266
2267 three
2268 fourˇ
2269 five
2270
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285 ˇ
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"one
2295 two
2296
2297 three
2298 four
2299 five
2300 ˇ
2301 sixˇ"#
2302 .unindent(),
2303 );
2304
2305 cx.update_editor(|editor, window, cx| {
2306 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2307 });
2308 cx.assert_editor_state(
2309 &r#"one
2310 two
2311
2312 three
2313 four
2314 five
2315
2316 sixˇ"#
2317 .unindent(),
2318 );
2319
2320 cx.update_editor(|editor, window, cx| {
2321 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2322 });
2323 cx.assert_editor_state(
2324 &r#"one
2325 two
2326
2327 three
2328 four
2329 five
2330 ˇ
2331 six"#
2332 .unindent(),
2333 );
2334
2335 cx.update_editor(|editor, window, cx| {
2336 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2337 });
2338 cx.assert_editor_state(
2339 &r#"one
2340 two
2341 ˇ
2342 three
2343 four
2344 five
2345
2346 six"#
2347 .unindent(),
2348 );
2349
2350 cx.update_editor(|editor, window, cx| {
2351 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2352 });
2353 cx.assert_editor_state(
2354 &r#"ˇone
2355 two
2356
2357 three
2358 four
2359 five
2360
2361 six"#
2362 .unindent(),
2363 );
2364}
2365
2366#[gpui::test]
2367async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2368 init_test(cx, |_| {});
2369 let mut cx = EditorTestContext::new(cx).await;
2370 let line_height = cx.editor(|editor, window, _| {
2371 editor
2372 .style()
2373 .unwrap()
2374 .text
2375 .line_height_in_pixels(window.rem_size())
2376 });
2377 let window = cx.window;
2378 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2379
2380 cx.set_state(
2381 r#"ˇone
2382 two
2383 three
2384 four
2385 five
2386 six
2387 seven
2388 eight
2389 nine
2390 ten
2391 "#,
2392 );
2393
2394 cx.update_editor(|editor, window, cx| {
2395 assert_eq!(
2396 editor.snapshot(window, cx).scroll_position(),
2397 gpui::Point::new(0., 0.)
2398 );
2399 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2400 assert_eq!(
2401 editor.snapshot(window, cx).scroll_position(),
2402 gpui::Point::new(0., 3.)
2403 );
2404 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2405 assert_eq!(
2406 editor.snapshot(window, cx).scroll_position(),
2407 gpui::Point::new(0., 6.)
2408 );
2409 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2410 assert_eq!(
2411 editor.snapshot(window, cx).scroll_position(),
2412 gpui::Point::new(0., 3.)
2413 );
2414
2415 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 1.)
2419 );
2420 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2421 assert_eq!(
2422 editor.snapshot(window, cx).scroll_position(),
2423 gpui::Point::new(0., 3.)
2424 );
2425 });
2426}
2427
2428#[gpui::test]
2429async fn test_autoscroll(cx: &mut TestAppContext) {
2430 init_test(cx, |_| {});
2431 let mut cx = EditorTestContext::new(cx).await;
2432
2433 let line_height = cx.update_editor(|editor, window, cx| {
2434 editor.set_vertical_scroll_margin(2, cx);
2435 editor
2436 .style()
2437 .unwrap()
2438 .text
2439 .line_height_in_pixels(window.rem_size())
2440 });
2441 let window = cx.window;
2442 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2443
2444 cx.set_state(
2445 r#"ˇone
2446 two
2447 three
2448 four
2449 five
2450 six
2451 seven
2452 eight
2453 nine
2454 ten
2455 "#,
2456 );
2457 cx.update_editor(|editor, window, cx| {
2458 assert_eq!(
2459 editor.snapshot(window, cx).scroll_position(),
2460 gpui::Point::new(0., 0.0)
2461 );
2462 });
2463
2464 // Add a cursor below the visible area. Since both cursors cannot fit
2465 // on screen, the editor autoscrolls to reveal the newest cursor, and
2466 // allows the vertical scroll margin below that cursor.
2467 cx.update_editor(|editor, window, cx| {
2468 editor.change_selections(Default::default(), window, cx, |selections| {
2469 selections.select_ranges([
2470 Point::new(0, 0)..Point::new(0, 0),
2471 Point::new(6, 0)..Point::new(6, 0),
2472 ]);
2473 })
2474 });
2475 cx.update_editor(|editor, window, cx| {
2476 assert_eq!(
2477 editor.snapshot(window, cx).scroll_position(),
2478 gpui::Point::new(0., 3.0)
2479 );
2480 });
2481
2482 // Move down. The editor cursor scrolls down to track the newest cursor.
2483 cx.update_editor(|editor, window, cx| {
2484 editor.move_down(&Default::default(), window, cx);
2485 });
2486 cx.update_editor(|editor, window, cx| {
2487 assert_eq!(
2488 editor.snapshot(window, cx).scroll_position(),
2489 gpui::Point::new(0., 4.0)
2490 );
2491 });
2492
2493 // Add a cursor above the visible area. Since both cursors fit on screen,
2494 // the editor scrolls to show both.
2495 cx.update_editor(|editor, window, cx| {
2496 editor.change_selections(Default::default(), window, cx, |selections| {
2497 selections.select_ranges([
2498 Point::new(1, 0)..Point::new(1, 0),
2499 Point::new(6, 0)..Point::new(6, 0),
2500 ]);
2501 })
2502 });
2503 cx.update_editor(|editor, window, cx| {
2504 assert_eq!(
2505 editor.snapshot(window, cx).scroll_position(),
2506 gpui::Point::new(0., 1.0)
2507 );
2508 });
2509}
2510
2511#[gpui::test]
2512async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2513 init_test(cx, |_| {});
2514 let mut cx = EditorTestContext::new(cx).await;
2515
2516 let line_height = cx.editor(|editor, window, _cx| {
2517 editor
2518 .style()
2519 .unwrap()
2520 .text
2521 .line_height_in_pixels(window.rem_size())
2522 });
2523 let window = cx.window;
2524 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2525 cx.set_state(
2526 &r#"
2527 ˇone
2528 two
2529 threeˇ
2530 four
2531 five
2532 six
2533 seven
2534 eight
2535 nine
2536 ten
2537 "#
2538 .unindent(),
2539 );
2540
2541 cx.update_editor(|editor, window, cx| {
2542 editor.move_page_down(&MovePageDown::default(), window, cx)
2543 });
2544 cx.assert_editor_state(
2545 &r#"
2546 one
2547 two
2548 three
2549 ˇfour
2550 five
2551 sixˇ
2552 seven
2553 eight
2554 nine
2555 ten
2556 "#
2557 .unindent(),
2558 );
2559
2560 cx.update_editor(|editor, window, cx| {
2561 editor.move_page_down(&MovePageDown::default(), window, cx)
2562 });
2563 cx.assert_editor_state(
2564 &r#"
2565 one
2566 two
2567 three
2568 four
2569 five
2570 six
2571 ˇseven
2572 eight
2573 nineˇ
2574 ten
2575 "#
2576 .unindent(),
2577 );
2578
2579 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2580 cx.assert_editor_state(
2581 &r#"
2582 one
2583 two
2584 three
2585 ˇfour
2586 five
2587 sixˇ
2588 seven
2589 eight
2590 nine
2591 ten
2592 "#
2593 .unindent(),
2594 );
2595
2596 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2597 cx.assert_editor_state(
2598 &r#"
2599 ˇone
2600 two
2601 threeˇ
2602 four
2603 five
2604 six
2605 seven
2606 eight
2607 nine
2608 ten
2609 "#
2610 .unindent(),
2611 );
2612
2613 // Test select collapsing
2614 cx.update_editor(|editor, window, cx| {
2615 editor.move_page_down(&MovePageDown::default(), window, cx);
2616 editor.move_page_down(&MovePageDown::default(), window, cx);
2617 editor.move_page_down(&MovePageDown::default(), window, cx);
2618 });
2619 cx.assert_editor_state(
2620 &r#"
2621 one
2622 two
2623 three
2624 four
2625 five
2626 six
2627 seven
2628 eight
2629 nine
2630 ˇten
2631 ˇ"#
2632 .unindent(),
2633 );
2634}
2635
2636#[gpui::test]
2637async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2638 init_test(cx, |_| {});
2639 let mut cx = EditorTestContext::new(cx).await;
2640 cx.set_state("one «two threeˇ» four");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_beginning_of_line(
2643 &DeleteToBeginningOfLine {
2644 stop_at_indent: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 assert_eq!(editor.text(cx), " four");
2650 });
2651}
2652
2653#[gpui::test]
2654async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 // For an empty selection, the preceding word fragment is deleted.
2660 // For non-empty selections, only selected characters are deleted.
2661 cx.set_state("onˇe two t«hreˇ»e four");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_previous_word_start(
2664 &DeleteToPreviousWordStart {
2665 ignore_newlines: false,
2666 ignore_brackets: false,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 cx.assert_editor_state("ˇe two tˇe four");
2673
2674 cx.set_state("e tˇwo te «fˇ»our");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: false,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("e tˇ te ˇour");
2686}
2687
2688#[gpui::test]
2689async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2690 init_test(cx, |_| {});
2691
2692 let mut cx = EditorTestContext::new(cx).await;
2693
2694 cx.set_state("here is some text ˇwith a space");
2695 cx.update_editor(|editor, window, cx| {
2696 editor.delete_to_previous_word_start(
2697 &DeleteToPreviousWordStart {
2698 ignore_newlines: false,
2699 ignore_brackets: true,
2700 },
2701 window,
2702 cx,
2703 );
2704 });
2705 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2706 cx.assert_editor_state("here is some textˇwith a space");
2707
2708 cx.set_state("here is some text ˇwith a space");
2709 cx.update_editor(|editor, window, cx| {
2710 editor.delete_to_previous_word_start(
2711 &DeleteToPreviousWordStart {
2712 ignore_newlines: false,
2713 ignore_brackets: false,
2714 },
2715 window,
2716 cx,
2717 );
2718 });
2719 cx.assert_editor_state("here is some textˇwith a space");
2720
2721 cx.set_state("here is some textˇ with a space");
2722 cx.update_editor(|editor, window, cx| {
2723 editor.delete_to_next_word_end(
2724 &DeleteToNextWordEnd {
2725 ignore_newlines: false,
2726 ignore_brackets: true,
2727 },
2728 window,
2729 cx,
2730 );
2731 });
2732 // Same happens in the other direction.
2733 cx.assert_editor_state("here is some textˇwith a space");
2734
2735 cx.set_state("here is some textˇ with a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_next_word_end(
2738 &DeleteToNextWordEnd {
2739 ignore_newlines: false,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 cx.assert_editor_state("here is some textˇwith a space");
2747
2748 cx.set_state("here is some textˇ with a space");
2749 cx.update_editor(|editor, window, cx| {
2750 editor.delete_to_next_word_end(
2751 &DeleteToNextWordEnd {
2752 ignore_newlines: true,
2753 ignore_brackets: false,
2754 },
2755 window,
2756 cx,
2757 );
2758 });
2759 cx.assert_editor_state("here is some textˇwith a space");
2760 cx.update_editor(|editor, window, cx| {
2761 editor.delete_to_previous_word_start(
2762 &DeleteToPreviousWordStart {
2763 ignore_newlines: true,
2764 ignore_brackets: false,
2765 },
2766 window,
2767 cx,
2768 );
2769 });
2770 cx.assert_editor_state("here is some ˇwith a space");
2771 cx.update_editor(|editor, window, cx| {
2772 editor.delete_to_previous_word_start(
2773 &DeleteToPreviousWordStart {
2774 ignore_newlines: true,
2775 ignore_brackets: false,
2776 },
2777 window,
2778 cx,
2779 );
2780 });
2781 // Single whitespaces are removed with the word behind them.
2782 cx.assert_editor_state("here is ˇwith a space");
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_previous_word_start(
2785 &DeleteToPreviousWordStart {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 cx.assert_editor_state("here ˇwith a space");
2794 cx.update_editor(|editor, window, cx| {
2795 editor.delete_to_previous_word_start(
2796 &DeleteToPreviousWordStart {
2797 ignore_newlines: true,
2798 ignore_brackets: false,
2799 },
2800 window,
2801 cx,
2802 );
2803 });
2804 cx.assert_editor_state("ˇwith a space");
2805 cx.update_editor(|editor, window, cx| {
2806 editor.delete_to_previous_word_start(
2807 &DeleteToPreviousWordStart {
2808 ignore_newlines: true,
2809 ignore_brackets: false,
2810 },
2811 window,
2812 cx,
2813 );
2814 });
2815 cx.assert_editor_state("ˇwith a space");
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 // Same happens in the other direction.
2827 cx.assert_editor_state("ˇ a space");
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state("ˇ space");
2839 cx.update_editor(|editor, window, cx| {
2840 editor.delete_to_next_word_end(
2841 &DeleteToNextWordEnd {
2842 ignore_newlines: true,
2843 ignore_brackets: false,
2844 },
2845 window,
2846 cx,
2847 );
2848 });
2849 cx.assert_editor_state("ˇ");
2850 cx.update_editor(|editor, window, cx| {
2851 editor.delete_to_next_word_end(
2852 &DeleteToNextWordEnd {
2853 ignore_newlines: true,
2854 ignore_brackets: false,
2855 },
2856 window,
2857 cx,
2858 );
2859 });
2860 cx.assert_editor_state("ˇ");
2861 cx.update_editor(|editor, window, cx| {
2862 editor.delete_to_previous_word_start(
2863 &DeleteToPreviousWordStart {
2864 ignore_newlines: true,
2865 ignore_brackets: false,
2866 },
2867 window,
2868 cx,
2869 );
2870 });
2871 cx.assert_editor_state("ˇ");
2872}
2873
2874#[gpui::test]
2875async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2876 init_test(cx, |_| {});
2877
2878 let language = Arc::new(
2879 Language::new(
2880 LanguageConfig {
2881 brackets: BracketPairConfig {
2882 pairs: vec![
2883 BracketPair {
2884 start: "\"".to_string(),
2885 end: "\"".to_string(),
2886 close: true,
2887 surround: true,
2888 newline: false,
2889 },
2890 BracketPair {
2891 start: "(".to_string(),
2892 end: ")".to_string(),
2893 close: true,
2894 surround: true,
2895 newline: true,
2896 },
2897 ],
2898 ..BracketPairConfig::default()
2899 },
2900 ..LanguageConfig::default()
2901 },
2902 Some(tree_sitter_rust::LANGUAGE.into()),
2903 )
2904 .with_brackets_query(
2905 r#"
2906 ("(" @open ")" @close)
2907 ("\"" @open "\"" @close)
2908 "#,
2909 )
2910 .unwrap(),
2911 );
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2915
2916 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_previous_word_start(
2919 &DeleteToPreviousWordStart {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 // Deletion stops before brackets if asked to not ignore them.
2928 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2929 cx.update_editor(|editor, window, cx| {
2930 editor.delete_to_previous_word_start(
2931 &DeleteToPreviousWordStart {
2932 ignore_newlines: true,
2933 ignore_brackets: false,
2934 },
2935 window,
2936 cx,
2937 );
2938 });
2939 // Deletion has to remove a single bracket and then stop again.
2940 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2941
2942 cx.update_editor(|editor, window, cx| {
2943 editor.delete_to_previous_word_start(
2944 &DeleteToPreviousWordStart {
2945 ignore_newlines: true,
2946 ignore_brackets: false,
2947 },
2948 window,
2949 cx,
2950 );
2951 });
2952 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2953
2954 cx.update_editor(|editor, window, cx| {
2955 editor.delete_to_previous_word_start(
2956 &DeleteToPreviousWordStart {
2957 ignore_newlines: true,
2958 ignore_brackets: false,
2959 },
2960 window,
2961 cx,
2962 );
2963 });
2964 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2965
2966 cx.update_editor(|editor, window, cx| {
2967 editor.delete_to_previous_word_start(
2968 &DeleteToPreviousWordStart {
2969 ignore_newlines: true,
2970 ignore_brackets: false,
2971 },
2972 window,
2973 cx,
2974 );
2975 });
2976 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2977
2978 cx.update_editor(|editor, window, cx| {
2979 editor.delete_to_next_word_end(
2980 &DeleteToNextWordEnd {
2981 ignore_newlines: true,
2982 ignore_brackets: false,
2983 },
2984 window,
2985 cx,
2986 );
2987 });
2988 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2989 cx.assert_editor_state(r#"ˇ");"#);
2990
2991 cx.update_editor(|editor, window, cx| {
2992 editor.delete_to_next_word_end(
2993 &DeleteToNextWordEnd {
2994 ignore_newlines: true,
2995 ignore_brackets: false,
2996 },
2997 window,
2998 cx,
2999 );
3000 });
3001 cx.assert_editor_state(r#"ˇ"#);
3002
3003 cx.update_editor(|editor, window, cx| {
3004 editor.delete_to_next_word_end(
3005 &DeleteToNextWordEnd {
3006 ignore_newlines: true,
3007 ignore_brackets: false,
3008 },
3009 window,
3010 cx,
3011 );
3012 });
3013 cx.assert_editor_state(r#"ˇ"#);
3014
3015 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
3016 cx.update_editor(|editor, window, cx| {
3017 editor.delete_to_previous_word_start(
3018 &DeleteToPreviousWordStart {
3019 ignore_newlines: true,
3020 ignore_brackets: true,
3021 },
3022 window,
3023 cx,
3024 );
3025 });
3026 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3027}
3028
3029#[gpui::test]
3030fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3031 init_test(cx, |_| {});
3032
3033 let editor = cx.add_window(|window, cx| {
3034 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3035 build_editor(buffer, window, cx)
3036 });
3037 let del_to_prev_word_start = DeleteToPreviousWordStart {
3038 ignore_newlines: false,
3039 ignore_brackets: false,
3040 };
3041 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3042 ignore_newlines: true,
3043 ignore_brackets: false,
3044 };
3045
3046 _ = editor.update(cx, |editor, window, cx| {
3047 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3048 s.select_display_ranges([
3049 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3050 ])
3051 });
3052 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3053 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3054 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3055 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3056 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3057 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3058 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3059 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3060 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3061 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3062 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3063 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3064 });
3065}
3066
3067#[gpui::test]
3068fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3069 init_test(cx, |_| {});
3070
3071 let editor = cx.add_window(|window, cx| {
3072 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3073 build_editor(buffer, window, cx)
3074 });
3075 let del_to_next_word_end = DeleteToNextWordEnd {
3076 ignore_newlines: false,
3077 ignore_brackets: false,
3078 };
3079 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3080 ignore_newlines: true,
3081 ignore_brackets: false,
3082 };
3083
3084 _ = editor.update(cx, |editor, window, cx| {
3085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3086 s.select_display_ranges([
3087 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3088 ])
3089 });
3090 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3091 assert_eq!(
3092 editor.buffer.read(cx).read(cx).text(),
3093 "one\n two\nthree\n four"
3094 );
3095 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3096 assert_eq!(
3097 editor.buffer.read(cx).read(cx).text(),
3098 "\n two\nthree\n four"
3099 );
3100 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3101 assert_eq!(
3102 editor.buffer.read(cx).read(cx).text(),
3103 "two\nthree\n four"
3104 );
3105 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3106 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3107 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3108 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3109 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3110 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3111 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3112 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3113 });
3114}
3115
3116#[gpui::test]
3117fn test_newline(cx: &mut TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let editor = cx.add_window(|window, cx| {
3121 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3122 build_editor(buffer, window, cx)
3123 });
3124
3125 _ = editor.update(cx, |editor, window, cx| {
3126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3127 s.select_display_ranges([
3128 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3129 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3130 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3131 ])
3132 });
3133
3134 editor.newline(&Newline, window, cx);
3135 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3136 });
3137}
3138
3139#[gpui::test]
3140fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3141 init_test(cx, |_| {});
3142
3143 let editor = cx.add_window(|window, cx| {
3144 let buffer = MultiBuffer::build_simple(
3145 "
3146 a
3147 b(
3148 X
3149 )
3150 c(
3151 X
3152 )
3153 "
3154 .unindent()
3155 .as_str(),
3156 cx,
3157 );
3158 let mut editor = build_editor(buffer, window, cx);
3159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3160 s.select_ranges([
3161 Point::new(2, 4)..Point::new(2, 5),
3162 Point::new(5, 4)..Point::new(5, 5),
3163 ])
3164 });
3165 editor
3166 });
3167
3168 _ = editor.update(cx, |editor, window, cx| {
3169 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3170 editor.buffer.update(cx, |buffer, cx| {
3171 buffer.edit(
3172 [
3173 (Point::new(1, 2)..Point::new(3, 0), ""),
3174 (Point::new(4, 2)..Point::new(6, 0), ""),
3175 ],
3176 None,
3177 cx,
3178 );
3179 assert_eq!(
3180 buffer.read(cx).text(),
3181 "
3182 a
3183 b()
3184 c()
3185 "
3186 .unindent()
3187 );
3188 });
3189 assert_eq!(
3190 editor.selections.ranges(&editor.display_snapshot(cx)),
3191 &[
3192 Point::new(1, 2)..Point::new(1, 2),
3193 Point::new(2, 2)..Point::new(2, 2),
3194 ],
3195 );
3196
3197 editor.newline(&Newline, window, cx);
3198 assert_eq!(
3199 editor.text(cx),
3200 "
3201 a
3202 b(
3203 )
3204 c(
3205 )
3206 "
3207 .unindent()
3208 );
3209
3210 // The selections are moved after the inserted newlines
3211 assert_eq!(
3212 editor.selections.ranges(&editor.display_snapshot(cx)),
3213 &[
3214 Point::new(2, 0)..Point::new(2, 0),
3215 Point::new(4, 0)..Point::new(4, 0),
3216 ],
3217 );
3218 });
3219}
3220
3221#[gpui::test]
3222async fn test_newline_above(cx: &mut TestAppContext) {
3223 init_test(cx, |settings| {
3224 settings.defaults.tab_size = NonZeroU32::new(4)
3225 });
3226
3227 let language = Arc::new(
3228 Language::new(
3229 LanguageConfig::default(),
3230 Some(tree_sitter_rust::LANGUAGE.into()),
3231 )
3232 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3233 .unwrap(),
3234 );
3235
3236 let mut cx = EditorTestContext::new(cx).await;
3237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3238 cx.set_state(indoc! {"
3239 const a: ˇA = (
3240 (ˇ
3241 «const_functionˇ»(ˇ),
3242 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3243 )ˇ
3244 ˇ);ˇ
3245 "});
3246
3247 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3248 cx.assert_editor_state(indoc! {"
3249 ˇ
3250 const a: A = (
3251 ˇ
3252 (
3253 ˇ
3254 ˇ
3255 const_function(),
3256 ˇ
3257 ˇ
3258 ˇ
3259 ˇ
3260 something_else,
3261 ˇ
3262 )
3263 ˇ
3264 ˇ
3265 );
3266 "});
3267}
3268
3269#[gpui::test]
3270async fn test_newline_below(cx: &mut TestAppContext) {
3271 init_test(cx, |settings| {
3272 settings.defaults.tab_size = NonZeroU32::new(4)
3273 });
3274
3275 let language = Arc::new(
3276 Language::new(
3277 LanguageConfig::default(),
3278 Some(tree_sitter_rust::LANGUAGE.into()),
3279 )
3280 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3281 .unwrap(),
3282 );
3283
3284 let mut cx = EditorTestContext::new(cx).await;
3285 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3286 cx.set_state(indoc! {"
3287 const a: ˇA = (
3288 (ˇ
3289 «const_functionˇ»(ˇ),
3290 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3291 )ˇ
3292 ˇ);ˇ
3293 "});
3294
3295 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: A = (
3298 ˇ
3299 (
3300 ˇ
3301 const_function(),
3302 ˇ
3303 ˇ
3304 something_else,
3305 ˇ
3306 ˇ
3307 ˇ
3308 ˇ
3309 )
3310 ˇ
3311 );
3312 ˇ
3313 ˇ
3314 "});
3315}
3316
3317#[gpui::test]
3318async fn test_newline_comments(cx: &mut TestAppContext) {
3319 init_test(cx, |settings| {
3320 settings.defaults.tab_size = NonZeroU32::new(4)
3321 });
3322
3323 let language = Arc::new(Language::new(
3324 LanguageConfig {
3325 line_comments: vec!["// ".into()],
3326 ..LanguageConfig::default()
3327 },
3328 None,
3329 ));
3330 {
3331 let mut cx = EditorTestContext::new(cx).await;
3332 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3333 cx.set_state(indoc! {"
3334 // Fooˇ
3335 "});
3336
3337 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3338 cx.assert_editor_state(indoc! {"
3339 // Foo
3340 // ˇ
3341 "});
3342 // Ensure that we add comment prefix when existing line contains space
3343 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3344 cx.assert_editor_state(
3345 indoc! {"
3346 // Foo
3347 //s
3348 // ˇ
3349 "}
3350 .replace("s", " ") // s is used as space placeholder to prevent format on save
3351 .as_str(),
3352 );
3353 // Ensure that we add comment prefix when existing line does not contain space
3354 cx.set_state(indoc! {"
3355 // Foo
3356 //ˇ
3357 "});
3358 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3359 cx.assert_editor_state(indoc! {"
3360 // Foo
3361 //
3362 // ˇ
3363 "});
3364 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3365 cx.set_state(indoc! {"
3366 ˇ// Foo
3367 "});
3368 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3369 cx.assert_editor_state(indoc! {"
3370
3371 ˇ// Foo
3372 "});
3373 }
3374 // Ensure that comment continuations can be disabled.
3375 update_test_language_settings(cx, |settings| {
3376 settings.defaults.extend_comment_on_newline = Some(false);
3377 });
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.set_state(indoc! {"
3380 // Fooˇ
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 // Foo
3385 ˇ
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4)
3393 });
3394
3395 let language = Arc::new(Language::new(
3396 LanguageConfig {
3397 line_comments: vec!["// ".into(), "/// ".into()],
3398 ..LanguageConfig::default()
3399 },
3400 None,
3401 ));
3402 {
3403 let mut cx = EditorTestContext::new(cx).await;
3404 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3405 cx.set_state(indoc! {"
3406 //ˇ
3407 "});
3408 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 //
3411 // ˇ
3412 "});
3413
3414 cx.set_state(indoc! {"
3415 ///ˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 ///
3420 /// ˇ
3421 "});
3422 }
3423}
3424
3425#[gpui::test]
3426async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3427 init_test(cx, |settings| {
3428 settings.defaults.tab_size = NonZeroU32::new(4)
3429 });
3430
3431 let language = Arc::new(
3432 Language::new(
3433 LanguageConfig {
3434 documentation_comment: Some(language::BlockCommentConfig {
3435 start: "/**".into(),
3436 end: "*/".into(),
3437 prefix: "* ".into(),
3438 tab_size: 1,
3439 }),
3440
3441 ..LanguageConfig::default()
3442 },
3443 Some(tree_sitter_rust::LANGUAGE.into()),
3444 )
3445 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3446 .unwrap(),
3447 );
3448
3449 {
3450 let mut cx = EditorTestContext::new(cx).await;
3451 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3452 cx.set_state(indoc! {"
3453 /**ˇ
3454 "});
3455
3456 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3457 cx.assert_editor_state(indoc! {"
3458 /**
3459 * ˇ
3460 "});
3461 // Ensure that if cursor is before the comment start,
3462 // we do not actually insert a comment prefix.
3463 cx.set_state(indoc! {"
3464 ˇ/**
3465 "});
3466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3467 cx.assert_editor_state(indoc! {"
3468
3469 ˇ/**
3470 "});
3471 // Ensure that if cursor is between it doesn't add comment prefix.
3472 cx.set_state(indoc! {"
3473 /*ˇ*
3474 "});
3475 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 /*
3478 ˇ*
3479 "});
3480 // Ensure that if suffix exists on same line after cursor it adds new line.
3481 cx.set_state(indoc! {"
3482 /**ˇ*/
3483 "});
3484 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3485 cx.assert_editor_state(indoc! {"
3486 /**
3487 * ˇ
3488 */
3489 "});
3490 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3491 cx.set_state(indoc! {"
3492 /**ˇ */
3493 "});
3494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 /**
3497 * ˇ
3498 */
3499 "});
3500 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3501 cx.set_state(indoc! {"
3502 /** ˇ*/
3503 "});
3504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3505 cx.assert_editor_state(
3506 indoc! {"
3507 /**s
3508 * ˇ
3509 */
3510 "}
3511 .replace("s", " ") // s is used as space placeholder to prevent format on save
3512 .as_str(),
3513 );
3514 // Ensure that delimiter space is preserved when newline on already
3515 // spaced delimiter.
3516 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3517 cx.assert_editor_state(
3518 indoc! {"
3519 /**s
3520 *s
3521 * ˇ
3522 */
3523 "}
3524 .replace("s", " ") // s is used as space placeholder to prevent format on save
3525 .as_str(),
3526 );
3527 // Ensure that delimiter space is preserved when space is not
3528 // on existing delimiter.
3529 cx.set_state(indoc! {"
3530 /**
3531 *ˇ
3532 */
3533 "});
3534 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3535 cx.assert_editor_state(indoc! {"
3536 /**
3537 *
3538 * ˇ
3539 */
3540 "});
3541 // Ensure that if suffix exists on same line after cursor it
3542 // doesn't add extra new line if prefix is not on same line.
3543 cx.set_state(indoc! {"
3544 /**
3545 ˇ*/
3546 "});
3547 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3548 cx.assert_editor_state(indoc! {"
3549 /**
3550
3551 ˇ*/
3552 "});
3553 // Ensure that it detects suffix after existing prefix.
3554 cx.set_state(indoc! {"
3555 /**ˇ/
3556 "});
3557 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3558 cx.assert_editor_state(indoc! {"
3559 /**
3560 ˇ/
3561 "});
3562 // Ensure that if suffix exists on same line before
3563 // cursor it does not add comment prefix.
3564 cx.set_state(indoc! {"
3565 /** */ˇ
3566 "});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 /** */
3570 ˇ
3571 "});
3572 // Ensure that if suffix exists on same line before
3573 // cursor it does not add comment prefix.
3574 cx.set_state(indoc! {"
3575 /**
3576 *
3577 */ˇ
3578 "});
3579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 /**
3582 *
3583 */
3584 ˇ
3585 "});
3586
3587 // Ensure that inline comment followed by code
3588 // doesn't add comment prefix on newline
3589 cx.set_state(indoc! {"
3590 /** */ textˇ
3591 "});
3592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 /** */ text
3595 ˇ
3596 "});
3597
3598 // Ensure that text after comment end tag
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /**
3602 *
3603 */ˇtext
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /**
3608 *
3609 */
3610 ˇtext
3611 "});
3612
3613 // Ensure if not comment block it doesn't
3614 // add comment prefix on newline
3615 cx.set_state(indoc! {"
3616 * textˇ
3617 "});
3618 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3619 cx.assert_editor_state(indoc! {"
3620 * text
3621 ˇ
3622 "});
3623 }
3624 // Ensure that comment continuations can be disabled.
3625 update_test_language_settings(cx, |settings| {
3626 settings.defaults.extend_comment_on_newline = Some(false);
3627 });
3628 let mut cx = EditorTestContext::new(cx).await;
3629 cx.set_state(indoc! {"
3630 /**ˇ
3631 "});
3632 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 /**
3635 ˇ
3636 "});
3637}
3638
3639#[gpui::test]
3640async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3641 init_test(cx, |settings| {
3642 settings.defaults.tab_size = NonZeroU32::new(4)
3643 });
3644
3645 let lua_language = Arc::new(Language::new(
3646 LanguageConfig {
3647 line_comments: vec!["--".into()],
3648 block_comment: Some(language::BlockCommentConfig {
3649 start: "--[[".into(),
3650 prefix: "".into(),
3651 end: "]]".into(),
3652 tab_size: 0,
3653 }),
3654 ..LanguageConfig::default()
3655 },
3656 None,
3657 ));
3658
3659 let mut cx = EditorTestContext::new(cx).await;
3660 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3661
3662 // Line with line comment should extend
3663 cx.set_state(indoc! {"
3664 --ˇ
3665 "});
3666 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3667 cx.assert_editor_state(indoc! {"
3668 --
3669 --ˇ
3670 "});
3671
3672 // Line with block comment that matches line comment should not extend
3673 cx.set_state(indoc! {"
3674 --[[ˇ
3675 "});
3676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 --[[
3679 ˇ
3680 "});
3681}
3682
3683#[gpui::test]
3684fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3685 init_test(cx, |_| {});
3686
3687 let editor = cx.add_window(|window, cx| {
3688 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3689 let mut editor = build_editor(buffer, window, cx);
3690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3691 s.select_ranges([3..4, 11..12, 19..20])
3692 });
3693 editor
3694 });
3695
3696 _ = editor.update(cx, |editor, window, cx| {
3697 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3698 editor.buffer.update(cx, |buffer, cx| {
3699 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3700 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3701 });
3702 assert_eq!(
3703 editor.selections.ranges(&editor.display_snapshot(cx)),
3704 &[2..2, 7..7, 12..12],
3705 );
3706
3707 editor.insert("Z", window, cx);
3708 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3709
3710 // The selections are moved after the inserted characters
3711 assert_eq!(
3712 editor.selections.ranges(&editor.display_snapshot(cx)),
3713 &[3..3, 9..9, 15..15],
3714 );
3715 });
3716}
3717
3718#[gpui::test]
3719async fn test_tab(cx: &mut TestAppContext) {
3720 init_test(cx, |settings| {
3721 settings.defaults.tab_size = NonZeroU32::new(3)
3722 });
3723
3724 let mut cx = EditorTestContext::new(cx).await;
3725 cx.set_state(indoc! {"
3726 ˇabˇc
3727 ˇ🏀ˇ🏀ˇefg
3728 dˇ
3729 "});
3730 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3731 cx.assert_editor_state(indoc! {"
3732 ˇab ˇc
3733 ˇ🏀 ˇ🏀 ˇefg
3734 d ˇ
3735 "});
3736
3737 cx.set_state(indoc! {"
3738 a
3739 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 a
3744 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3745 "});
3746}
3747
3748#[gpui::test]
3749async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3750 init_test(cx, |_| {});
3751
3752 let mut cx = EditorTestContext::new(cx).await;
3753 let language = Arc::new(
3754 Language::new(
3755 LanguageConfig::default(),
3756 Some(tree_sitter_rust::LANGUAGE.into()),
3757 )
3758 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3759 .unwrap(),
3760 );
3761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3762
3763 // test when all cursors are not at suggested indent
3764 // then simply move to their suggested indent location
3765 cx.set_state(indoc! {"
3766 const a: B = (
3767 c(
3768 ˇ
3769 ˇ )
3770 );
3771 "});
3772 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3773 cx.assert_editor_state(indoc! {"
3774 const a: B = (
3775 c(
3776 ˇ
3777 ˇ)
3778 );
3779 "});
3780
3781 // test cursor already at suggested indent not moving when
3782 // other cursors are yet to reach their suggested indents
3783 cx.set_state(indoc! {"
3784 ˇ
3785 const a: B = (
3786 c(
3787 d(
3788 ˇ
3789 )
3790 ˇ
3791 ˇ )
3792 );
3793 "});
3794 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3795 cx.assert_editor_state(indoc! {"
3796 ˇ
3797 const a: B = (
3798 c(
3799 d(
3800 ˇ
3801 )
3802 ˇ
3803 ˇ)
3804 );
3805 "});
3806 // test when all cursors are at suggested indent then tab is inserted
3807 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 ˇ
3810 const a: B = (
3811 c(
3812 d(
3813 ˇ
3814 )
3815 ˇ
3816 ˇ)
3817 );
3818 "});
3819
3820 // test when current indent is less than suggested indent,
3821 // we adjust line to match suggested indent and move cursor to it
3822 //
3823 // when no other cursor is at word boundary, all of them should move
3824 cx.set_state(indoc! {"
3825 const a: B = (
3826 c(
3827 d(
3828 ˇ
3829 ˇ )
3830 ˇ )
3831 );
3832 "});
3833 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3834 cx.assert_editor_state(indoc! {"
3835 const a: B = (
3836 c(
3837 d(
3838 ˇ
3839 ˇ)
3840 ˇ)
3841 );
3842 "});
3843
3844 // test when current indent is less than suggested indent,
3845 // we adjust line to match suggested indent and move cursor to it
3846 //
3847 // when some other cursor is at word boundary, it should not move
3848 cx.set_state(indoc! {"
3849 const a: B = (
3850 c(
3851 d(
3852 ˇ
3853 ˇ )
3854 ˇ)
3855 );
3856 "});
3857 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3858 cx.assert_editor_state(indoc! {"
3859 const a: B = (
3860 c(
3861 d(
3862 ˇ
3863 ˇ)
3864 ˇ)
3865 );
3866 "});
3867
3868 // test when current indent is more than suggested indent,
3869 // we just move cursor to current indent instead of suggested indent
3870 //
3871 // when no other cursor is at word boundary, all of them should move
3872 cx.set_state(indoc! {"
3873 const a: B = (
3874 c(
3875 d(
3876 ˇ
3877 ˇ )
3878 ˇ )
3879 );
3880 "});
3881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3882 cx.assert_editor_state(indoc! {"
3883 const a: B = (
3884 c(
3885 d(
3886 ˇ
3887 ˇ)
3888 ˇ)
3889 );
3890 "});
3891 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3892 cx.assert_editor_state(indoc! {"
3893 const a: B = (
3894 c(
3895 d(
3896 ˇ
3897 ˇ)
3898 ˇ)
3899 );
3900 "});
3901
3902 // test when current indent is more than suggested indent,
3903 // we just move cursor to current indent instead of suggested indent
3904 //
3905 // when some other cursor is at word boundary, it doesn't move
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 d(
3910 ˇ
3911 ˇ )
3912 ˇ)
3913 );
3914 "});
3915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3916 cx.assert_editor_state(indoc! {"
3917 const a: B = (
3918 c(
3919 d(
3920 ˇ
3921 ˇ)
3922 ˇ)
3923 );
3924 "});
3925
3926 // handle auto-indent when there are multiple cursors on the same line
3927 cx.set_state(indoc! {"
3928 const a: B = (
3929 c(
3930 ˇ ˇ
3931 ˇ )
3932 );
3933 "});
3934 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3935 cx.assert_editor_state(indoc! {"
3936 const a: B = (
3937 c(
3938 ˇ
3939 ˇ)
3940 );
3941 "});
3942}
3943
3944#[gpui::test]
3945async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3946 init_test(cx, |settings| {
3947 settings.defaults.tab_size = NonZeroU32::new(3)
3948 });
3949
3950 let mut cx = EditorTestContext::new(cx).await;
3951 cx.set_state(indoc! {"
3952 ˇ
3953 \t ˇ
3954 \t ˇ
3955 \t ˇ
3956 \t \t\t \t \t\t \t\t \t \t ˇ
3957 "});
3958
3959 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 ˇ
3962 \t ˇ
3963 \t ˇ
3964 \t ˇ
3965 \t \t\t \t \t\t \t\t \t \t ˇ
3966 "});
3967}
3968
3969#[gpui::test]
3970async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3971 init_test(cx, |settings| {
3972 settings.defaults.tab_size = NonZeroU32::new(4)
3973 });
3974
3975 let language = Arc::new(
3976 Language::new(
3977 LanguageConfig::default(),
3978 Some(tree_sitter_rust::LANGUAGE.into()),
3979 )
3980 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3981 .unwrap(),
3982 );
3983
3984 let mut cx = EditorTestContext::new(cx).await;
3985 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3986 cx.set_state(indoc! {"
3987 fn a() {
3988 if b {
3989 \t ˇc
3990 }
3991 }
3992 "});
3993
3994 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3995 cx.assert_editor_state(indoc! {"
3996 fn a() {
3997 if b {
3998 ˇc
3999 }
4000 }
4001 "});
4002}
4003
4004#[gpui::test]
4005async fn test_indent_outdent(cx: &mut TestAppContext) {
4006 init_test(cx, |settings| {
4007 settings.defaults.tab_size = NonZeroU32::new(4);
4008 });
4009
4010 let mut cx = EditorTestContext::new(cx).await;
4011
4012 cx.set_state(indoc! {"
4013 «oneˇ» «twoˇ»
4014 three
4015 four
4016 "});
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 «oneˇ» «twoˇ»
4020 three
4021 four
4022 "});
4023
4024 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4025 cx.assert_editor_state(indoc! {"
4026 «oneˇ» «twoˇ»
4027 three
4028 four
4029 "});
4030
4031 // select across line ending
4032 cx.set_state(indoc! {"
4033 one two
4034 t«hree
4035 ˇ» four
4036 "});
4037 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 t«hree
4041 ˇ» four
4042 "});
4043
4044 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4045 cx.assert_editor_state(indoc! {"
4046 one two
4047 t«hree
4048 ˇ» four
4049 "});
4050
4051 // Ensure that indenting/outdenting works when the cursor is at column 0.
4052 cx.set_state(indoc! {"
4053 one two
4054 ˇthree
4055 four
4056 "});
4057 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4058 cx.assert_editor_state(indoc! {"
4059 one two
4060 ˇthree
4061 four
4062 "});
4063
4064 cx.set_state(indoc! {"
4065 one two
4066 ˇ three
4067 four
4068 "});
4069 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4070 cx.assert_editor_state(indoc! {"
4071 one two
4072 ˇthree
4073 four
4074 "});
4075}
4076
4077#[gpui::test]
4078async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4079 // This is a regression test for issue #33761
4080 init_test(cx, |_| {});
4081
4082 let mut cx = EditorTestContext::new(cx).await;
4083 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4085
4086 cx.set_state(
4087 r#"ˇ# ingress:
4088ˇ# api:
4089ˇ# enabled: false
4090ˇ# pathType: Prefix
4091ˇ# console:
4092ˇ# enabled: false
4093ˇ# pathType: Prefix
4094"#,
4095 );
4096
4097 // Press tab to indent all lines
4098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4099
4100 cx.assert_editor_state(
4101 r#" ˇ# ingress:
4102 ˇ# api:
4103 ˇ# enabled: false
4104 ˇ# pathType: Prefix
4105 ˇ# console:
4106 ˇ# enabled: false
4107 ˇ# pathType: Prefix
4108"#,
4109 );
4110}
4111
4112#[gpui::test]
4113async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4114 // This is a test to make sure our fix for issue #33761 didn't break anything
4115 init_test(cx, |_| {});
4116
4117 let mut cx = EditorTestContext::new(cx).await;
4118 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4119 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4120
4121 cx.set_state(
4122 r#"ˇingress:
4123ˇ api:
4124ˇ enabled: false
4125ˇ pathType: Prefix
4126"#,
4127 );
4128
4129 // Press tab to indent all lines
4130 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4131
4132 cx.assert_editor_state(
4133 r#"ˇingress:
4134 ˇapi:
4135 ˇenabled: false
4136 ˇpathType: Prefix
4137"#,
4138 );
4139}
4140
4141#[gpui::test]
4142async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4143 init_test(cx, |settings| {
4144 settings.defaults.hard_tabs = Some(true);
4145 });
4146
4147 let mut cx = EditorTestContext::new(cx).await;
4148
4149 // select two ranges on one line
4150 cx.set_state(indoc! {"
4151 «oneˇ» «twoˇ»
4152 three
4153 four
4154 "});
4155 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4156 cx.assert_editor_state(indoc! {"
4157 \t«oneˇ» «twoˇ»
4158 three
4159 four
4160 "});
4161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4162 cx.assert_editor_state(indoc! {"
4163 \t\t«oneˇ» «twoˇ»
4164 three
4165 four
4166 "});
4167 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4168 cx.assert_editor_state(indoc! {"
4169 \t«oneˇ» «twoˇ»
4170 three
4171 four
4172 "});
4173 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4174 cx.assert_editor_state(indoc! {"
4175 «oneˇ» «twoˇ»
4176 three
4177 four
4178 "});
4179
4180 // select across a line ending
4181 cx.set_state(indoc! {"
4182 one two
4183 t«hree
4184 ˇ»four
4185 "});
4186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4187 cx.assert_editor_state(indoc! {"
4188 one two
4189 \tt«hree
4190 ˇ»four
4191 "});
4192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4193 cx.assert_editor_state(indoc! {"
4194 one two
4195 \t\tt«hree
4196 ˇ»four
4197 "});
4198 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 \tt«hree
4202 ˇ»four
4203 "});
4204 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4205 cx.assert_editor_state(indoc! {"
4206 one two
4207 t«hree
4208 ˇ»four
4209 "});
4210
4211 // Ensure that indenting/outdenting works when the cursor is at column 0.
4212 cx.set_state(indoc! {"
4213 one two
4214 ˇthree
4215 four
4216 "});
4217 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 one two
4220 ˇthree
4221 four
4222 "});
4223 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4224 cx.assert_editor_state(indoc! {"
4225 one two
4226 \tˇthree
4227 four
4228 "});
4229 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4230 cx.assert_editor_state(indoc! {"
4231 one two
4232 ˇthree
4233 four
4234 "});
4235}
4236
4237#[gpui::test]
4238fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4239 init_test(cx, |settings| {
4240 settings.languages.0.extend([
4241 (
4242 "TOML".into(),
4243 LanguageSettingsContent {
4244 tab_size: NonZeroU32::new(2),
4245 ..Default::default()
4246 },
4247 ),
4248 (
4249 "Rust".into(),
4250 LanguageSettingsContent {
4251 tab_size: NonZeroU32::new(4),
4252 ..Default::default()
4253 },
4254 ),
4255 ]);
4256 });
4257
4258 let toml_language = Arc::new(Language::new(
4259 LanguageConfig {
4260 name: "TOML".into(),
4261 ..Default::default()
4262 },
4263 None,
4264 ));
4265 let rust_language = Arc::new(Language::new(
4266 LanguageConfig {
4267 name: "Rust".into(),
4268 ..Default::default()
4269 },
4270 None,
4271 ));
4272
4273 let toml_buffer =
4274 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4275 let rust_buffer =
4276 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4277 let multibuffer = cx.new(|cx| {
4278 let mut multibuffer = MultiBuffer::new(ReadWrite);
4279 multibuffer.push_excerpts(
4280 toml_buffer.clone(),
4281 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4282 cx,
4283 );
4284 multibuffer.push_excerpts(
4285 rust_buffer.clone(),
4286 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4287 cx,
4288 );
4289 multibuffer
4290 });
4291
4292 cx.add_window(|window, cx| {
4293 let mut editor = build_editor(multibuffer, window, cx);
4294
4295 assert_eq!(
4296 editor.text(cx),
4297 indoc! {"
4298 a = 1
4299 b = 2
4300
4301 const c: usize = 3;
4302 "}
4303 );
4304
4305 select_ranges(
4306 &mut editor,
4307 indoc! {"
4308 «aˇ» = 1
4309 b = 2
4310
4311 «const c:ˇ» usize = 3;
4312 "},
4313 window,
4314 cx,
4315 );
4316
4317 editor.tab(&Tab, window, cx);
4318 assert_text_with_selections(
4319 &mut editor,
4320 indoc! {"
4321 «aˇ» = 1
4322 b = 2
4323
4324 «const c:ˇ» usize = 3;
4325 "},
4326 cx,
4327 );
4328 editor.backtab(&Backtab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339
4340 editor
4341 });
4342}
4343
4344#[gpui::test]
4345async fn test_backspace(cx: &mut TestAppContext) {
4346 init_test(cx, |_| {});
4347
4348 let mut cx = EditorTestContext::new(cx).await;
4349
4350 // Basic backspace
4351 cx.set_state(indoc! {"
4352 onˇe two three
4353 fou«rˇ» five six
4354 seven «ˇeight nine
4355 »ten
4356 "});
4357 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 oˇe two three
4360 fouˇ five six
4361 seven ˇten
4362 "});
4363
4364 // Test backspace inside and around indents
4365 cx.set_state(indoc! {"
4366 zero
4367 ˇone
4368 ˇtwo
4369 ˇ ˇ ˇ three
4370 ˇ ˇ four
4371 "});
4372 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4373 cx.assert_editor_state(indoc! {"
4374 zero
4375 ˇone
4376 ˇtwo
4377 ˇ threeˇ four
4378 "});
4379}
4380
4381#[gpui::test]
4382async fn test_delete(cx: &mut TestAppContext) {
4383 init_test(cx, |_| {});
4384
4385 let mut cx = EditorTestContext::new(cx).await;
4386 cx.set_state(indoc! {"
4387 onˇe two three
4388 fou«rˇ» five six
4389 seven «ˇeight nine
4390 »ten
4391 "});
4392 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4393 cx.assert_editor_state(indoc! {"
4394 onˇ two three
4395 fouˇ five six
4396 seven ˇten
4397 "});
4398}
4399
4400#[gpui::test]
4401fn test_delete_line(cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let editor = cx.add_window(|window, cx| {
4405 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4406 build_editor(buffer, window, cx)
4407 });
4408 _ = editor.update(cx, |editor, window, cx| {
4409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4410 s.select_display_ranges([
4411 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4412 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4413 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4414 ])
4415 });
4416 editor.delete_line(&DeleteLine, window, cx);
4417 assert_eq!(editor.display_text(cx), "ghi");
4418 assert_eq!(
4419 editor.selections.display_ranges(cx),
4420 vec![
4421 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 ]
4424 );
4425 });
4426
4427 let editor = cx.add_window(|window, cx| {
4428 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4429 build_editor(buffer, window, cx)
4430 });
4431 _ = editor.update(cx, |editor, window, cx| {
4432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4433 s.select_display_ranges([
4434 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4435 ])
4436 });
4437 editor.delete_line(&DeleteLine, window, cx);
4438 assert_eq!(editor.display_text(cx), "ghi\n");
4439 assert_eq!(
4440 editor.selections.display_ranges(cx),
4441 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4442 );
4443 });
4444
4445 let editor = cx.add_window(|window, cx| {
4446 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4447 build_editor(buffer, window, cx)
4448 });
4449 _ = editor.update(cx, |editor, window, cx| {
4450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4451 s.select_display_ranges([
4452 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4453 ])
4454 });
4455 editor.delete_line(&DeleteLine, window, cx);
4456 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4457 assert_eq!(
4458 editor.selections.display_ranges(cx),
4459 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4460 );
4461 });
4462}
4463
4464#[gpui::test]
4465fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4466 init_test(cx, |_| {});
4467
4468 cx.add_window(|window, cx| {
4469 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4470 let mut editor = build_editor(buffer.clone(), window, cx);
4471 let buffer = buffer.read(cx).as_singleton().unwrap();
4472
4473 assert_eq!(
4474 editor
4475 .selections
4476 .ranges::<Point>(&editor.display_snapshot(cx)),
4477 &[Point::new(0, 0)..Point::new(0, 0)]
4478 );
4479
4480 // When on single line, replace newline at end by space
4481 editor.join_lines(&JoinLines, window, cx);
4482 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4483 assert_eq!(
4484 editor
4485 .selections
4486 .ranges::<Point>(&editor.display_snapshot(cx)),
4487 &[Point::new(0, 3)..Point::new(0, 3)]
4488 );
4489
4490 // When multiple lines are selected, remove newlines that are spanned by the selection
4491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4492 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4493 });
4494 editor.join_lines(&JoinLines, window, cx);
4495 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4496 assert_eq!(
4497 editor
4498 .selections
4499 .ranges::<Point>(&editor.display_snapshot(cx)),
4500 &[Point::new(0, 11)..Point::new(0, 11)]
4501 );
4502
4503 // Undo should be transactional
4504 editor.undo(&Undo, window, cx);
4505 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4506 assert_eq!(
4507 editor
4508 .selections
4509 .ranges::<Point>(&editor.display_snapshot(cx)),
4510 &[Point::new(0, 5)..Point::new(2, 2)]
4511 );
4512
4513 // When joining an empty line don't insert a space
4514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4515 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4516 });
4517 editor.join_lines(&JoinLines, window, cx);
4518 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4519 assert_eq!(
4520 editor
4521 .selections
4522 .ranges::<Point>(&editor.display_snapshot(cx)),
4523 [Point::new(2, 3)..Point::new(2, 3)]
4524 );
4525
4526 // We can remove trailing newlines
4527 editor.join_lines(&JoinLines, window, cx);
4528 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 [Point::new(2, 3)..Point::new(2, 3)]
4534 );
4535
4536 // We don't blow up on the last line
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 [Point::new(2, 3)..Point::new(2, 3)]
4544 );
4545
4546 // reset to test indentation
4547 editor.buffer.update(cx, |buffer, cx| {
4548 buffer.edit(
4549 [
4550 (Point::new(1, 0)..Point::new(1, 2), " "),
4551 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4552 ],
4553 None,
4554 cx,
4555 )
4556 });
4557
4558 // We remove any leading spaces
4559 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4560 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4561 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4562 });
4563 editor.join_lines(&JoinLines, window, cx);
4564 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4565
4566 // We don't insert a space for a line containing only spaces
4567 editor.join_lines(&JoinLines, window, cx);
4568 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4569
4570 // We ignore any leading tabs
4571 editor.join_lines(&JoinLines, window, cx);
4572 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4573
4574 editor
4575 });
4576}
4577
4578#[gpui::test]
4579fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4580 init_test(cx, |_| {});
4581
4582 cx.add_window(|window, cx| {
4583 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4584 let mut editor = build_editor(buffer.clone(), window, cx);
4585 let buffer = buffer.read(cx).as_singleton().unwrap();
4586
4587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4588 s.select_ranges([
4589 Point::new(0, 2)..Point::new(1, 1),
4590 Point::new(1, 2)..Point::new(1, 2),
4591 Point::new(3, 1)..Point::new(3, 2),
4592 ])
4593 });
4594
4595 editor.join_lines(&JoinLines, window, cx);
4596 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4597
4598 assert_eq!(
4599 editor
4600 .selections
4601 .ranges::<Point>(&editor.display_snapshot(cx)),
4602 [
4603 Point::new(0, 7)..Point::new(0, 7),
4604 Point::new(1, 3)..Point::new(1, 3)
4605 ]
4606 );
4607 editor
4608 });
4609}
4610
4611#[gpui::test]
4612async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4613 init_test(cx, |_| {});
4614
4615 let mut cx = EditorTestContext::new(cx).await;
4616
4617 let diff_base = r#"
4618 Line 0
4619 Line 1
4620 Line 2
4621 Line 3
4622 "#
4623 .unindent();
4624
4625 cx.set_state(
4626 &r#"
4627 ˇLine 0
4628 Line 1
4629 Line 2
4630 Line 3
4631 "#
4632 .unindent(),
4633 );
4634
4635 cx.set_head_text(&diff_base);
4636 executor.run_until_parked();
4637
4638 // Join lines
4639 cx.update_editor(|editor, window, cx| {
4640 editor.join_lines(&JoinLines, window, cx);
4641 });
4642 executor.run_until_parked();
4643
4644 cx.assert_editor_state(
4645 &r#"
4646 Line 0ˇ Line 1
4647 Line 2
4648 Line 3
4649 "#
4650 .unindent(),
4651 );
4652 // Join again
4653 cx.update_editor(|editor, window, cx| {
4654 editor.join_lines(&JoinLines, window, cx);
4655 });
4656 executor.run_until_parked();
4657
4658 cx.assert_editor_state(
4659 &r#"
4660 Line 0 Line 1ˇ Line 2
4661 Line 3
4662 "#
4663 .unindent(),
4664 );
4665}
4666
4667#[gpui::test]
4668async fn test_custom_newlines_cause_no_false_positive_diffs(
4669 executor: BackgroundExecutor,
4670 cx: &mut TestAppContext,
4671) {
4672 init_test(cx, |_| {});
4673 let mut cx = EditorTestContext::new(cx).await;
4674 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4675 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4676 executor.run_until_parked();
4677
4678 cx.update_editor(|editor, window, cx| {
4679 let snapshot = editor.snapshot(window, cx);
4680 assert_eq!(
4681 snapshot
4682 .buffer_snapshot()
4683 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4684 .collect::<Vec<_>>(),
4685 Vec::new(),
4686 "Should not have any diffs for files with custom newlines"
4687 );
4688 });
4689}
4690
4691#[gpui::test]
4692async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4693 init_test(cx, |_| {});
4694
4695 let mut cx = EditorTestContext::new(cx).await;
4696
4697 // Test sort_lines_case_insensitive()
4698 cx.set_state(indoc! {"
4699 «z
4700 y
4701 x
4702 Z
4703 Y
4704 Xˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| {
4707 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4708 });
4709 cx.assert_editor_state(indoc! {"
4710 «x
4711 X
4712 y
4713 Y
4714 z
4715 Zˇ»
4716 "});
4717
4718 // Test sort_lines_by_length()
4719 //
4720 // Demonstrates:
4721 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4722 // - sort is stable
4723 cx.set_state(indoc! {"
4724 «123
4725 æ
4726 12
4727 ∞
4728 1
4729 æˇ»
4730 "});
4731 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4732 cx.assert_editor_state(indoc! {"
4733 «æ
4734 ∞
4735 1
4736 æ
4737 12
4738 123ˇ»
4739 "});
4740
4741 // Test reverse_lines()
4742 cx.set_state(indoc! {"
4743 «5
4744 4
4745 3
4746 2
4747 1ˇ»
4748 "});
4749 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4750 cx.assert_editor_state(indoc! {"
4751 «1
4752 2
4753 3
4754 4
4755 5ˇ»
4756 "});
4757
4758 // Skip testing shuffle_line()
4759
4760 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4761 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4762
4763 // Don't manipulate when cursor is on single line, but expand the selection
4764 cx.set_state(indoc! {"
4765 ddˇdd
4766 ccc
4767 bb
4768 a
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «ddddˇ»
4775 ccc
4776 bb
4777 a
4778 "});
4779
4780 // Basic manipulate case
4781 // Start selection moves to column 0
4782 // End of selection shrinks to fit shorter line
4783 cx.set_state(indoc! {"
4784 dd«d
4785 ccc
4786 bb
4787 aaaaaˇ»
4788 "});
4789 cx.update_editor(|e, window, cx| {
4790 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4791 });
4792 cx.assert_editor_state(indoc! {"
4793 «aaaaa
4794 bb
4795 ccc
4796 dddˇ»
4797 "});
4798
4799 // Manipulate case with newlines
4800 cx.set_state(indoc! {"
4801 dd«d
4802 ccc
4803
4804 bb
4805 aaaaa
4806
4807 ˇ»
4808 "});
4809 cx.update_editor(|e, window, cx| {
4810 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4811 });
4812 cx.assert_editor_state(indoc! {"
4813 «
4814
4815 aaaaa
4816 bb
4817 ccc
4818 dddˇ»
4819
4820 "});
4821
4822 // Adding new line
4823 cx.set_state(indoc! {"
4824 aa«a
4825 bbˇ»b
4826 "});
4827 cx.update_editor(|e, window, cx| {
4828 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4829 });
4830 cx.assert_editor_state(indoc! {"
4831 «aaa
4832 bbb
4833 added_lineˇ»
4834 "});
4835
4836 // Removing line
4837 cx.set_state(indoc! {"
4838 aa«a
4839 bbbˇ»
4840 "});
4841 cx.update_editor(|e, window, cx| {
4842 e.manipulate_immutable_lines(window, cx, |lines| {
4843 lines.pop();
4844 })
4845 });
4846 cx.assert_editor_state(indoc! {"
4847 «aaaˇ»
4848 "});
4849
4850 // Removing all lines
4851 cx.set_state(indoc! {"
4852 aa«a
4853 bbbˇ»
4854 "});
4855 cx.update_editor(|e, window, cx| {
4856 e.manipulate_immutable_lines(window, cx, |lines| {
4857 lines.drain(..);
4858 })
4859 });
4860 cx.assert_editor_state(indoc! {"
4861 ˇ
4862 "});
4863}
4864
4865#[gpui::test]
4866async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4867 init_test(cx, |_| {});
4868
4869 let mut cx = EditorTestContext::new(cx).await;
4870
4871 // Consider continuous selection as single selection
4872 cx.set_state(indoc! {"
4873 Aaa«aa
4874 cˇ»c«c
4875 bb
4876 aaaˇ»aa
4877 "});
4878 cx.update_editor(|e, window, cx| {
4879 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4880 });
4881 cx.assert_editor_state(indoc! {"
4882 «Aaaaa
4883 ccc
4884 bb
4885 aaaaaˇ»
4886 "});
4887
4888 cx.set_state(indoc! {"
4889 Aaa«aa
4890 cˇ»c«c
4891 bb
4892 aaaˇ»aa
4893 "});
4894 cx.update_editor(|e, window, cx| {
4895 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4896 });
4897 cx.assert_editor_state(indoc! {"
4898 «Aaaaa
4899 ccc
4900 bbˇ»
4901 "});
4902
4903 // Consider non continuous selection as distinct dedup operations
4904 cx.set_state(indoc! {"
4905 «aaaaa
4906 bb
4907 aaaaa
4908 aaaaaˇ»
4909
4910 aaa«aaˇ»
4911 "});
4912 cx.update_editor(|e, window, cx| {
4913 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4914 });
4915 cx.assert_editor_state(indoc! {"
4916 «aaaaa
4917 bbˇ»
4918
4919 «aaaaaˇ»
4920 "});
4921}
4922
4923#[gpui::test]
4924async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4925 init_test(cx, |_| {});
4926
4927 let mut cx = EditorTestContext::new(cx).await;
4928
4929 cx.set_state(indoc! {"
4930 «Aaa
4931 aAa
4932 Aaaˇ»
4933 "});
4934 cx.update_editor(|e, window, cx| {
4935 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4936 });
4937 cx.assert_editor_state(indoc! {"
4938 «Aaa
4939 aAaˇ»
4940 "});
4941
4942 cx.set_state(indoc! {"
4943 «Aaa
4944 aAa
4945 aaAˇ»
4946 "});
4947 cx.update_editor(|e, window, cx| {
4948 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4949 });
4950 cx.assert_editor_state(indoc! {"
4951 «Aaaˇ»
4952 "});
4953}
4954
4955#[gpui::test]
4956async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4957 init_test(cx, |_| {});
4958
4959 let mut cx = EditorTestContext::new(cx).await;
4960
4961 let js_language = Arc::new(Language::new(
4962 LanguageConfig {
4963 name: "JavaScript".into(),
4964 wrap_characters: Some(language::WrapCharactersConfig {
4965 start_prefix: "<".into(),
4966 start_suffix: ">".into(),
4967 end_prefix: "</".into(),
4968 end_suffix: ">".into(),
4969 }),
4970 ..LanguageConfig::default()
4971 },
4972 None,
4973 ));
4974
4975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4976
4977 cx.set_state(indoc! {"
4978 «testˇ»
4979 "});
4980 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4981 cx.assert_editor_state(indoc! {"
4982 <«ˇ»>test</«ˇ»>
4983 "});
4984
4985 cx.set_state(indoc! {"
4986 «test
4987 testˇ»
4988 "});
4989 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4990 cx.assert_editor_state(indoc! {"
4991 <«ˇ»>test
4992 test</«ˇ»>
4993 "});
4994
4995 cx.set_state(indoc! {"
4996 teˇst
4997 "});
4998 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4999 cx.assert_editor_state(indoc! {"
5000 te<«ˇ»></«ˇ»>st
5001 "});
5002}
5003
5004#[gpui::test]
5005async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5006 init_test(cx, |_| {});
5007
5008 let mut cx = EditorTestContext::new(cx).await;
5009
5010 let js_language = Arc::new(Language::new(
5011 LanguageConfig {
5012 name: "JavaScript".into(),
5013 wrap_characters: Some(language::WrapCharactersConfig {
5014 start_prefix: "<".into(),
5015 start_suffix: ">".into(),
5016 end_prefix: "</".into(),
5017 end_suffix: ">".into(),
5018 }),
5019 ..LanguageConfig::default()
5020 },
5021 None,
5022 ));
5023
5024 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5025
5026 cx.set_state(indoc! {"
5027 «testˇ»
5028 «testˇ» «testˇ»
5029 «testˇ»
5030 "});
5031 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5032 cx.assert_editor_state(indoc! {"
5033 <«ˇ»>test</«ˇ»>
5034 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5035 <«ˇ»>test</«ˇ»>
5036 "});
5037
5038 cx.set_state(indoc! {"
5039 «test
5040 testˇ»
5041 «test
5042 testˇ»
5043 "});
5044 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5045 cx.assert_editor_state(indoc! {"
5046 <«ˇ»>test
5047 test</«ˇ»>
5048 <«ˇ»>test
5049 test</«ˇ»>
5050 "});
5051}
5052
5053#[gpui::test]
5054async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5055 init_test(cx, |_| {});
5056
5057 let mut cx = EditorTestContext::new(cx).await;
5058
5059 let plaintext_language = Arc::new(Language::new(
5060 LanguageConfig {
5061 name: "Plain Text".into(),
5062 ..LanguageConfig::default()
5063 },
5064 None,
5065 ));
5066
5067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5068
5069 cx.set_state(indoc! {"
5070 «testˇ»
5071 "});
5072 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5073 cx.assert_editor_state(indoc! {"
5074 «testˇ»
5075 "});
5076}
5077
5078#[gpui::test]
5079async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5080 init_test(cx, |_| {});
5081
5082 let mut cx = EditorTestContext::new(cx).await;
5083
5084 // Manipulate with multiple selections on a single line
5085 cx.set_state(indoc! {"
5086 dd«dd
5087 cˇ»c«c
5088 bb
5089 aaaˇ»aa
5090 "});
5091 cx.update_editor(|e, window, cx| {
5092 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5093 });
5094 cx.assert_editor_state(indoc! {"
5095 «aaaaa
5096 bb
5097 ccc
5098 ddddˇ»
5099 "});
5100
5101 // Manipulate with multiple disjoin selections
5102 cx.set_state(indoc! {"
5103 5«
5104 4
5105 3
5106 2
5107 1ˇ»
5108
5109 dd«dd
5110 ccc
5111 bb
5112 aaaˇ»aa
5113 "});
5114 cx.update_editor(|e, window, cx| {
5115 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5116 });
5117 cx.assert_editor_state(indoc! {"
5118 «1
5119 2
5120 3
5121 4
5122 5ˇ»
5123
5124 «aaaaa
5125 bb
5126 ccc
5127 ddddˇ»
5128 "});
5129
5130 // Adding lines on each selection
5131 cx.set_state(indoc! {"
5132 2«
5133 1ˇ»
5134
5135 bb«bb
5136 aaaˇ»aa
5137 "});
5138 cx.update_editor(|e, window, cx| {
5139 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5140 });
5141 cx.assert_editor_state(indoc! {"
5142 «2
5143 1
5144 added lineˇ»
5145
5146 «bbbb
5147 aaaaa
5148 added lineˇ»
5149 "});
5150
5151 // Removing lines on each selection
5152 cx.set_state(indoc! {"
5153 2«
5154 1ˇ»
5155
5156 bb«bb
5157 aaaˇ»aa
5158 "});
5159 cx.update_editor(|e, window, cx| {
5160 e.manipulate_immutable_lines(window, cx, |lines| {
5161 lines.pop();
5162 })
5163 });
5164 cx.assert_editor_state(indoc! {"
5165 «2ˇ»
5166
5167 «bbbbˇ»
5168 "});
5169}
5170
5171#[gpui::test]
5172async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5173 init_test(cx, |settings| {
5174 settings.defaults.tab_size = NonZeroU32::new(3)
5175 });
5176
5177 let mut cx = EditorTestContext::new(cx).await;
5178
5179 // MULTI SELECTION
5180 // Ln.1 "«" tests empty lines
5181 // Ln.9 tests just leading whitespace
5182 cx.set_state(indoc! {"
5183 «
5184 abc // No indentationˇ»
5185 «\tabc // 1 tabˇ»
5186 \t\tabc « ˇ» // 2 tabs
5187 \t ab«c // Tab followed by space
5188 \tabc // Space followed by tab (3 spaces should be the result)
5189 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5190 abˇ»ˇc ˇ ˇ // Already space indented«
5191 \t
5192 \tabc\tdef // Only the leading tab is manipulatedˇ»
5193 "});
5194 cx.update_editor(|e, window, cx| {
5195 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5196 });
5197 cx.assert_editor_state(
5198 indoc! {"
5199 «
5200 abc // No indentation
5201 abc // 1 tab
5202 abc // 2 tabs
5203 abc // Tab followed by space
5204 abc // Space followed by tab (3 spaces should be the result)
5205 abc // Mixed indentation (tab conversion depends on the column)
5206 abc // Already space indented
5207 ·
5208 abc\tdef // Only the leading tab is manipulatedˇ»
5209 "}
5210 .replace("·", "")
5211 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5212 );
5213
5214 // Test on just a few lines, the others should remain unchanged
5215 // Only lines (3, 5, 10, 11) should change
5216 cx.set_state(
5217 indoc! {"
5218 ·
5219 abc // No indentation
5220 \tabcˇ // 1 tab
5221 \t\tabc // 2 tabs
5222 \t abcˇ // Tab followed by space
5223 \tabc // Space followed by tab (3 spaces should be the result)
5224 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5225 abc // Already space indented
5226 «\t
5227 \tabc\tdef // Only the leading tab is manipulatedˇ»
5228 "}
5229 .replace("·", "")
5230 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5231 );
5232 cx.update_editor(|e, window, cx| {
5233 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5234 });
5235 cx.assert_editor_state(
5236 indoc! {"
5237 ·
5238 abc // No indentation
5239 « abc // 1 tabˇ»
5240 \t\tabc // 2 tabs
5241 « abc // Tab followed by spaceˇ»
5242 \tabc // Space followed by tab (3 spaces should be the result)
5243 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5244 abc // Already space indented
5245 « ·
5246 abc\tdef // Only the leading tab is manipulatedˇ»
5247 "}
5248 .replace("·", "")
5249 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5250 );
5251
5252 // SINGLE SELECTION
5253 // Ln.1 "«" tests empty lines
5254 // Ln.9 tests just leading whitespace
5255 cx.set_state(indoc! {"
5256 «
5257 abc // No indentation
5258 \tabc // 1 tab
5259 \t\tabc // 2 tabs
5260 \t abc // Tab followed by space
5261 \tabc // Space followed by tab (3 spaces should be the result)
5262 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5263 abc // Already space indented
5264 \t
5265 \tabc\tdef // Only the leading tab is manipulatedˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| {
5268 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5269 });
5270 cx.assert_editor_state(
5271 indoc! {"
5272 «
5273 abc // No indentation
5274 abc // 1 tab
5275 abc // 2 tabs
5276 abc // Tab followed by space
5277 abc // Space followed by tab (3 spaces should be the result)
5278 abc // Mixed indentation (tab conversion depends on the column)
5279 abc // Already space indented
5280 ·
5281 abc\tdef // Only the leading tab is manipulatedˇ»
5282 "}
5283 .replace("·", "")
5284 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5285 );
5286}
5287
5288#[gpui::test]
5289async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5290 init_test(cx, |settings| {
5291 settings.defaults.tab_size = NonZeroU32::new(3)
5292 });
5293
5294 let mut cx = EditorTestContext::new(cx).await;
5295
5296 // MULTI SELECTION
5297 // Ln.1 "«" tests empty lines
5298 // Ln.11 tests just leading whitespace
5299 cx.set_state(indoc! {"
5300 «
5301 abˇ»ˇc // No indentation
5302 abc ˇ ˇ // 1 space (< 3 so dont convert)
5303 abc « // 2 spaces (< 3 so dont convert)
5304 abc // 3 spaces (convert)
5305 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5306 «\tˇ»\t«\tˇ»abc // Already tab indented
5307 «\t abc // Tab followed by space
5308 \tabc // Space followed by tab (should be consumed due to tab)
5309 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5310 \tˇ» «\t
5311 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5312 "});
5313 cx.update_editor(|e, window, cx| {
5314 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5315 });
5316 cx.assert_editor_state(indoc! {"
5317 «
5318 abc // No indentation
5319 abc // 1 space (< 3 so dont convert)
5320 abc // 2 spaces (< 3 so dont convert)
5321 \tabc // 3 spaces (convert)
5322 \t abc // 5 spaces (1 tab + 2 spaces)
5323 \t\t\tabc // Already tab indented
5324 \t abc // Tab followed by space
5325 \tabc // Space followed by tab (should be consumed due to tab)
5326 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5327 \t\t\t
5328 \tabc \t // Only the leading spaces should be convertedˇ»
5329 "});
5330
5331 // Test on just a few lines, the other should remain unchanged
5332 // Only lines (4, 8, 11, 12) should change
5333 cx.set_state(
5334 indoc! {"
5335 ·
5336 abc // No indentation
5337 abc // 1 space (< 3 so dont convert)
5338 abc // 2 spaces (< 3 so dont convert)
5339 « abc // 3 spaces (convert)ˇ»
5340 abc // 5 spaces (1 tab + 2 spaces)
5341 \t\t\tabc // Already tab indented
5342 \t abc // Tab followed by space
5343 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5344 \t\t \tabc // Mixed indentation
5345 \t \t \t \tabc // Mixed indentation
5346 \t \tˇ
5347 « abc \t // Only the leading spaces should be convertedˇ»
5348 "}
5349 .replace("·", "")
5350 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5351 );
5352 cx.update_editor(|e, window, cx| {
5353 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5354 });
5355 cx.assert_editor_state(
5356 indoc! {"
5357 ·
5358 abc // No indentation
5359 abc // 1 space (< 3 so dont convert)
5360 abc // 2 spaces (< 3 so dont convert)
5361 «\tabc // 3 spaces (convert)ˇ»
5362 abc // 5 spaces (1 tab + 2 spaces)
5363 \t\t\tabc // Already tab indented
5364 \t abc // Tab followed by space
5365 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5366 \t\t \tabc // Mixed indentation
5367 \t \t \t \tabc // Mixed indentation
5368 «\t\t\t
5369 \tabc \t // Only the leading spaces should be convertedˇ»
5370 "}
5371 .replace("·", "")
5372 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5373 );
5374
5375 // SINGLE SELECTION
5376 // Ln.1 "«" tests empty lines
5377 // Ln.11 tests just leading whitespace
5378 cx.set_state(indoc! {"
5379 «
5380 abc // No indentation
5381 abc // 1 space (< 3 so dont convert)
5382 abc // 2 spaces (< 3 so dont convert)
5383 abc // 3 spaces (convert)
5384 abc // 5 spaces (1 tab + 2 spaces)
5385 \t\t\tabc // Already tab indented
5386 \t abc // Tab followed by space
5387 \tabc // Space followed by tab (should be consumed due to tab)
5388 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5389 \t \t
5390 abc \t // Only the leading spaces should be convertedˇ»
5391 "});
5392 cx.update_editor(|e, window, cx| {
5393 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5394 });
5395 cx.assert_editor_state(indoc! {"
5396 «
5397 abc // No indentation
5398 abc // 1 space (< 3 so dont convert)
5399 abc // 2 spaces (< 3 so dont convert)
5400 \tabc // 3 spaces (convert)
5401 \t abc // 5 spaces (1 tab + 2 spaces)
5402 \t\t\tabc // Already tab indented
5403 \t abc // Tab followed by space
5404 \tabc // Space followed by tab (should be consumed due to tab)
5405 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5406 \t\t\t
5407 \tabc \t // Only the leading spaces should be convertedˇ»
5408 "});
5409}
5410
5411#[gpui::test]
5412async fn test_toggle_case(cx: &mut TestAppContext) {
5413 init_test(cx, |_| {});
5414
5415 let mut cx = EditorTestContext::new(cx).await;
5416
5417 // If all lower case -> upper case
5418 cx.set_state(indoc! {"
5419 «hello worldˇ»
5420 "});
5421 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5422 cx.assert_editor_state(indoc! {"
5423 «HELLO WORLDˇ»
5424 "});
5425
5426 // If all upper case -> lower case
5427 cx.set_state(indoc! {"
5428 «HELLO WORLDˇ»
5429 "});
5430 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5431 cx.assert_editor_state(indoc! {"
5432 «hello worldˇ»
5433 "});
5434
5435 // If any upper case characters are identified -> lower case
5436 // This matches JetBrains IDEs
5437 cx.set_state(indoc! {"
5438 «hEllo worldˇ»
5439 "});
5440 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5441 cx.assert_editor_state(indoc! {"
5442 «hello worldˇ»
5443 "});
5444}
5445
5446#[gpui::test]
5447async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5448 init_test(cx, |_| {});
5449
5450 let mut cx = EditorTestContext::new(cx).await;
5451
5452 cx.set_state(indoc! {"
5453 «implement-windows-supportˇ»
5454 "});
5455 cx.update_editor(|e, window, cx| {
5456 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5457 });
5458 cx.assert_editor_state(indoc! {"
5459 «Implement windows supportˇ»
5460 "});
5461}
5462
5463#[gpui::test]
5464async fn test_manipulate_text(cx: &mut TestAppContext) {
5465 init_test(cx, |_| {});
5466
5467 let mut cx = EditorTestContext::new(cx).await;
5468
5469 // Test convert_to_upper_case()
5470 cx.set_state(indoc! {"
5471 «hello worldˇ»
5472 "});
5473 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5474 cx.assert_editor_state(indoc! {"
5475 «HELLO WORLDˇ»
5476 "});
5477
5478 // Test convert_to_lower_case()
5479 cx.set_state(indoc! {"
5480 «HELLO WORLDˇ»
5481 "});
5482 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5483 cx.assert_editor_state(indoc! {"
5484 «hello worldˇ»
5485 "});
5486
5487 // Test multiple line, single selection case
5488 cx.set_state(indoc! {"
5489 «The quick brown
5490 fox jumps over
5491 the lazy dogˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «The Quick Brown
5496 Fox Jumps Over
5497 The Lazy Dogˇ»
5498 "});
5499
5500 // Test multiple line, single selection case
5501 cx.set_state(indoc! {"
5502 «The quick brown
5503 fox jumps over
5504 the lazy dogˇ»
5505 "});
5506 cx.update_editor(|e, window, cx| {
5507 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5508 });
5509 cx.assert_editor_state(indoc! {"
5510 «TheQuickBrown
5511 FoxJumpsOver
5512 TheLazyDogˇ»
5513 "});
5514
5515 // From here on out, test more complex cases of manipulate_text()
5516
5517 // Test no selection case - should affect words cursors are in
5518 // Cursor at beginning, middle, and end of word
5519 cx.set_state(indoc! {"
5520 ˇhello big beauˇtiful worldˇ
5521 "});
5522 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5523 cx.assert_editor_state(indoc! {"
5524 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5525 "});
5526
5527 // Test multiple selections on a single line and across multiple lines
5528 cx.set_state(indoc! {"
5529 «Theˇ» quick «brown
5530 foxˇ» jumps «overˇ»
5531 the «lazyˇ» dog
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «THEˇ» quick «BROWN
5536 FOXˇ» jumps «OVERˇ»
5537 the «LAZYˇ» dog
5538 "});
5539
5540 // Test case where text length grows
5541 cx.set_state(indoc! {"
5542 «tschüߡ»
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «TSCHÜSSˇ»
5547 "});
5548
5549 // Test to make sure we don't crash when text shrinks
5550 cx.set_state(indoc! {"
5551 aaa_bbbˇ
5552 "});
5553 cx.update_editor(|e, window, cx| {
5554 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5555 });
5556 cx.assert_editor_state(indoc! {"
5557 «aaaBbbˇ»
5558 "});
5559
5560 // Test to make sure we all aware of the fact that each word can grow and shrink
5561 // Final selections should be aware of this fact
5562 cx.set_state(indoc! {"
5563 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5564 "});
5565 cx.update_editor(|e, window, cx| {
5566 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5567 });
5568 cx.assert_editor_state(indoc! {"
5569 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5570 "});
5571
5572 cx.set_state(indoc! {"
5573 «hElLo, WoRld!ˇ»
5574 "});
5575 cx.update_editor(|e, window, cx| {
5576 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5577 });
5578 cx.assert_editor_state(indoc! {"
5579 «HeLlO, wOrLD!ˇ»
5580 "});
5581
5582 // Test selections with `line_mode() = true`.
5583 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5584 cx.set_state(indoc! {"
5585 «The quick brown
5586 fox jumps over
5587 tˇ»he lazy dog
5588 "});
5589 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 «THE QUICK BROWN
5592 FOX JUMPS OVER
5593 THE LAZY DOGˇ»
5594 "});
5595}
5596
5597#[gpui::test]
5598fn test_duplicate_line(cx: &mut TestAppContext) {
5599 init_test(cx, |_| {});
5600
5601 let editor = cx.add_window(|window, cx| {
5602 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5603 build_editor(buffer, window, cx)
5604 });
5605 _ = editor.update(cx, |editor, window, cx| {
5606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5607 s.select_display_ranges([
5608 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5609 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5610 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5611 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5612 ])
5613 });
5614 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5615 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5616 assert_eq!(
5617 editor.selections.display_ranges(cx),
5618 vec![
5619 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5620 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5621 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5622 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5623 ]
5624 );
5625 });
5626
5627 let editor = cx.add_window(|window, cx| {
5628 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5629 build_editor(buffer, window, cx)
5630 });
5631 _ = editor.update(cx, |editor, window, cx| {
5632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5633 s.select_display_ranges([
5634 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5635 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5636 ])
5637 });
5638 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5639 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5640 assert_eq!(
5641 editor.selections.display_ranges(cx),
5642 vec![
5643 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5644 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5645 ]
5646 );
5647 });
5648
5649 // With `duplicate_line_up` the selections move to the duplicated lines,
5650 // which are inserted above the original lines
5651 let editor = cx.add_window(|window, cx| {
5652 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5653 build_editor(buffer, window, cx)
5654 });
5655 _ = editor.update(cx, |editor, window, cx| {
5656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5657 s.select_display_ranges([
5658 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5659 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5660 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5661 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5662 ])
5663 });
5664 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5665 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5666 assert_eq!(
5667 editor.selections.display_ranges(cx),
5668 vec![
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5672 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5673 ]
5674 );
5675 });
5676
5677 let editor = cx.add_window(|window, cx| {
5678 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5679 build_editor(buffer, window, cx)
5680 });
5681 _ = editor.update(cx, |editor, window, cx| {
5682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5683 s.select_display_ranges([
5684 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5685 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5686 ])
5687 });
5688 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5689 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5690 assert_eq!(
5691 editor.selections.display_ranges(cx),
5692 vec![
5693 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5694 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5695 ]
5696 );
5697 });
5698
5699 let editor = cx.add_window(|window, cx| {
5700 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5701 build_editor(buffer, window, cx)
5702 });
5703 _ = editor.update(cx, |editor, window, cx| {
5704 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5705 s.select_display_ranges([
5706 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5707 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5708 ])
5709 });
5710 editor.duplicate_selection(&DuplicateSelection, window, cx);
5711 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5712 assert_eq!(
5713 editor.selections.display_ranges(cx),
5714 vec![
5715 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5716 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5717 ]
5718 );
5719 });
5720}
5721
5722#[gpui::test]
5723fn test_move_line_up_down(cx: &mut TestAppContext) {
5724 init_test(cx, |_| {});
5725
5726 let editor = cx.add_window(|window, cx| {
5727 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5728 build_editor(buffer, window, cx)
5729 });
5730 _ = editor.update(cx, |editor, window, cx| {
5731 editor.fold_creases(
5732 vec![
5733 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5734 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5735 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5736 ],
5737 true,
5738 window,
5739 cx,
5740 );
5741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5742 s.select_display_ranges([
5743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5744 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5745 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5746 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5747 ])
5748 });
5749 assert_eq!(
5750 editor.display_text(cx),
5751 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5752 );
5753
5754 editor.move_line_up(&MoveLineUp, window, cx);
5755 assert_eq!(
5756 editor.display_text(cx),
5757 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5758 );
5759 assert_eq!(
5760 editor.selections.display_ranges(cx),
5761 vec![
5762 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5763 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5764 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5765 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5766 ]
5767 );
5768 });
5769
5770 _ = editor.update(cx, |editor, window, cx| {
5771 editor.move_line_down(&MoveLineDown, window, cx);
5772 assert_eq!(
5773 editor.display_text(cx),
5774 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5775 );
5776 assert_eq!(
5777 editor.selections.display_ranges(cx),
5778 vec![
5779 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5780 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5781 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5782 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5783 ]
5784 );
5785 });
5786
5787 _ = editor.update(cx, |editor, window, cx| {
5788 editor.move_line_down(&MoveLineDown, window, cx);
5789 assert_eq!(
5790 editor.display_text(cx),
5791 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5792 );
5793 assert_eq!(
5794 editor.selections.display_ranges(cx),
5795 vec![
5796 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5797 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5798 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5799 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5800 ]
5801 );
5802 });
5803
5804 _ = editor.update(cx, |editor, window, cx| {
5805 editor.move_line_up(&MoveLineUp, window, cx);
5806 assert_eq!(
5807 editor.display_text(cx),
5808 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5809 );
5810 assert_eq!(
5811 editor.selections.display_ranges(cx),
5812 vec![
5813 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5814 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5815 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5816 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5817 ]
5818 );
5819 });
5820}
5821
5822#[gpui::test]
5823fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825 let editor = cx.add_window(|window, cx| {
5826 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5827 build_editor(buffer, window, cx)
5828 });
5829 _ = editor.update(cx, |editor, window, cx| {
5830 editor.fold_creases(
5831 vec![Crease::simple(
5832 Point::new(6, 4)..Point::new(7, 4),
5833 FoldPlaceholder::test(),
5834 )],
5835 true,
5836 window,
5837 cx,
5838 );
5839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5840 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5841 });
5842 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5843 editor.move_line_up(&MoveLineUp, window, cx);
5844 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5845 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5846 });
5847}
5848
5849#[gpui::test]
5850fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5851 init_test(cx, |_| {});
5852
5853 let editor = cx.add_window(|window, cx| {
5854 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5855 build_editor(buffer, window, cx)
5856 });
5857 _ = editor.update(cx, |editor, window, cx| {
5858 let snapshot = editor.buffer.read(cx).snapshot(cx);
5859 editor.insert_blocks(
5860 [BlockProperties {
5861 style: BlockStyle::Fixed,
5862 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5863 height: Some(1),
5864 render: Arc::new(|_| div().into_any()),
5865 priority: 0,
5866 }],
5867 Some(Autoscroll::fit()),
5868 cx,
5869 );
5870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5871 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5872 });
5873 editor.move_line_down(&MoveLineDown, window, cx);
5874 });
5875}
5876
5877#[gpui::test]
5878async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5879 init_test(cx, |_| {});
5880
5881 let mut cx = EditorTestContext::new(cx).await;
5882 cx.set_state(
5883 &"
5884 ˇzero
5885 one
5886 two
5887 three
5888 four
5889 five
5890 "
5891 .unindent(),
5892 );
5893
5894 // Create a four-line block that replaces three lines of text.
5895 cx.update_editor(|editor, window, cx| {
5896 let snapshot = editor.snapshot(window, cx);
5897 let snapshot = &snapshot.buffer_snapshot();
5898 let placement = BlockPlacement::Replace(
5899 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5900 );
5901 editor.insert_blocks(
5902 [BlockProperties {
5903 placement,
5904 height: Some(4),
5905 style: BlockStyle::Sticky,
5906 render: Arc::new(|_| gpui::div().into_any_element()),
5907 priority: 0,
5908 }],
5909 None,
5910 cx,
5911 );
5912 });
5913
5914 // Move down so that the cursor touches the block.
5915 cx.update_editor(|editor, window, cx| {
5916 editor.move_down(&Default::default(), window, cx);
5917 });
5918 cx.assert_editor_state(
5919 &"
5920 zero
5921 «one
5922 two
5923 threeˇ»
5924 four
5925 five
5926 "
5927 .unindent(),
5928 );
5929
5930 // Move down past the block.
5931 cx.update_editor(|editor, window, cx| {
5932 editor.move_down(&Default::default(), window, cx);
5933 });
5934 cx.assert_editor_state(
5935 &"
5936 zero
5937 one
5938 two
5939 three
5940 ˇfour
5941 five
5942 "
5943 .unindent(),
5944 );
5945}
5946
5947#[gpui::test]
5948fn test_transpose(cx: &mut TestAppContext) {
5949 init_test(cx, |_| {});
5950
5951 _ = cx.add_window(|window, cx| {
5952 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5953 editor.set_style(EditorStyle::default(), window, cx);
5954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5955 s.select_ranges([1..1])
5956 });
5957 editor.transpose(&Default::default(), window, cx);
5958 assert_eq!(editor.text(cx), "bac");
5959 assert_eq!(
5960 editor.selections.ranges(&editor.display_snapshot(cx)),
5961 [2..2]
5962 );
5963
5964 editor.transpose(&Default::default(), window, cx);
5965 assert_eq!(editor.text(cx), "bca");
5966 assert_eq!(
5967 editor.selections.ranges(&editor.display_snapshot(cx)),
5968 [3..3]
5969 );
5970
5971 editor.transpose(&Default::default(), window, cx);
5972 assert_eq!(editor.text(cx), "bac");
5973 assert_eq!(
5974 editor.selections.ranges(&editor.display_snapshot(cx)),
5975 [3..3]
5976 );
5977
5978 editor
5979 });
5980
5981 _ = cx.add_window(|window, cx| {
5982 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5983 editor.set_style(EditorStyle::default(), window, cx);
5984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5985 s.select_ranges([3..3])
5986 });
5987 editor.transpose(&Default::default(), window, cx);
5988 assert_eq!(editor.text(cx), "acb\nde");
5989 assert_eq!(
5990 editor.selections.ranges(&editor.display_snapshot(cx)),
5991 [3..3]
5992 );
5993
5994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5995 s.select_ranges([4..4])
5996 });
5997 editor.transpose(&Default::default(), window, cx);
5998 assert_eq!(editor.text(cx), "acbd\ne");
5999 assert_eq!(
6000 editor.selections.ranges(&editor.display_snapshot(cx)),
6001 [5..5]
6002 );
6003
6004 editor.transpose(&Default::default(), window, cx);
6005 assert_eq!(editor.text(cx), "acbde\n");
6006 assert_eq!(
6007 editor.selections.ranges(&editor.display_snapshot(cx)),
6008 [6..6]
6009 );
6010
6011 editor.transpose(&Default::default(), window, cx);
6012 assert_eq!(editor.text(cx), "acbd\ne");
6013 assert_eq!(
6014 editor.selections.ranges(&editor.display_snapshot(cx)),
6015 [6..6]
6016 );
6017
6018 editor
6019 });
6020
6021 _ = cx.add_window(|window, cx| {
6022 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6023 editor.set_style(EditorStyle::default(), window, cx);
6024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6025 s.select_ranges([1..1, 2..2, 4..4])
6026 });
6027 editor.transpose(&Default::default(), window, cx);
6028 assert_eq!(editor.text(cx), "bacd\ne");
6029 assert_eq!(
6030 editor.selections.ranges(&editor.display_snapshot(cx)),
6031 [2..2, 3..3, 5..5]
6032 );
6033
6034 editor.transpose(&Default::default(), window, cx);
6035 assert_eq!(editor.text(cx), "bcade\n");
6036 assert_eq!(
6037 editor.selections.ranges(&editor.display_snapshot(cx)),
6038 [3..3, 4..4, 6..6]
6039 );
6040
6041 editor.transpose(&Default::default(), window, cx);
6042 assert_eq!(editor.text(cx), "bcda\ne");
6043 assert_eq!(
6044 editor.selections.ranges(&editor.display_snapshot(cx)),
6045 [4..4, 6..6]
6046 );
6047
6048 editor.transpose(&Default::default(), window, cx);
6049 assert_eq!(editor.text(cx), "bcade\n");
6050 assert_eq!(
6051 editor.selections.ranges(&editor.display_snapshot(cx)),
6052 [4..4, 6..6]
6053 );
6054
6055 editor.transpose(&Default::default(), window, cx);
6056 assert_eq!(editor.text(cx), "bcaed\n");
6057 assert_eq!(
6058 editor.selections.ranges(&editor.display_snapshot(cx)),
6059 [5..5, 6..6]
6060 );
6061
6062 editor
6063 });
6064
6065 _ = cx.add_window(|window, cx| {
6066 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6067 editor.set_style(EditorStyle::default(), window, cx);
6068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6069 s.select_ranges([4..4])
6070 });
6071 editor.transpose(&Default::default(), window, cx);
6072 assert_eq!(editor.text(cx), "🏀🍐✋");
6073 assert_eq!(
6074 editor.selections.ranges(&editor.display_snapshot(cx)),
6075 [8..8]
6076 );
6077
6078 editor.transpose(&Default::default(), window, cx);
6079 assert_eq!(editor.text(cx), "🏀✋🍐");
6080 assert_eq!(
6081 editor.selections.ranges(&editor.display_snapshot(cx)),
6082 [11..11]
6083 );
6084
6085 editor.transpose(&Default::default(), window, cx);
6086 assert_eq!(editor.text(cx), "🏀🍐✋");
6087 assert_eq!(
6088 editor.selections.ranges(&editor.display_snapshot(cx)),
6089 [11..11]
6090 );
6091
6092 editor
6093 });
6094}
6095
6096#[gpui::test]
6097async fn test_rewrap(cx: &mut TestAppContext) {
6098 init_test(cx, |settings| {
6099 settings.languages.0.extend([
6100 (
6101 "Markdown".into(),
6102 LanguageSettingsContent {
6103 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6104 preferred_line_length: Some(40),
6105 ..Default::default()
6106 },
6107 ),
6108 (
6109 "Plain Text".into(),
6110 LanguageSettingsContent {
6111 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6112 preferred_line_length: Some(40),
6113 ..Default::default()
6114 },
6115 ),
6116 (
6117 "C++".into(),
6118 LanguageSettingsContent {
6119 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6120 preferred_line_length: Some(40),
6121 ..Default::default()
6122 },
6123 ),
6124 (
6125 "Python".into(),
6126 LanguageSettingsContent {
6127 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6128 preferred_line_length: Some(40),
6129 ..Default::default()
6130 },
6131 ),
6132 (
6133 "Rust".into(),
6134 LanguageSettingsContent {
6135 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6136 preferred_line_length: Some(40),
6137 ..Default::default()
6138 },
6139 ),
6140 ])
6141 });
6142
6143 let mut cx = EditorTestContext::new(cx).await;
6144
6145 let cpp_language = Arc::new(Language::new(
6146 LanguageConfig {
6147 name: "C++".into(),
6148 line_comments: vec!["// ".into()],
6149 ..LanguageConfig::default()
6150 },
6151 None,
6152 ));
6153 let python_language = Arc::new(Language::new(
6154 LanguageConfig {
6155 name: "Python".into(),
6156 line_comments: vec!["# ".into()],
6157 ..LanguageConfig::default()
6158 },
6159 None,
6160 ));
6161 let markdown_language = Arc::new(Language::new(
6162 LanguageConfig {
6163 name: "Markdown".into(),
6164 rewrap_prefixes: vec![
6165 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6166 regex::Regex::new("[-*+]\\s+").unwrap(),
6167 ],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let rust_language = Arc::new(
6173 Language::new(
6174 LanguageConfig {
6175 name: "Rust".into(),
6176 line_comments: vec!["// ".into(), "/// ".into()],
6177 ..LanguageConfig::default()
6178 },
6179 Some(tree_sitter_rust::LANGUAGE.into()),
6180 )
6181 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6182 .unwrap(),
6183 );
6184
6185 let plaintext_language = Arc::new(Language::new(
6186 LanguageConfig {
6187 name: "Plain Text".into(),
6188 ..LanguageConfig::default()
6189 },
6190 None,
6191 ));
6192
6193 // Test basic rewrapping of a long line with a cursor
6194 assert_rewrap(
6195 indoc! {"
6196 // ˇThis is a long comment that needs to be wrapped.
6197 "},
6198 indoc! {"
6199 // ˇThis is a long comment that needs to
6200 // be wrapped.
6201 "},
6202 cpp_language.clone(),
6203 &mut cx,
6204 );
6205
6206 // Test rewrapping a full selection
6207 assert_rewrap(
6208 indoc! {"
6209 «// This selected long comment needs to be wrapped.ˇ»"
6210 },
6211 indoc! {"
6212 «// This selected long comment needs to
6213 // be wrapped.ˇ»"
6214 },
6215 cpp_language.clone(),
6216 &mut cx,
6217 );
6218
6219 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6220 assert_rewrap(
6221 indoc! {"
6222 // ˇThis is the first line.
6223 // Thisˇ is the second line.
6224 // This is the thirdˇ line, all part of one paragraph.
6225 "},
6226 indoc! {"
6227 // ˇThis is the first line. Thisˇ is the
6228 // second line. This is the thirdˇ line,
6229 // all part of one paragraph.
6230 "},
6231 cpp_language.clone(),
6232 &mut cx,
6233 );
6234
6235 // Test multiple cursors in different paragraphs trigger separate rewraps
6236 assert_rewrap(
6237 indoc! {"
6238 // ˇThis is the first paragraph, first line.
6239 // ˇThis is the first paragraph, second line.
6240
6241 // ˇThis is the second paragraph, first line.
6242 // ˇThis is the second paragraph, second line.
6243 "},
6244 indoc! {"
6245 // ˇThis is the first paragraph, first
6246 // line. ˇThis is the first paragraph,
6247 // second line.
6248
6249 // ˇThis is the second paragraph, first
6250 // line. ˇThis is the second paragraph,
6251 // second line.
6252 "},
6253 cpp_language.clone(),
6254 &mut cx,
6255 );
6256
6257 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6258 assert_rewrap(
6259 indoc! {"
6260 «// A regular long long comment to be wrapped.
6261 /// A documentation long comment to be wrapped.ˇ»
6262 "},
6263 indoc! {"
6264 «// A regular long long comment to be
6265 // wrapped.
6266 /// A documentation long comment to be
6267 /// wrapped.ˇ»
6268 "},
6269 rust_language.clone(),
6270 &mut cx,
6271 );
6272
6273 // Test that change in indentation level trigger seperate rewraps
6274 assert_rewrap(
6275 indoc! {"
6276 fn foo() {
6277 «// This is a long comment at the base indent.
6278 // This is a long comment at the next indent.ˇ»
6279 }
6280 "},
6281 indoc! {"
6282 fn foo() {
6283 «// This is a long comment at the
6284 // base indent.
6285 // This is a long comment at the
6286 // next indent.ˇ»
6287 }
6288 "},
6289 rust_language.clone(),
6290 &mut cx,
6291 );
6292
6293 // Test that different comment prefix characters (e.g., '#') are handled correctly
6294 assert_rewrap(
6295 indoc! {"
6296 # ˇThis is a long comment using a pound sign.
6297 "},
6298 indoc! {"
6299 # ˇThis is a long comment using a pound
6300 # sign.
6301 "},
6302 python_language,
6303 &mut cx,
6304 );
6305
6306 // Test rewrapping only affects comments, not code even when selected
6307 assert_rewrap(
6308 indoc! {"
6309 «/// This doc comment is long and should be wrapped.
6310 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6311 "},
6312 indoc! {"
6313 «/// This doc comment is long and should
6314 /// be wrapped.
6315 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6316 "},
6317 rust_language.clone(),
6318 &mut cx,
6319 );
6320
6321 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6322 assert_rewrap(
6323 indoc! {"
6324 # Header
6325
6326 A long long long line of markdown text to wrap.ˇ
6327 "},
6328 indoc! {"
6329 # Header
6330
6331 A long long long line of markdown text
6332 to wrap.ˇ
6333 "},
6334 markdown_language.clone(),
6335 &mut cx,
6336 );
6337
6338 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6339 assert_rewrap(
6340 indoc! {"
6341 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6342 2. This is a numbered list item that is very long and needs to be wrapped properly.
6343 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6344 "},
6345 indoc! {"
6346 «1. This is a numbered list item that is
6347 very long and needs to be wrapped
6348 properly.
6349 2. This is a numbered list item that is
6350 very long and needs to be wrapped
6351 properly.
6352 - This is an unordered list item that is
6353 also very long and should not merge
6354 with the numbered item.ˇ»
6355 "},
6356 markdown_language.clone(),
6357 &mut cx,
6358 );
6359
6360 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6361 assert_rewrap(
6362 indoc! {"
6363 «1. This is a numbered list item that is
6364 very long and needs to be wrapped
6365 properly.
6366 2. This is a numbered list item that is
6367 very long and needs to be wrapped
6368 properly.
6369 - This is an unordered list item that is
6370 also very long and should not merge with
6371 the numbered item.ˇ»
6372 "},
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge
6382 with the numbered item.ˇ»
6383 "},
6384 markdown_language.clone(),
6385 &mut cx,
6386 );
6387
6388 // Test that rewrapping maintain indents even when they already exists.
6389 assert_rewrap(
6390 indoc! {"
6391 «1. This is a numbered list
6392 item that is very long and needs to be wrapped properly.
6393 2. This is a numbered list
6394 item that is very long and needs to be wrapped properly.
6395 - This is an unordered list item that is also very long and
6396 should not merge with the numbered item.ˇ»
6397 "},
6398 indoc! {"
6399 «1. This is a numbered list item that is
6400 very long and needs to be wrapped
6401 properly.
6402 2. This is a numbered list item that is
6403 very long and needs to be wrapped
6404 properly.
6405 - This is an unordered list item that is
6406 also very long and should not merge
6407 with the numbered item.ˇ»
6408 "},
6409 markdown_language,
6410 &mut cx,
6411 );
6412
6413 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6414 assert_rewrap(
6415 indoc! {"
6416 ˇThis is a very long line of plain text that will be wrapped.
6417 "},
6418 indoc! {"
6419 ˇThis is a very long line of plain text
6420 that will be wrapped.
6421 "},
6422 plaintext_language.clone(),
6423 &mut cx,
6424 );
6425
6426 // Test that non-commented code acts as a paragraph boundary within a selection
6427 assert_rewrap(
6428 indoc! {"
6429 «// This is the first long comment block to be wrapped.
6430 fn my_func(a: u32);
6431 // This is the second long comment block to be wrapped.ˇ»
6432 "},
6433 indoc! {"
6434 «// This is the first long comment block
6435 // to be wrapped.
6436 fn my_func(a: u32);
6437 // This is the second long comment block
6438 // to be wrapped.ˇ»
6439 "},
6440 rust_language,
6441 &mut cx,
6442 );
6443
6444 // Test rewrapping multiple selections, including ones with blank lines or tabs
6445 assert_rewrap(
6446 indoc! {"
6447 «ˇThis is a very long line that will be wrapped.
6448
6449 This is another paragraph in the same selection.»
6450
6451 «\tThis is a very long indented line that will be wrapped.ˇ»
6452 "},
6453 indoc! {"
6454 «ˇThis is a very long line that will be
6455 wrapped.
6456
6457 This is another paragraph in the same
6458 selection.»
6459
6460 «\tThis is a very long indented line
6461 \tthat will be wrapped.ˇ»
6462 "},
6463 plaintext_language,
6464 &mut cx,
6465 );
6466
6467 // Test that an empty comment line acts as a paragraph boundary
6468 assert_rewrap(
6469 indoc! {"
6470 // ˇThis is a long comment that will be wrapped.
6471 //
6472 // And this is another long comment that will also be wrapped.ˇ
6473 "},
6474 indoc! {"
6475 // ˇThis is a long comment that will be
6476 // wrapped.
6477 //
6478 // And this is another long comment that
6479 // will also be wrapped.ˇ
6480 "},
6481 cpp_language,
6482 &mut cx,
6483 );
6484
6485 #[track_caller]
6486 fn assert_rewrap(
6487 unwrapped_text: &str,
6488 wrapped_text: &str,
6489 language: Arc<Language>,
6490 cx: &mut EditorTestContext,
6491 ) {
6492 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6493 cx.set_state(unwrapped_text);
6494 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6495 cx.assert_editor_state(wrapped_text);
6496 }
6497}
6498
6499#[gpui::test]
6500async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6501 init_test(cx, |settings| {
6502 settings.languages.0.extend([(
6503 "Rust".into(),
6504 LanguageSettingsContent {
6505 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6506 preferred_line_length: Some(40),
6507 ..Default::default()
6508 },
6509 )])
6510 });
6511
6512 let mut cx = EditorTestContext::new(cx).await;
6513
6514 let rust_lang = Arc::new(
6515 Language::new(
6516 LanguageConfig {
6517 name: "Rust".into(),
6518 line_comments: vec!["// ".into()],
6519 block_comment: Some(BlockCommentConfig {
6520 start: "/*".into(),
6521 end: "*/".into(),
6522 prefix: "* ".into(),
6523 tab_size: 1,
6524 }),
6525 documentation_comment: Some(BlockCommentConfig {
6526 start: "/**".into(),
6527 end: "*/".into(),
6528 prefix: "* ".into(),
6529 tab_size: 1,
6530 }),
6531
6532 ..LanguageConfig::default()
6533 },
6534 Some(tree_sitter_rust::LANGUAGE.into()),
6535 )
6536 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6537 .unwrap(),
6538 );
6539
6540 // regular block comment
6541 assert_rewrap(
6542 indoc! {"
6543 /*
6544 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6545 */
6546 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6547 "},
6548 indoc! {"
6549 /*
6550 *ˇ Lorem ipsum dolor sit amet,
6551 * consectetur adipiscing elit.
6552 */
6553 /*
6554 *ˇ Lorem ipsum dolor sit amet,
6555 * consectetur adipiscing elit.
6556 */
6557 "},
6558 rust_lang.clone(),
6559 &mut cx,
6560 );
6561
6562 // indent is respected
6563 assert_rewrap(
6564 indoc! {"
6565 {}
6566 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6567 "},
6568 indoc! {"
6569 {}
6570 /*
6571 *ˇ Lorem ipsum dolor sit amet,
6572 * consectetur adipiscing elit.
6573 */
6574 "},
6575 rust_lang.clone(),
6576 &mut cx,
6577 );
6578
6579 // short block comments with inline delimiters
6580 assert_rewrap(
6581 indoc! {"
6582 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6583 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6584 */
6585 /*
6586 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6587 "},
6588 indoc! {"
6589 /*
6590 *ˇ Lorem ipsum dolor sit amet,
6591 * consectetur adipiscing elit.
6592 */
6593 /*
6594 *ˇ Lorem ipsum dolor sit amet,
6595 * consectetur adipiscing elit.
6596 */
6597 /*
6598 *ˇ Lorem ipsum dolor sit amet,
6599 * consectetur adipiscing elit.
6600 */
6601 "},
6602 rust_lang.clone(),
6603 &mut cx,
6604 );
6605
6606 // multiline block comment with inline start/end delimiters
6607 assert_rewrap(
6608 indoc! {"
6609 /*ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit. */
6611 "},
6612 indoc! {"
6613 /*
6614 *ˇ Lorem ipsum dolor sit amet,
6615 * consectetur adipiscing elit.
6616 */
6617 "},
6618 rust_lang.clone(),
6619 &mut cx,
6620 );
6621
6622 // block comment rewrap still respects paragraph bounds
6623 assert_rewrap(
6624 indoc! {"
6625 /*
6626 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6627 *
6628 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6629 */
6630 "},
6631 indoc! {"
6632 /*
6633 *ˇ Lorem ipsum dolor sit amet,
6634 * consectetur adipiscing elit.
6635 *
6636 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6637 */
6638 "},
6639 rust_lang.clone(),
6640 &mut cx,
6641 );
6642
6643 // documentation comments
6644 assert_rewrap(
6645 indoc! {"
6646 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6647 /**
6648 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6649 */
6650 "},
6651 indoc! {"
6652 /**
6653 *ˇ Lorem ipsum dolor sit amet,
6654 * consectetur adipiscing elit.
6655 */
6656 /**
6657 *ˇ Lorem ipsum dolor sit amet,
6658 * consectetur adipiscing elit.
6659 */
6660 "},
6661 rust_lang.clone(),
6662 &mut cx,
6663 );
6664
6665 // different, adjacent comments
6666 assert_rewrap(
6667 indoc! {"
6668 /**
6669 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6670 */
6671 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6672 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6673 "},
6674 indoc! {"
6675 /**
6676 *ˇ Lorem ipsum dolor sit amet,
6677 * consectetur adipiscing elit.
6678 */
6679 /*
6680 *ˇ Lorem ipsum dolor sit amet,
6681 * consectetur adipiscing elit.
6682 */
6683 //ˇ Lorem ipsum dolor sit amet,
6684 // consectetur adipiscing elit.
6685 "},
6686 rust_lang.clone(),
6687 &mut cx,
6688 );
6689
6690 // selection w/ single short block comment
6691 assert_rewrap(
6692 indoc! {"
6693 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6694 "},
6695 indoc! {"
6696 «/*
6697 * Lorem ipsum dolor sit amet,
6698 * consectetur adipiscing elit.
6699 */ˇ»
6700 "},
6701 rust_lang.clone(),
6702 &mut cx,
6703 );
6704
6705 // rewrapping a single comment w/ abutting comments
6706 assert_rewrap(
6707 indoc! {"
6708 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6709 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6710 "},
6711 indoc! {"
6712 /*
6713 * ˇLorem ipsum dolor sit amet,
6714 * consectetur adipiscing elit.
6715 */
6716 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6717 "},
6718 rust_lang.clone(),
6719 &mut cx,
6720 );
6721
6722 // selection w/ non-abutting short block comments
6723 assert_rewrap(
6724 indoc! {"
6725 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6726
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6728 "},
6729 indoc! {"
6730 «/*
6731 * Lorem ipsum dolor sit amet,
6732 * consectetur adipiscing elit.
6733 */
6734
6735 /*
6736 * Lorem ipsum dolor sit amet,
6737 * consectetur adipiscing elit.
6738 */ˇ»
6739 "},
6740 rust_lang.clone(),
6741 &mut cx,
6742 );
6743
6744 // selection of multiline block comments
6745 assert_rewrap(
6746 indoc! {"
6747 «/* Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit. */ˇ»
6749 "},
6750 indoc! {"
6751 «/*
6752 * Lorem ipsum dolor sit amet,
6753 * consectetur adipiscing elit.
6754 */ˇ»
6755 "},
6756 rust_lang.clone(),
6757 &mut cx,
6758 );
6759
6760 // partial selection of multiline block comments
6761 assert_rewrap(
6762 indoc! {"
6763 «/* Lorem ipsum dolor sit amet,ˇ»
6764 * consectetur adipiscing elit. */
6765 /* Lorem ipsum dolor sit amet,
6766 «* consectetur adipiscing elit. */ˇ»
6767 "},
6768 indoc! {"
6769 «/*
6770 * Lorem ipsum dolor sit amet,ˇ»
6771 * consectetur adipiscing elit. */
6772 /* Lorem ipsum dolor sit amet,
6773 «* consectetur adipiscing elit.
6774 */ˇ»
6775 "},
6776 rust_lang.clone(),
6777 &mut cx,
6778 );
6779
6780 // selection w/ abutting short block comments
6781 // TODO: should not be combined; should rewrap as 2 comments
6782 assert_rewrap(
6783 indoc! {"
6784 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6785 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6786 "},
6787 // desired behavior:
6788 // indoc! {"
6789 // «/*
6790 // * Lorem ipsum dolor sit amet,
6791 // * consectetur adipiscing elit.
6792 // */
6793 // /*
6794 // * Lorem ipsum dolor sit amet,
6795 // * consectetur adipiscing elit.
6796 // */ˇ»
6797 // "},
6798 // actual behaviour:
6799 indoc! {"
6800 «/*
6801 * Lorem ipsum dolor sit amet,
6802 * consectetur adipiscing elit. Lorem
6803 * ipsum dolor sit amet, consectetur
6804 * adipiscing elit.
6805 */ˇ»
6806 "},
6807 rust_lang.clone(),
6808 &mut cx,
6809 );
6810
6811 // TODO: same as above, but with delimiters on separate line
6812 // assert_rewrap(
6813 // indoc! {"
6814 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6815 // */
6816 // /*
6817 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6818 // "},
6819 // // desired:
6820 // // indoc! {"
6821 // // «/*
6822 // // * Lorem ipsum dolor sit amet,
6823 // // * consectetur adipiscing elit.
6824 // // */
6825 // // /*
6826 // // * Lorem ipsum dolor sit amet,
6827 // // * consectetur adipiscing elit.
6828 // // */ˇ»
6829 // // "},
6830 // // actual: (but with trailing w/s on the empty lines)
6831 // indoc! {"
6832 // «/*
6833 // * Lorem ipsum dolor sit amet,
6834 // * consectetur adipiscing elit.
6835 // *
6836 // */
6837 // /*
6838 // *
6839 // * Lorem ipsum dolor sit amet,
6840 // * consectetur adipiscing elit.
6841 // */ˇ»
6842 // "},
6843 // rust_lang.clone(),
6844 // &mut cx,
6845 // );
6846
6847 // TODO these are unhandled edge cases; not correct, just documenting known issues
6848 assert_rewrap(
6849 indoc! {"
6850 /*
6851 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6852 */
6853 /*
6854 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6855 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6856 "},
6857 // desired:
6858 // indoc! {"
6859 // /*
6860 // *ˇ Lorem ipsum dolor sit amet,
6861 // * consectetur adipiscing elit.
6862 // */
6863 // /*
6864 // *ˇ Lorem ipsum dolor sit amet,
6865 // * consectetur adipiscing elit.
6866 // */
6867 // /*
6868 // *ˇ Lorem ipsum dolor sit amet
6869 // */ /* consectetur adipiscing elit. */
6870 // "},
6871 // actual:
6872 indoc! {"
6873 /*
6874 //ˇ Lorem ipsum dolor sit amet,
6875 // consectetur adipiscing elit.
6876 */
6877 /*
6878 * //ˇ Lorem ipsum dolor sit amet,
6879 * consectetur adipiscing elit.
6880 */
6881 /*
6882 *ˇ Lorem ipsum dolor sit amet */ /*
6883 * consectetur adipiscing elit.
6884 */
6885 "},
6886 rust_lang,
6887 &mut cx,
6888 );
6889
6890 #[track_caller]
6891 fn assert_rewrap(
6892 unwrapped_text: &str,
6893 wrapped_text: &str,
6894 language: Arc<Language>,
6895 cx: &mut EditorTestContext,
6896 ) {
6897 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6898 cx.set_state(unwrapped_text);
6899 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6900 cx.assert_editor_state(wrapped_text);
6901 }
6902}
6903
6904#[gpui::test]
6905async fn test_hard_wrap(cx: &mut TestAppContext) {
6906 init_test(cx, |_| {});
6907 let mut cx = EditorTestContext::new(cx).await;
6908
6909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6910 cx.update_editor(|editor, _, cx| {
6911 editor.set_hard_wrap(Some(14), cx);
6912 });
6913
6914 cx.set_state(indoc!(
6915 "
6916 one two three ˇ
6917 "
6918 ));
6919 cx.simulate_input("four");
6920 cx.run_until_parked();
6921
6922 cx.assert_editor_state(indoc!(
6923 "
6924 one two three
6925 fourˇ
6926 "
6927 ));
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.newline(&Default::default(), window, cx);
6931 });
6932 cx.run_until_parked();
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 four
6937 ˇ
6938 "
6939 ));
6940
6941 cx.simulate_input("five");
6942 cx.run_until_parked();
6943 cx.assert_editor_state(indoc!(
6944 "
6945 one two three
6946 four
6947 fiveˇ
6948 "
6949 ));
6950
6951 cx.update_editor(|editor, window, cx| {
6952 editor.newline(&Default::default(), window, cx);
6953 });
6954 cx.run_until_parked();
6955 cx.simulate_input("# ");
6956 cx.run_until_parked();
6957 cx.assert_editor_state(indoc!(
6958 "
6959 one two three
6960 four
6961 five
6962 # ˇ
6963 "
6964 ));
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.newline(&Default::default(), window, cx);
6968 });
6969 cx.run_until_parked();
6970 cx.assert_editor_state(indoc!(
6971 "
6972 one two three
6973 four
6974 five
6975 #\x20
6976 #ˇ
6977 "
6978 ));
6979
6980 cx.simulate_input(" 6");
6981 cx.run_until_parked();
6982 cx.assert_editor_state(indoc!(
6983 "
6984 one two three
6985 four
6986 five
6987 #
6988 # 6ˇ
6989 "
6990 ));
6991}
6992
6993#[gpui::test]
6994async fn test_cut_line_ends(cx: &mut TestAppContext) {
6995 init_test(cx, |_| {});
6996
6997 let mut cx = EditorTestContext::new(cx).await;
6998
6999 cx.set_state(indoc! {"The quick brownˇ"});
7000 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7001 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7002
7003 cx.set_state(indoc! {"The emacs foxˇ"});
7004 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7005 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7006
7007 cx.set_state(indoc! {"
7008 The quick« brownˇ»
7009 fox jumps overˇ
7010 the lazy dog"});
7011 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7012 cx.assert_editor_state(indoc! {"
7013 The quickˇ
7014 ˇthe lazy dog"});
7015
7016 cx.set_state(indoc! {"
7017 The quick« brownˇ»
7018 fox jumps overˇ
7019 the lazy dog"});
7020 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7021 cx.assert_editor_state(indoc! {"
7022 The quickˇ
7023 fox jumps overˇthe lazy dog"});
7024
7025 cx.set_state(indoc! {"
7026 The quick« brownˇ»
7027 fox jumps overˇ
7028 the lazy dog"});
7029 cx.update_editor(|e, window, cx| {
7030 e.cut_to_end_of_line(
7031 &CutToEndOfLine {
7032 stop_at_newlines: true,
7033 },
7034 window,
7035 cx,
7036 )
7037 });
7038 cx.assert_editor_state(indoc! {"
7039 The quickˇ
7040 fox jumps overˇ
7041 the lazy dog"});
7042
7043 cx.set_state(indoc! {"
7044 The quick« brownˇ»
7045 fox jumps overˇ
7046 the lazy dog"});
7047 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7048 cx.assert_editor_state(indoc! {"
7049 The quickˇ
7050 fox jumps overˇthe lazy dog"});
7051}
7052
7053#[gpui::test]
7054async fn test_clipboard(cx: &mut TestAppContext) {
7055 init_test(cx, |_| {});
7056
7057 let mut cx = EditorTestContext::new(cx).await;
7058
7059 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7060 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7061 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7062
7063 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7064 cx.set_state("two ˇfour ˇsix ˇ");
7065 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7066 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7067
7068 // Paste again but with only two cursors. Since the number of cursors doesn't
7069 // match the number of slices in the clipboard, the entire clipboard text
7070 // is pasted at each cursor.
7071 cx.set_state("ˇtwo one✅ four three six five ˇ");
7072 cx.update_editor(|e, window, cx| {
7073 e.handle_input("( ", window, cx);
7074 e.paste(&Paste, window, cx);
7075 e.handle_input(") ", window, cx);
7076 });
7077 cx.assert_editor_state(
7078 &([
7079 "( one✅ ",
7080 "three ",
7081 "five ) ˇtwo one✅ four three six five ( one✅ ",
7082 "three ",
7083 "five ) ˇ",
7084 ]
7085 .join("\n")),
7086 );
7087
7088 // Cut with three selections, one of which is full-line.
7089 cx.set_state(indoc! {"
7090 1«2ˇ»3
7091 4ˇ567
7092 «8ˇ»9"});
7093 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7094 cx.assert_editor_state(indoc! {"
7095 1ˇ3
7096 ˇ9"});
7097
7098 // Paste with three selections, noticing how the copied selection that was full-line
7099 // gets inserted before the second cursor.
7100 cx.set_state(indoc! {"
7101 1ˇ3
7102 9ˇ
7103 «oˇ»ne"});
7104 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 12ˇ3
7107 4567
7108 9ˇ
7109 8ˇne"});
7110
7111 // Copy with a single cursor only, which writes the whole line into the clipboard.
7112 cx.set_state(indoc! {"
7113 The quick brown
7114 fox juˇmps over
7115 the lazy dog"});
7116 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7117 assert_eq!(
7118 cx.read_from_clipboard()
7119 .and_then(|item| item.text().as_deref().map(str::to_string)),
7120 Some("fox jumps over\n".to_string())
7121 );
7122
7123 // Paste with three selections, noticing how the copied full-line selection is inserted
7124 // before the empty selections but replaces the selection that is non-empty.
7125 cx.set_state(indoc! {"
7126 Tˇhe quick brown
7127 «foˇ»x jumps over
7128 tˇhe lazy dog"});
7129 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7130 cx.assert_editor_state(indoc! {"
7131 fox jumps over
7132 Tˇhe quick brown
7133 fox jumps over
7134 ˇx jumps over
7135 fox jumps over
7136 tˇhe lazy dog"});
7137}
7138
7139#[gpui::test]
7140async fn test_copy_trim(cx: &mut TestAppContext) {
7141 init_test(cx, |_| {});
7142
7143 let mut cx = EditorTestContext::new(cx).await;
7144 cx.set_state(
7145 r#" «for selection in selections.iter() {
7146 let mut start = selection.start;
7147 let mut end = selection.end;
7148 let is_entire_line = selection.is_empty();
7149 if is_entire_line {
7150 start = Point::new(start.row, 0);ˇ»
7151 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7152 }
7153 "#,
7154 );
7155 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7156 assert_eq!(
7157 cx.read_from_clipboard()
7158 .and_then(|item| item.text().as_deref().map(str::to_string)),
7159 Some(
7160 "for selection in selections.iter() {
7161 let mut start = selection.start;
7162 let mut end = selection.end;
7163 let is_entire_line = selection.is_empty();
7164 if is_entire_line {
7165 start = Point::new(start.row, 0);"
7166 .to_string()
7167 ),
7168 "Regular copying preserves all indentation selected",
7169 );
7170 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7171 assert_eq!(
7172 cx.read_from_clipboard()
7173 .and_then(|item| item.text().as_deref().map(str::to_string)),
7174 Some(
7175 "for selection in selections.iter() {
7176let mut start = selection.start;
7177let mut end = selection.end;
7178let is_entire_line = selection.is_empty();
7179if is_entire_line {
7180 start = Point::new(start.row, 0);"
7181 .to_string()
7182 ),
7183 "Copying with stripping should strip all leading whitespaces"
7184 );
7185
7186 cx.set_state(
7187 r#" « for selection in selections.iter() {
7188 let mut start = selection.start;
7189 let mut end = selection.end;
7190 let is_entire_line = selection.is_empty();
7191 if is_entire_line {
7192 start = Point::new(start.row, 0);ˇ»
7193 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7194 }
7195 "#,
7196 );
7197 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7198 assert_eq!(
7199 cx.read_from_clipboard()
7200 .and_then(|item| item.text().as_deref().map(str::to_string)),
7201 Some(
7202 " for selection in selections.iter() {
7203 let mut start = selection.start;
7204 let mut end = selection.end;
7205 let is_entire_line = selection.is_empty();
7206 if is_entire_line {
7207 start = Point::new(start.row, 0);"
7208 .to_string()
7209 ),
7210 "Regular copying preserves all indentation selected",
7211 );
7212 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7213 assert_eq!(
7214 cx.read_from_clipboard()
7215 .and_then(|item| item.text().as_deref().map(str::to_string)),
7216 Some(
7217 "for selection in selections.iter() {
7218let mut start = selection.start;
7219let mut end = selection.end;
7220let is_entire_line = selection.is_empty();
7221if is_entire_line {
7222 start = Point::new(start.row, 0);"
7223 .to_string()
7224 ),
7225 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7226 );
7227
7228 cx.set_state(
7229 r#" «ˇ for selection in selections.iter() {
7230 let mut start = selection.start;
7231 let mut end = selection.end;
7232 let is_entire_line = selection.is_empty();
7233 if is_entire_line {
7234 start = Point::new(start.row, 0);»
7235 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7236 }
7237 "#,
7238 );
7239 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7240 assert_eq!(
7241 cx.read_from_clipboard()
7242 .and_then(|item| item.text().as_deref().map(str::to_string)),
7243 Some(
7244 " for selection in selections.iter() {
7245 let mut start = selection.start;
7246 let mut end = selection.end;
7247 let is_entire_line = selection.is_empty();
7248 if is_entire_line {
7249 start = Point::new(start.row, 0);"
7250 .to_string()
7251 ),
7252 "Regular copying for reverse selection works the same",
7253 );
7254 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7255 assert_eq!(
7256 cx.read_from_clipboard()
7257 .and_then(|item| item.text().as_deref().map(str::to_string)),
7258 Some(
7259 "for selection in selections.iter() {
7260let mut start = selection.start;
7261let mut end = selection.end;
7262let is_entire_line = selection.is_empty();
7263if is_entire_line {
7264 start = Point::new(start.row, 0);"
7265 .to_string()
7266 ),
7267 "Copying with stripping for reverse selection works the same"
7268 );
7269
7270 cx.set_state(
7271 r#" for selection «in selections.iter() {
7272 let mut start = selection.start;
7273 let mut end = selection.end;
7274 let is_entire_line = selection.is_empty();
7275 if is_entire_line {
7276 start = Point::new(start.row, 0);ˇ»
7277 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7278 }
7279 "#,
7280 );
7281 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7282 assert_eq!(
7283 cx.read_from_clipboard()
7284 .and_then(|item| item.text().as_deref().map(str::to_string)),
7285 Some(
7286 "in selections.iter() {
7287 let mut start = selection.start;
7288 let mut end = selection.end;
7289 let is_entire_line = selection.is_empty();
7290 if is_entire_line {
7291 start = Point::new(start.row, 0);"
7292 .to_string()
7293 ),
7294 "When selecting past the indent, the copying works as usual",
7295 );
7296 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7297 assert_eq!(
7298 cx.read_from_clipboard()
7299 .and_then(|item| item.text().as_deref().map(str::to_string)),
7300 Some(
7301 "in selections.iter() {
7302 let mut start = selection.start;
7303 let mut end = selection.end;
7304 let is_entire_line = selection.is_empty();
7305 if is_entire_line {
7306 start = Point::new(start.row, 0);"
7307 .to_string()
7308 ),
7309 "When selecting past the indent, nothing is trimmed"
7310 );
7311
7312 cx.set_state(
7313 r#" «for selection in selections.iter() {
7314 let mut start = selection.start;
7315
7316 let mut end = selection.end;
7317 let is_entire_line = selection.is_empty();
7318 if is_entire_line {
7319 start = Point::new(start.row, 0);
7320ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7321 }
7322 "#,
7323 );
7324 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7325 assert_eq!(
7326 cx.read_from_clipboard()
7327 .and_then(|item| item.text().as_deref().map(str::to_string)),
7328 Some(
7329 "for selection in selections.iter() {
7330let mut start = selection.start;
7331
7332let mut end = selection.end;
7333let is_entire_line = selection.is_empty();
7334if is_entire_line {
7335 start = Point::new(start.row, 0);
7336"
7337 .to_string()
7338 ),
7339 "Copying with stripping should ignore empty lines"
7340 );
7341}
7342
7343#[gpui::test]
7344async fn test_paste_multiline(cx: &mut TestAppContext) {
7345 init_test(cx, |_| {});
7346
7347 let mut cx = EditorTestContext::new(cx).await;
7348 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7349
7350 // Cut an indented block, without the leading whitespace.
7351 cx.set_state(indoc! {"
7352 const a: B = (
7353 c(),
7354 «d(
7355 e,
7356 f
7357 )ˇ»
7358 );
7359 "});
7360 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7361 cx.assert_editor_state(indoc! {"
7362 const a: B = (
7363 c(),
7364 ˇ
7365 );
7366 "});
7367
7368 // Paste it at the same position.
7369 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7370 cx.assert_editor_state(indoc! {"
7371 const a: B = (
7372 c(),
7373 d(
7374 e,
7375 f
7376 )ˇ
7377 );
7378 "});
7379
7380 // Paste it at a line with a lower indent level.
7381 cx.set_state(indoc! {"
7382 ˇ
7383 const a: B = (
7384 c(),
7385 );
7386 "});
7387 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7388 cx.assert_editor_state(indoc! {"
7389 d(
7390 e,
7391 f
7392 )ˇ
7393 const a: B = (
7394 c(),
7395 );
7396 "});
7397
7398 // Cut an indented block, with the leading whitespace.
7399 cx.set_state(indoc! {"
7400 const a: B = (
7401 c(),
7402 « d(
7403 e,
7404 f
7405 )
7406 ˇ»);
7407 "});
7408 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7409 cx.assert_editor_state(indoc! {"
7410 const a: B = (
7411 c(),
7412 ˇ);
7413 "});
7414
7415 // Paste it at the same position.
7416 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7417 cx.assert_editor_state(indoc! {"
7418 const a: B = (
7419 c(),
7420 d(
7421 e,
7422 f
7423 )
7424 ˇ);
7425 "});
7426
7427 // Paste it at a line with a higher indent level.
7428 cx.set_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 fˇ
7434 )
7435 );
7436 "});
7437 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7438 cx.assert_editor_state(indoc! {"
7439 const a: B = (
7440 c(),
7441 d(
7442 e,
7443 f d(
7444 e,
7445 f
7446 )
7447 ˇ
7448 )
7449 );
7450 "});
7451
7452 // Copy an indented block, starting mid-line
7453 cx.set_state(indoc! {"
7454 const a: B = (
7455 c(),
7456 somethin«g(
7457 e,
7458 f
7459 )ˇ»
7460 );
7461 "});
7462 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7463
7464 // Paste it on a line with a lower indent level
7465 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7466 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7467 cx.assert_editor_state(indoc! {"
7468 const a: B = (
7469 c(),
7470 something(
7471 e,
7472 f
7473 )
7474 );
7475 g(
7476 e,
7477 f
7478 )ˇ"});
7479}
7480
7481#[gpui::test]
7482async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7483 init_test(cx, |_| {});
7484
7485 cx.write_to_clipboard(ClipboardItem::new_string(
7486 " d(\n e\n );\n".into(),
7487 ));
7488
7489 let mut cx = EditorTestContext::new(cx).await;
7490 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7491
7492 cx.set_state(indoc! {"
7493 fn a() {
7494 b();
7495 if c() {
7496 ˇ
7497 }
7498 }
7499 "});
7500
7501 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7502 cx.assert_editor_state(indoc! {"
7503 fn a() {
7504 b();
7505 if c() {
7506 d(
7507 e
7508 );
7509 ˇ
7510 }
7511 }
7512 "});
7513
7514 cx.set_state(indoc! {"
7515 fn a() {
7516 b();
7517 ˇ
7518 }
7519 "});
7520
7521 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7522 cx.assert_editor_state(indoc! {"
7523 fn a() {
7524 b();
7525 d(
7526 e
7527 );
7528 ˇ
7529 }
7530 "});
7531}
7532
7533#[gpui::test]
7534fn test_select_all(cx: &mut TestAppContext) {
7535 init_test(cx, |_| {});
7536
7537 let editor = cx.add_window(|window, cx| {
7538 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7539 build_editor(buffer, window, cx)
7540 });
7541 _ = editor.update(cx, |editor, window, cx| {
7542 editor.select_all(&SelectAll, window, cx);
7543 assert_eq!(
7544 editor.selections.display_ranges(cx),
7545 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7546 );
7547 });
7548}
7549
7550#[gpui::test]
7551fn test_select_line(cx: &mut TestAppContext) {
7552 init_test(cx, |_| {});
7553
7554 let editor = cx.add_window(|window, cx| {
7555 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7556 build_editor(buffer, window, cx)
7557 });
7558 _ = editor.update(cx, |editor, window, cx| {
7559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7560 s.select_display_ranges([
7561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7564 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7565 ])
7566 });
7567 editor.select_line(&SelectLine, window, cx);
7568 assert_eq!(
7569 editor.selections.display_ranges(cx),
7570 vec![
7571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7572 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7573 ]
7574 );
7575 });
7576
7577 _ = editor.update(cx, |editor, window, cx| {
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 editor.selections.display_ranges(cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 editor.selections.display_ranges(cx),
7592 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7593 );
7594 });
7595}
7596
7597#[gpui::test]
7598async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7599 init_test(cx, |_| {});
7600 let mut cx = EditorTestContext::new(cx).await;
7601
7602 #[track_caller]
7603 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7604 cx.set_state(initial_state);
7605 cx.update_editor(|e, window, cx| {
7606 e.split_selection_into_lines(&Default::default(), window, cx)
7607 });
7608 cx.assert_editor_state(expected_state);
7609 }
7610
7611 // Selection starts and ends at the middle of lines, left-to-right
7612 test(
7613 &mut cx,
7614 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7615 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7616 );
7617 // Same thing, right-to-left
7618 test(
7619 &mut cx,
7620 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7621 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7622 );
7623
7624 // Whole buffer, left-to-right, last line *doesn't* end with newline
7625 test(
7626 &mut cx,
7627 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7628 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7629 );
7630 // Same thing, right-to-left
7631 test(
7632 &mut cx,
7633 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7634 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7635 );
7636
7637 // Whole buffer, left-to-right, last line ends with newline
7638 test(
7639 &mut cx,
7640 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7641 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7642 );
7643 // Same thing, right-to-left
7644 test(
7645 &mut cx,
7646 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7647 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7648 );
7649
7650 // Starts at the end of a line, ends at the start of another
7651 test(
7652 &mut cx,
7653 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7654 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7655 );
7656}
7657
7658#[gpui::test]
7659async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7660 init_test(cx, |_| {});
7661
7662 let editor = cx.add_window(|window, cx| {
7663 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7664 build_editor(buffer, window, cx)
7665 });
7666
7667 // setup
7668 _ = editor.update(cx, |editor, window, cx| {
7669 editor.fold_creases(
7670 vec![
7671 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7672 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7673 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7674 ],
7675 true,
7676 window,
7677 cx,
7678 );
7679 assert_eq!(
7680 editor.display_text(cx),
7681 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7682 );
7683 });
7684
7685 _ = editor.update(cx, |editor, window, cx| {
7686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7687 s.select_display_ranges([
7688 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7689 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7691 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7692 ])
7693 });
7694 editor.split_selection_into_lines(&Default::default(), window, cx);
7695 assert_eq!(
7696 editor.display_text(cx),
7697 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7698 );
7699 });
7700 EditorTestContext::for_editor(editor, cx)
7701 .await
7702 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7703
7704 _ = editor.update(cx, |editor, window, cx| {
7705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7706 s.select_display_ranges([
7707 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7708 ])
7709 });
7710 editor.split_selection_into_lines(&Default::default(), window, cx);
7711 assert_eq!(
7712 editor.display_text(cx),
7713 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7714 );
7715 assert_eq!(
7716 editor.selections.display_ranges(cx),
7717 [
7718 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7719 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7720 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7721 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7722 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7723 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7724 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7725 ]
7726 );
7727 });
7728 EditorTestContext::for_editor(editor, cx)
7729 .await
7730 .assert_editor_state(
7731 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7732 );
7733}
7734
7735#[gpui::test]
7736async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7737 init_test(cx, |_| {});
7738
7739 let mut cx = EditorTestContext::new(cx).await;
7740
7741 cx.set_state(indoc!(
7742 r#"abc
7743 defˇghi
7744
7745 jk
7746 nlmo
7747 "#
7748 ));
7749
7750 cx.update_editor(|editor, window, cx| {
7751 editor.add_selection_above(&Default::default(), window, cx);
7752 });
7753
7754 cx.assert_editor_state(indoc!(
7755 r#"abcˇ
7756 defˇghi
7757
7758 jk
7759 nlmo
7760 "#
7761 ));
7762
7763 cx.update_editor(|editor, window, cx| {
7764 editor.add_selection_above(&Default::default(), window, cx);
7765 });
7766
7767 cx.assert_editor_state(indoc!(
7768 r#"abcˇ
7769 defˇghi
7770
7771 jk
7772 nlmo
7773 "#
7774 ));
7775
7776 cx.update_editor(|editor, window, cx| {
7777 editor.add_selection_below(&Default::default(), window, cx);
7778 });
7779
7780 cx.assert_editor_state(indoc!(
7781 r#"abc
7782 defˇghi
7783
7784 jk
7785 nlmo
7786 "#
7787 ));
7788
7789 cx.update_editor(|editor, window, cx| {
7790 editor.undo_selection(&Default::default(), window, cx);
7791 });
7792
7793 cx.assert_editor_state(indoc!(
7794 r#"abcˇ
7795 defˇghi
7796
7797 jk
7798 nlmo
7799 "#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.redo_selection(&Default::default(), window, cx);
7804 });
7805
7806 cx.assert_editor_state(indoc!(
7807 r#"abc
7808 defˇghi
7809
7810 jk
7811 nlmo
7812 "#
7813 ));
7814
7815 cx.update_editor(|editor, window, cx| {
7816 editor.add_selection_below(&Default::default(), window, cx);
7817 });
7818
7819 cx.assert_editor_state(indoc!(
7820 r#"abc
7821 defˇghi
7822 ˇ
7823 jk
7824 nlmo
7825 "#
7826 ));
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.add_selection_below(&Default::default(), window, cx);
7830 });
7831
7832 cx.assert_editor_state(indoc!(
7833 r#"abc
7834 defˇghi
7835 ˇ
7836 jkˇ
7837 nlmo
7838 "#
7839 ));
7840
7841 cx.update_editor(|editor, window, cx| {
7842 editor.add_selection_below(&Default::default(), window, cx);
7843 });
7844
7845 cx.assert_editor_state(indoc!(
7846 r#"abc
7847 defˇghi
7848 ˇ
7849 jkˇ
7850 nlmˇo
7851 "#
7852 ));
7853
7854 cx.update_editor(|editor, window, cx| {
7855 editor.add_selection_below(&Default::default(), window, cx);
7856 });
7857
7858 cx.assert_editor_state(indoc!(
7859 r#"abc
7860 defˇghi
7861 ˇ
7862 jkˇ
7863 nlmˇo
7864 ˇ"#
7865 ));
7866
7867 // change selections
7868 cx.set_state(indoc!(
7869 r#"abc
7870 def«ˇg»hi
7871
7872 jk
7873 nlmo
7874 "#
7875 ));
7876
7877 cx.update_editor(|editor, window, cx| {
7878 editor.add_selection_below(&Default::default(), window, cx);
7879 });
7880
7881 cx.assert_editor_state(indoc!(
7882 r#"abc
7883 def«ˇg»hi
7884
7885 jk
7886 nlm«ˇo»
7887 "#
7888 ));
7889
7890 cx.update_editor(|editor, window, cx| {
7891 editor.add_selection_below(&Default::default(), window, cx);
7892 });
7893
7894 cx.assert_editor_state(indoc!(
7895 r#"abc
7896 def«ˇg»hi
7897
7898 jk
7899 nlm«ˇo»
7900 "#
7901 ));
7902
7903 cx.update_editor(|editor, window, cx| {
7904 editor.add_selection_above(&Default::default(), window, cx);
7905 });
7906
7907 cx.assert_editor_state(indoc!(
7908 r#"abc
7909 def«ˇg»hi
7910
7911 jk
7912 nlmo
7913 "#
7914 ));
7915
7916 cx.update_editor(|editor, window, cx| {
7917 editor.add_selection_above(&Default::default(), window, cx);
7918 });
7919
7920 cx.assert_editor_state(indoc!(
7921 r#"abc
7922 def«ˇg»hi
7923
7924 jk
7925 nlmo
7926 "#
7927 ));
7928
7929 // Change selections again
7930 cx.set_state(indoc!(
7931 r#"a«bc
7932 defgˇ»hi
7933
7934 jk
7935 nlmo
7936 "#
7937 ));
7938
7939 cx.update_editor(|editor, window, cx| {
7940 editor.add_selection_below(&Default::default(), window, cx);
7941 });
7942
7943 cx.assert_editor_state(indoc!(
7944 r#"a«bcˇ»
7945 d«efgˇ»hi
7946
7947 j«kˇ»
7948 nlmo
7949 "#
7950 ));
7951
7952 cx.update_editor(|editor, window, cx| {
7953 editor.add_selection_below(&Default::default(), window, cx);
7954 });
7955 cx.assert_editor_state(indoc!(
7956 r#"a«bcˇ»
7957 d«efgˇ»hi
7958
7959 j«kˇ»
7960 n«lmoˇ»
7961 "#
7962 ));
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_above(&Default::default(), window, cx);
7965 });
7966
7967 cx.assert_editor_state(indoc!(
7968 r#"a«bcˇ»
7969 d«efgˇ»hi
7970
7971 j«kˇ»
7972 nlmo
7973 "#
7974 ));
7975
7976 // Change selections again
7977 cx.set_state(indoc!(
7978 r#"abc
7979 d«ˇefghi
7980
7981 jk
7982 nlm»o
7983 "#
7984 ));
7985
7986 cx.update_editor(|editor, window, cx| {
7987 editor.add_selection_above(&Default::default(), window, cx);
7988 });
7989
7990 cx.assert_editor_state(indoc!(
7991 r#"a«ˇbc»
7992 d«ˇef»ghi
7993
7994 j«ˇk»
7995 n«ˇlm»o
7996 "#
7997 ));
7998
7999 cx.update_editor(|editor, window, cx| {
8000 editor.add_selection_below(&Default::default(), window, cx);
8001 });
8002
8003 cx.assert_editor_state(indoc!(
8004 r#"abc
8005 d«ˇef»ghi
8006
8007 j«ˇk»
8008 n«ˇlm»o
8009 "#
8010 ));
8011}
8012
8013#[gpui::test]
8014async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8015 init_test(cx, |_| {});
8016 let mut cx = EditorTestContext::new(cx).await;
8017
8018 cx.set_state(indoc!(
8019 r#"line onˇe
8020 liˇne two
8021 line three
8022 line four"#
8023 ));
8024
8025 cx.update_editor(|editor, window, cx| {
8026 editor.add_selection_below(&Default::default(), window, cx);
8027 });
8028
8029 // test multiple cursors expand in the same direction
8030 cx.assert_editor_state(indoc!(
8031 r#"line onˇe
8032 liˇne twˇo
8033 liˇne three
8034 line four"#
8035 ));
8036
8037 cx.update_editor(|editor, window, cx| {
8038 editor.add_selection_below(&Default::default(), window, cx);
8039 });
8040
8041 cx.update_editor(|editor, window, cx| {
8042 editor.add_selection_below(&Default::default(), window, cx);
8043 });
8044
8045 // test multiple cursors expand below overflow
8046 cx.assert_editor_state(indoc!(
8047 r#"line onˇe
8048 liˇne twˇo
8049 liˇne thˇree
8050 liˇne foˇur"#
8051 ));
8052
8053 cx.update_editor(|editor, window, cx| {
8054 editor.add_selection_above(&Default::default(), window, cx);
8055 });
8056
8057 // test multiple cursors retrieves back correctly
8058 cx.assert_editor_state(indoc!(
8059 r#"line onˇe
8060 liˇne twˇo
8061 liˇne thˇree
8062 line four"#
8063 ));
8064
8065 cx.update_editor(|editor, window, cx| {
8066 editor.add_selection_above(&Default::default(), window, cx);
8067 });
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.add_selection_above(&Default::default(), window, cx);
8071 });
8072
8073 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8074 cx.assert_editor_state(indoc!(
8075 r#"liˇne onˇe
8076 liˇne two
8077 line three
8078 line four"#
8079 ));
8080
8081 cx.update_editor(|editor, window, cx| {
8082 editor.undo_selection(&Default::default(), window, cx);
8083 });
8084
8085 // test undo
8086 cx.assert_editor_state(indoc!(
8087 r#"line onˇe
8088 liˇne twˇo
8089 line three
8090 line four"#
8091 ));
8092
8093 cx.update_editor(|editor, window, cx| {
8094 editor.redo_selection(&Default::default(), window, cx);
8095 });
8096
8097 // test redo
8098 cx.assert_editor_state(indoc!(
8099 r#"liˇne onˇe
8100 liˇne two
8101 line three
8102 line four"#
8103 ));
8104
8105 cx.set_state(indoc!(
8106 r#"abcd
8107 ef«ghˇ»
8108 ijkl
8109 «mˇ»nop"#
8110 ));
8111
8112 cx.update_editor(|editor, window, cx| {
8113 editor.add_selection_above(&Default::default(), window, cx);
8114 });
8115
8116 // test multiple selections expand in the same direction
8117 cx.assert_editor_state(indoc!(
8118 r#"ab«cdˇ»
8119 ef«ghˇ»
8120 «iˇ»jkl
8121 «mˇ»nop"#
8122 ));
8123
8124 cx.update_editor(|editor, window, cx| {
8125 editor.add_selection_above(&Default::default(), window, cx);
8126 });
8127
8128 // test multiple selection upward overflow
8129 cx.assert_editor_state(indoc!(
8130 r#"ab«cdˇ»
8131 «eˇ»f«ghˇ»
8132 «iˇ»jkl
8133 «mˇ»nop"#
8134 ));
8135
8136 cx.update_editor(|editor, window, cx| {
8137 editor.add_selection_below(&Default::default(), window, cx);
8138 });
8139
8140 // test multiple selection retrieves back correctly
8141 cx.assert_editor_state(indoc!(
8142 r#"abcd
8143 ef«ghˇ»
8144 «iˇ»jkl
8145 «mˇ»nop"#
8146 ));
8147
8148 cx.update_editor(|editor, window, cx| {
8149 editor.add_selection_below(&Default::default(), window, cx);
8150 });
8151
8152 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8153 cx.assert_editor_state(indoc!(
8154 r#"abcd
8155 ef«ghˇ»
8156 ij«klˇ»
8157 «mˇ»nop"#
8158 ));
8159
8160 cx.update_editor(|editor, window, cx| {
8161 editor.undo_selection(&Default::default(), window, cx);
8162 });
8163
8164 // test undo
8165 cx.assert_editor_state(indoc!(
8166 r#"abcd
8167 ef«ghˇ»
8168 «iˇ»jkl
8169 «mˇ»nop"#
8170 ));
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.redo_selection(&Default::default(), window, cx);
8174 });
8175
8176 // test redo
8177 cx.assert_editor_state(indoc!(
8178 r#"abcd
8179 ef«ghˇ»
8180 ij«klˇ»
8181 «mˇ»nop"#
8182 ));
8183}
8184
8185#[gpui::test]
8186async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8187 init_test(cx, |_| {});
8188 let mut cx = EditorTestContext::new(cx).await;
8189
8190 cx.set_state(indoc!(
8191 r#"line onˇe
8192 liˇne two
8193 line three
8194 line four"#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 editor.add_selection_below(&Default::default(), window, cx);
8200 editor.add_selection_below(&Default::default(), window, cx);
8201 });
8202
8203 // initial state with two multi cursor groups
8204 cx.assert_editor_state(indoc!(
8205 r#"line onˇe
8206 liˇne twˇo
8207 liˇne thˇree
8208 liˇne foˇur"#
8209 ));
8210
8211 // add single cursor in middle - simulate opt click
8212 cx.update_editor(|editor, window, cx| {
8213 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8214 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8215 editor.end_selection(window, cx);
8216 });
8217
8218 cx.assert_editor_state(indoc!(
8219 r#"line onˇe
8220 liˇne twˇo
8221 liˇneˇ thˇree
8222 liˇne foˇur"#
8223 ));
8224
8225 cx.update_editor(|editor, window, cx| {
8226 editor.add_selection_above(&Default::default(), window, cx);
8227 });
8228
8229 // test new added selection expands above and existing selection shrinks
8230 cx.assert_editor_state(indoc!(
8231 r#"line onˇe
8232 liˇneˇ twˇo
8233 liˇneˇ thˇree
8234 line four"#
8235 ));
8236
8237 cx.update_editor(|editor, window, cx| {
8238 editor.add_selection_above(&Default::default(), window, cx);
8239 });
8240
8241 // test new added selection expands above and existing selection shrinks
8242 cx.assert_editor_state(indoc!(
8243 r#"lineˇ onˇe
8244 liˇneˇ twˇo
8245 lineˇ three
8246 line four"#
8247 ));
8248
8249 // intial state with two selection groups
8250 cx.set_state(indoc!(
8251 r#"abcd
8252 ef«ghˇ»
8253 ijkl
8254 «mˇ»nop"#
8255 ));
8256
8257 cx.update_editor(|editor, window, cx| {
8258 editor.add_selection_above(&Default::default(), window, cx);
8259 editor.add_selection_above(&Default::default(), window, cx);
8260 });
8261
8262 cx.assert_editor_state(indoc!(
8263 r#"ab«cdˇ»
8264 «eˇ»f«ghˇ»
8265 «iˇ»jkl
8266 «mˇ»nop"#
8267 ));
8268
8269 // add single selection in middle - simulate opt drag
8270 cx.update_editor(|editor, window, cx| {
8271 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8272 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8273 editor.update_selection(
8274 DisplayPoint::new(DisplayRow(2), 4),
8275 0,
8276 gpui::Point::<f32>::default(),
8277 window,
8278 cx,
8279 );
8280 editor.end_selection(window, cx);
8281 });
8282
8283 cx.assert_editor_state(indoc!(
8284 r#"ab«cdˇ»
8285 «eˇ»f«ghˇ»
8286 «iˇ»jk«lˇ»
8287 «mˇ»nop"#
8288 ));
8289
8290 cx.update_editor(|editor, window, cx| {
8291 editor.add_selection_below(&Default::default(), window, cx);
8292 });
8293
8294 // test new added selection expands below, others shrinks from above
8295 cx.assert_editor_state(indoc!(
8296 r#"abcd
8297 ef«ghˇ»
8298 «iˇ»jk«lˇ»
8299 «mˇ»no«pˇ»"#
8300 ));
8301}
8302
8303#[gpui::test]
8304async fn test_select_next(cx: &mut TestAppContext) {
8305 init_test(cx, |_| {});
8306
8307 let mut cx = EditorTestContext::new(cx).await;
8308 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8309
8310 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8311 .unwrap();
8312 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8313
8314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8315 .unwrap();
8316 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8317
8318 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8319 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8320
8321 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8322 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8323
8324 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8325 .unwrap();
8326 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8327
8328 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8329 .unwrap();
8330 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8331
8332 // Test selection direction should be preserved
8333 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8334
8335 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8336 .unwrap();
8337 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8338}
8339
8340#[gpui::test]
8341async fn test_select_all_matches(cx: &mut TestAppContext) {
8342 init_test(cx, |_| {});
8343
8344 let mut cx = EditorTestContext::new(cx).await;
8345
8346 // Test caret-only selections
8347 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8348 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8349 .unwrap();
8350 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8351
8352 // Test left-to-right selections
8353 cx.set_state("abc\n«abcˇ»\nabc");
8354 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8355 .unwrap();
8356 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8357
8358 // Test right-to-left selections
8359 cx.set_state("abc\n«ˇabc»\nabc");
8360 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8361 .unwrap();
8362 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8363
8364 // Test selecting whitespace with caret selection
8365 cx.set_state("abc\nˇ abc\nabc");
8366 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8367 .unwrap();
8368 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8369
8370 // Test selecting whitespace with left-to-right selection
8371 cx.set_state("abc\n«ˇ »abc\nabc");
8372 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8373 .unwrap();
8374 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8375
8376 // Test no matches with right-to-left selection
8377 cx.set_state("abc\n« ˇ»abc\nabc");
8378 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8379 .unwrap();
8380 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8381
8382 // Test with a single word and clip_at_line_ends=true (#29823)
8383 cx.set_state("aˇbc");
8384 cx.update_editor(|e, window, cx| {
8385 e.set_clip_at_line_ends(true, cx);
8386 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8387 e.set_clip_at_line_ends(false, cx);
8388 });
8389 cx.assert_editor_state("«abcˇ»");
8390}
8391
8392#[gpui::test]
8393async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let mut cx = EditorTestContext::new(cx).await;
8397
8398 let large_body_1 = "\nd".repeat(200);
8399 let large_body_2 = "\ne".repeat(200);
8400
8401 cx.set_state(&format!(
8402 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8403 ));
8404 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8405 let scroll_position = editor.scroll_position(cx);
8406 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8407 scroll_position
8408 });
8409
8410 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8411 .unwrap();
8412 cx.assert_editor_state(&format!(
8413 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8414 ));
8415 let scroll_position_after_selection =
8416 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8417 assert_eq!(
8418 initial_scroll_position, scroll_position_after_selection,
8419 "Scroll position should not change after selecting all matches"
8420 );
8421}
8422
8423#[gpui::test]
8424async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8425 init_test(cx, |_| {});
8426
8427 let mut cx = EditorLspTestContext::new_rust(
8428 lsp::ServerCapabilities {
8429 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8430 ..Default::default()
8431 },
8432 cx,
8433 )
8434 .await;
8435
8436 cx.set_state(indoc! {"
8437 line 1
8438 line 2
8439 linˇe 3
8440 line 4
8441 line 5
8442 "});
8443
8444 // Make an edit
8445 cx.update_editor(|editor, window, cx| {
8446 editor.handle_input("X", window, cx);
8447 });
8448
8449 // Move cursor to a different position
8450 cx.update_editor(|editor, window, cx| {
8451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8452 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8453 });
8454 });
8455
8456 cx.assert_editor_state(indoc! {"
8457 line 1
8458 line 2
8459 linXe 3
8460 line 4
8461 liˇne 5
8462 "});
8463
8464 cx.lsp
8465 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8466 Ok(Some(vec![lsp::TextEdit::new(
8467 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8468 "PREFIX ".to_string(),
8469 )]))
8470 });
8471
8472 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8473 .unwrap()
8474 .await
8475 .unwrap();
8476
8477 cx.assert_editor_state(indoc! {"
8478 PREFIX line 1
8479 line 2
8480 linXe 3
8481 line 4
8482 liˇne 5
8483 "});
8484
8485 // Undo formatting
8486 cx.update_editor(|editor, window, cx| {
8487 editor.undo(&Default::default(), window, cx);
8488 });
8489
8490 // Verify cursor moved back to position after edit
8491 cx.assert_editor_state(indoc! {"
8492 line 1
8493 line 2
8494 linXˇe 3
8495 line 4
8496 line 5
8497 "});
8498}
8499
8500#[gpui::test]
8501async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8502 init_test(cx, |_| {});
8503
8504 let mut cx = EditorTestContext::new(cx).await;
8505
8506 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8507 cx.update_editor(|editor, window, cx| {
8508 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8509 });
8510
8511 cx.set_state(indoc! {"
8512 line 1
8513 line 2
8514 linˇe 3
8515 line 4
8516 line 5
8517 line 6
8518 line 7
8519 line 8
8520 line 9
8521 line 10
8522 "});
8523
8524 let snapshot = cx.buffer_snapshot();
8525 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8526
8527 cx.update(|_, cx| {
8528 provider.update(cx, |provider, _| {
8529 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8530 id: None,
8531 edits: vec![(edit_position..edit_position, "X".into())],
8532 edit_preview: None,
8533 }))
8534 })
8535 });
8536
8537 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8538 cx.update_editor(|editor, window, cx| {
8539 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8540 });
8541
8542 cx.assert_editor_state(indoc! {"
8543 line 1
8544 line 2
8545 lineXˇ 3
8546 line 4
8547 line 5
8548 line 6
8549 line 7
8550 line 8
8551 line 9
8552 line 10
8553 "});
8554
8555 cx.update_editor(|editor, window, cx| {
8556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8557 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8558 });
8559 });
8560
8561 cx.assert_editor_state(indoc! {"
8562 line 1
8563 line 2
8564 lineX 3
8565 line 4
8566 line 5
8567 line 6
8568 line 7
8569 line 8
8570 line 9
8571 liˇne 10
8572 "});
8573
8574 cx.update_editor(|editor, window, cx| {
8575 editor.undo(&Default::default(), window, cx);
8576 });
8577
8578 cx.assert_editor_state(indoc! {"
8579 line 1
8580 line 2
8581 lineˇ 3
8582 line 4
8583 line 5
8584 line 6
8585 line 7
8586 line 8
8587 line 9
8588 line 10
8589 "});
8590}
8591
8592#[gpui::test]
8593async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8594 init_test(cx, |_| {});
8595
8596 let mut cx = EditorTestContext::new(cx).await;
8597 cx.set_state(
8598 r#"let foo = 2;
8599lˇet foo = 2;
8600let fooˇ = 2;
8601let foo = 2;
8602let foo = ˇ2;"#,
8603 );
8604
8605 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8606 .unwrap();
8607 cx.assert_editor_state(
8608 r#"let foo = 2;
8609«letˇ» foo = 2;
8610let «fooˇ» = 2;
8611let foo = 2;
8612let foo = «2ˇ»;"#,
8613 );
8614
8615 // noop for multiple selections with different contents
8616 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8617 .unwrap();
8618 cx.assert_editor_state(
8619 r#"let foo = 2;
8620«letˇ» foo = 2;
8621let «fooˇ» = 2;
8622let foo = 2;
8623let foo = «2ˇ»;"#,
8624 );
8625
8626 // Test last selection direction should be preserved
8627 cx.set_state(
8628 r#"let foo = 2;
8629let foo = 2;
8630let «fooˇ» = 2;
8631let «ˇfoo» = 2;
8632let foo = 2;"#,
8633 );
8634
8635 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8636 .unwrap();
8637 cx.assert_editor_state(
8638 r#"let foo = 2;
8639let foo = 2;
8640let «fooˇ» = 2;
8641let «ˇfoo» = 2;
8642let «ˇfoo» = 2;"#,
8643 );
8644}
8645
8646#[gpui::test]
8647async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8648 init_test(cx, |_| {});
8649
8650 let mut cx =
8651 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8652
8653 cx.assert_editor_state(indoc! {"
8654 ˇbbb
8655 ccc
8656
8657 bbb
8658 ccc
8659 "});
8660 cx.dispatch_action(SelectPrevious::default());
8661 cx.assert_editor_state(indoc! {"
8662 «bbbˇ»
8663 ccc
8664
8665 bbb
8666 ccc
8667 "});
8668 cx.dispatch_action(SelectPrevious::default());
8669 cx.assert_editor_state(indoc! {"
8670 «bbbˇ»
8671 ccc
8672
8673 «bbbˇ»
8674 ccc
8675 "});
8676}
8677
8678#[gpui::test]
8679async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8680 init_test(cx, |_| {});
8681
8682 let mut cx = EditorTestContext::new(cx).await;
8683 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8684
8685 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8686 .unwrap();
8687 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8688
8689 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8690 .unwrap();
8691 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8692
8693 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8694 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8695
8696 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8697 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8698
8699 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8700 .unwrap();
8701 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8702
8703 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8704 .unwrap();
8705 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8706}
8707
8708#[gpui::test]
8709async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8710 init_test(cx, |_| {});
8711
8712 let mut cx = EditorTestContext::new(cx).await;
8713 cx.set_state("aˇ");
8714
8715 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8716 .unwrap();
8717 cx.assert_editor_state("«aˇ»");
8718 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8719 .unwrap();
8720 cx.assert_editor_state("«aˇ»");
8721}
8722
8723#[gpui::test]
8724async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728 cx.set_state(
8729 r#"let foo = 2;
8730lˇet foo = 2;
8731let fooˇ = 2;
8732let foo = 2;
8733let foo = ˇ2;"#,
8734 );
8735
8736 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8737 .unwrap();
8738 cx.assert_editor_state(
8739 r#"let foo = 2;
8740«letˇ» foo = 2;
8741let «fooˇ» = 2;
8742let foo = 2;
8743let foo = «2ˇ»;"#,
8744 );
8745
8746 // noop for multiple selections with different contents
8747 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756}
8757
8758#[gpui::test]
8759async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8760 init_test(cx, |_| {});
8761
8762 let mut cx = EditorTestContext::new(cx).await;
8763 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8764
8765 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8766 .unwrap();
8767 // selection direction is preserved
8768 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8769
8770 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8771 .unwrap();
8772 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8773
8774 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8775 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8776
8777 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8778 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8779
8780 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8783
8784 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8785 .unwrap();
8786 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8787}
8788
8789#[gpui::test]
8790async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8791 init_test(cx, |_| {});
8792
8793 let language = Arc::new(Language::new(
8794 LanguageConfig::default(),
8795 Some(tree_sitter_rust::LANGUAGE.into()),
8796 ));
8797
8798 let text = r#"
8799 use mod1::mod2::{mod3, mod4};
8800
8801 fn fn_1(param1: bool, param2: &str) {
8802 let var1 = "text";
8803 }
8804 "#
8805 .unindent();
8806
8807 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8809 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8810
8811 editor
8812 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8813 .await;
8814
8815 editor.update_in(cx, |editor, window, cx| {
8816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8817 s.select_display_ranges([
8818 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8819 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8820 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8821 ]);
8822 });
8823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8824 });
8825 editor.update(cx, |editor, cx| {
8826 assert_text_with_selections(
8827 editor,
8828 indoc! {r#"
8829 use mod1::mod2::{mod3, «mod4ˇ»};
8830
8831 fn fn_1«ˇ(param1: bool, param2: &str)» {
8832 let var1 = "«ˇtext»";
8833 }
8834 "#},
8835 cx,
8836 );
8837 });
8838
8839 editor.update_in(cx, |editor, window, cx| {
8840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8841 });
8842 editor.update(cx, |editor, cx| {
8843 assert_text_with_selections(
8844 editor,
8845 indoc! {r#"
8846 use mod1::mod2::«{mod3, mod4}ˇ»;
8847
8848 «ˇfn fn_1(param1: bool, param2: &str) {
8849 let var1 = "text";
8850 }»
8851 "#},
8852 cx,
8853 );
8854 });
8855
8856 editor.update_in(cx, |editor, window, cx| {
8857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8858 });
8859 assert_eq!(
8860 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8861 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8862 );
8863
8864 // Trying to expand the selected syntax node one more time has no effect.
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 assert_eq!(
8869 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8870 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8871 );
8872
8873 editor.update_in(cx, |editor, window, cx| {
8874 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8875 });
8876 editor.update(cx, |editor, cx| {
8877 assert_text_with_selections(
8878 editor,
8879 indoc! {r#"
8880 use mod1::mod2::«{mod3, mod4}ˇ»;
8881
8882 «ˇfn fn_1(param1: bool, param2: &str) {
8883 let var1 = "text";
8884 }»
8885 "#},
8886 cx,
8887 );
8888 });
8889
8890 editor.update_in(cx, |editor, window, cx| {
8891 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8892 });
8893 editor.update(cx, |editor, cx| {
8894 assert_text_with_selections(
8895 editor,
8896 indoc! {r#"
8897 use mod1::mod2::{mod3, «mod4ˇ»};
8898
8899 fn fn_1«ˇ(param1: bool, param2: &str)» {
8900 let var1 = "«ˇtext»";
8901 }
8902 "#},
8903 cx,
8904 );
8905 });
8906
8907 editor.update_in(cx, |editor, window, cx| {
8908 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8909 });
8910 editor.update(cx, |editor, cx| {
8911 assert_text_with_selections(
8912 editor,
8913 indoc! {r#"
8914 use mod1::mod2::{mod3, moˇd4};
8915
8916 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8917 let var1 = "teˇxt";
8918 }
8919 "#},
8920 cx,
8921 );
8922 });
8923
8924 // Trying to shrink the selected syntax node one more time has no effect.
8925 editor.update_in(cx, |editor, window, cx| {
8926 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8927 });
8928 editor.update_in(cx, |editor, _, cx| {
8929 assert_text_with_selections(
8930 editor,
8931 indoc! {r#"
8932 use mod1::mod2::{mod3, moˇd4};
8933
8934 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8935 let var1 = "teˇxt";
8936 }
8937 "#},
8938 cx,
8939 );
8940 });
8941
8942 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8943 // a fold.
8944 editor.update_in(cx, |editor, window, cx| {
8945 editor.fold_creases(
8946 vec![
8947 Crease::simple(
8948 Point::new(0, 21)..Point::new(0, 24),
8949 FoldPlaceholder::test(),
8950 ),
8951 Crease::simple(
8952 Point::new(3, 20)..Point::new(3, 22),
8953 FoldPlaceholder::test(),
8954 ),
8955 ],
8956 true,
8957 window,
8958 cx,
8959 );
8960 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8961 });
8962 editor.update(cx, |editor, cx| {
8963 assert_text_with_selections(
8964 editor,
8965 indoc! {r#"
8966 use mod1::mod2::«{mod3, mod4}ˇ»;
8967
8968 fn fn_1«ˇ(param1: bool, param2: &str)» {
8969 let var1 = "«ˇtext»";
8970 }
8971 "#},
8972 cx,
8973 );
8974 });
8975}
8976
8977#[gpui::test]
8978async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8979 init_test(cx, |_| {});
8980
8981 let language = Arc::new(Language::new(
8982 LanguageConfig::default(),
8983 Some(tree_sitter_rust::LANGUAGE.into()),
8984 ));
8985
8986 let text = "let a = 2;";
8987
8988 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8989 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8990 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8991
8992 editor
8993 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8994 .await;
8995
8996 // Test case 1: Cursor at end of word
8997 editor.update_in(cx, |editor, window, cx| {
8998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8999 s.select_display_ranges([
9000 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9001 ]);
9002 });
9003 });
9004 editor.update(cx, |editor, cx| {
9005 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9006 });
9007 editor.update_in(cx, |editor, window, cx| {
9008 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9009 });
9010 editor.update(cx, |editor, cx| {
9011 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9012 });
9013 editor.update_in(cx, |editor, window, cx| {
9014 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9015 });
9016 editor.update(cx, |editor, cx| {
9017 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9018 });
9019
9020 // Test case 2: Cursor at end of statement
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9023 s.select_display_ranges([
9024 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9025 ]);
9026 });
9027 });
9028 editor.update(cx, |editor, cx| {
9029 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9030 });
9031 editor.update_in(cx, |editor, window, cx| {
9032 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9033 });
9034 editor.update(cx, |editor, cx| {
9035 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9036 });
9037}
9038
9039#[gpui::test]
9040async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9041 init_test(cx, |_| {});
9042
9043 let language = Arc::new(Language::new(
9044 LanguageConfig {
9045 name: "JavaScript".into(),
9046 ..Default::default()
9047 },
9048 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9049 ));
9050
9051 let text = r#"
9052 let a = {
9053 key: "value",
9054 };
9055 "#
9056 .unindent();
9057
9058 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9059 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9060 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9061
9062 editor
9063 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9064 .await;
9065
9066 // Test case 1: Cursor after '{'
9067 editor.update_in(cx, |editor, window, cx| {
9068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9069 s.select_display_ranges([
9070 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9071 ]);
9072 });
9073 });
9074 editor.update(cx, |editor, cx| {
9075 assert_text_with_selections(
9076 editor,
9077 indoc! {r#"
9078 let a = {ˇ
9079 key: "value",
9080 };
9081 "#},
9082 cx,
9083 );
9084 });
9085 editor.update_in(cx, |editor, window, cx| {
9086 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9087 });
9088 editor.update(cx, |editor, cx| {
9089 assert_text_with_selections(
9090 editor,
9091 indoc! {r#"
9092 let a = «ˇ{
9093 key: "value",
9094 }»;
9095 "#},
9096 cx,
9097 );
9098 });
9099
9100 // Test case 2: Cursor after ':'
9101 editor.update_in(cx, |editor, window, cx| {
9102 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9103 s.select_display_ranges([
9104 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9105 ]);
9106 });
9107 });
9108 editor.update(cx, |editor, cx| {
9109 assert_text_with_selections(
9110 editor,
9111 indoc! {r#"
9112 let a = {
9113 key:ˇ "value",
9114 };
9115 "#},
9116 cx,
9117 );
9118 });
9119 editor.update_in(cx, |editor, window, cx| {
9120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9121 });
9122 editor.update(cx, |editor, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 let a = {
9127 «ˇkey: "value"»,
9128 };
9129 "#},
9130 cx,
9131 );
9132 });
9133 editor.update_in(cx, |editor, window, cx| {
9134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9135 });
9136 editor.update(cx, |editor, cx| {
9137 assert_text_with_selections(
9138 editor,
9139 indoc! {r#"
9140 let a = «ˇ{
9141 key: "value",
9142 }»;
9143 "#},
9144 cx,
9145 );
9146 });
9147
9148 // Test case 3: Cursor after ','
9149 editor.update_in(cx, |editor, window, cx| {
9150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9151 s.select_display_ranges([
9152 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9153 ]);
9154 });
9155 });
9156 editor.update(cx, |editor, cx| {
9157 assert_text_with_selections(
9158 editor,
9159 indoc! {r#"
9160 let a = {
9161 key: "value",ˇ
9162 };
9163 "#},
9164 cx,
9165 );
9166 });
9167 editor.update_in(cx, |editor, window, cx| {
9168 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9169 });
9170 editor.update(cx, |editor, cx| {
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 let a = «ˇ{
9175 key: "value",
9176 }»;
9177 "#},
9178 cx,
9179 );
9180 });
9181
9182 // Test case 4: Cursor after ';'
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9185 s.select_display_ranges([
9186 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9187 ]);
9188 });
9189 });
9190 editor.update(cx, |editor, cx| {
9191 assert_text_with_selections(
9192 editor,
9193 indoc! {r#"
9194 let a = {
9195 key: "value",
9196 };ˇ
9197 "#},
9198 cx,
9199 );
9200 });
9201 editor.update_in(cx, |editor, window, cx| {
9202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9203 });
9204 editor.update(cx, |editor, cx| {
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 «ˇlet a = {
9209 key: "value",
9210 };
9211 »"#},
9212 cx,
9213 );
9214 });
9215}
9216
9217#[gpui::test]
9218async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9219 init_test(cx, |_| {});
9220
9221 let language = Arc::new(Language::new(
9222 LanguageConfig::default(),
9223 Some(tree_sitter_rust::LANGUAGE.into()),
9224 ));
9225
9226 let text = r#"
9227 use mod1::mod2::{mod3, mod4};
9228
9229 fn fn_1(param1: bool, param2: &str) {
9230 let var1 = "hello world";
9231 }
9232 "#
9233 .unindent();
9234
9235 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9236 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9237 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9238
9239 editor
9240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9241 .await;
9242
9243 // Test 1: Cursor on a letter of a string word
9244 editor.update_in(cx, |editor, window, cx| {
9245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9246 s.select_display_ranges([
9247 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9248 ]);
9249 });
9250 });
9251 editor.update_in(cx, |editor, window, cx| {
9252 assert_text_with_selections(
9253 editor,
9254 indoc! {r#"
9255 use mod1::mod2::{mod3, mod4};
9256
9257 fn fn_1(param1: bool, param2: &str) {
9258 let var1 = "hˇello world";
9259 }
9260 "#},
9261 cx,
9262 );
9263 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9264 assert_text_with_selections(
9265 editor,
9266 indoc! {r#"
9267 use mod1::mod2::{mod3, mod4};
9268
9269 fn fn_1(param1: bool, param2: &str) {
9270 let var1 = "«ˇhello» world";
9271 }
9272 "#},
9273 cx,
9274 );
9275 });
9276
9277 // Test 2: Partial selection within a word
9278 editor.update_in(cx, |editor, window, cx| {
9279 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9280 s.select_display_ranges([
9281 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9282 ]);
9283 });
9284 });
9285 editor.update_in(cx, |editor, window, cx| {
9286 assert_text_with_selections(
9287 editor,
9288 indoc! {r#"
9289 use mod1::mod2::{mod3, mod4};
9290
9291 fn fn_1(param1: bool, param2: &str) {
9292 let var1 = "h«elˇ»lo world";
9293 }
9294 "#},
9295 cx,
9296 );
9297 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9298 assert_text_with_selections(
9299 editor,
9300 indoc! {r#"
9301 use mod1::mod2::{mod3, mod4};
9302
9303 fn fn_1(param1: bool, param2: &str) {
9304 let var1 = "«ˇhello» world";
9305 }
9306 "#},
9307 cx,
9308 );
9309 });
9310
9311 // Test 3: Complete word already selected
9312 editor.update_in(cx, |editor, window, cx| {
9313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9314 s.select_display_ranges([
9315 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9316 ]);
9317 });
9318 });
9319 editor.update_in(cx, |editor, window, cx| {
9320 assert_text_with_selections(
9321 editor,
9322 indoc! {r#"
9323 use mod1::mod2::{mod3, mod4};
9324
9325 fn fn_1(param1: bool, param2: &str) {
9326 let var1 = "«helloˇ» world";
9327 }
9328 "#},
9329 cx,
9330 );
9331 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9332 assert_text_with_selections(
9333 editor,
9334 indoc! {r#"
9335 use mod1::mod2::{mod3, mod4};
9336
9337 fn fn_1(param1: bool, param2: &str) {
9338 let var1 = "«hello worldˇ»";
9339 }
9340 "#},
9341 cx,
9342 );
9343 });
9344
9345 // Test 4: Selection spanning across words
9346 editor.update_in(cx, |editor, window, cx| {
9347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9348 s.select_display_ranges([
9349 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9350 ]);
9351 });
9352 });
9353 editor.update_in(cx, |editor, window, cx| {
9354 assert_text_with_selections(
9355 editor,
9356 indoc! {r#"
9357 use mod1::mod2::{mod3, mod4};
9358
9359 fn fn_1(param1: bool, param2: &str) {
9360 let var1 = "hel«lo woˇ»rld";
9361 }
9362 "#},
9363 cx,
9364 );
9365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9366 assert_text_with_selections(
9367 editor,
9368 indoc! {r#"
9369 use mod1::mod2::{mod3, mod4};
9370
9371 fn fn_1(param1: bool, param2: &str) {
9372 let var1 = "«ˇhello world»";
9373 }
9374 "#},
9375 cx,
9376 );
9377 });
9378
9379 // Test 5: Expansion beyond string
9380 editor.update_in(cx, |editor, window, cx| {
9381 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9382 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9383 assert_text_with_selections(
9384 editor,
9385 indoc! {r#"
9386 use mod1::mod2::{mod3, mod4};
9387
9388 fn fn_1(param1: bool, param2: &str) {
9389 «ˇlet var1 = "hello world";»
9390 }
9391 "#},
9392 cx,
9393 );
9394 });
9395}
9396
9397#[gpui::test]
9398async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9399 init_test(cx, |_| {});
9400
9401 let mut cx = EditorTestContext::new(cx).await;
9402
9403 let language = Arc::new(Language::new(
9404 LanguageConfig::default(),
9405 Some(tree_sitter_rust::LANGUAGE.into()),
9406 ));
9407
9408 cx.update_buffer(|buffer, cx| {
9409 buffer.set_language(Some(language), cx);
9410 });
9411
9412 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9413 cx.update_editor(|editor, window, cx| {
9414 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9415 });
9416
9417 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9418
9419 cx.set_state(indoc! { r#"fn a() {
9420 // what
9421 // a
9422 // ˇlong
9423 // method
9424 // I
9425 // sure
9426 // hope
9427 // it
9428 // works
9429 }"# });
9430
9431 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9432 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9433 cx.update(|_, cx| {
9434 multi_buffer.update(cx, |multi_buffer, cx| {
9435 multi_buffer.set_excerpts_for_path(
9436 PathKey::for_buffer(&buffer, cx),
9437 buffer,
9438 [Point::new(1, 0)..Point::new(1, 0)],
9439 3,
9440 cx,
9441 );
9442 });
9443 });
9444
9445 let editor2 = cx.new_window_entity(|window, cx| {
9446 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9447 });
9448
9449 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9450 cx.update_editor(|editor, window, cx| {
9451 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9452 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9453 })
9454 });
9455
9456 cx.assert_editor_state(indoc! { "
9457 fn a() {
9458 // what
9459 // a
9460 ˇ // long
9461 // method"});
9462
9463 cx.update_editor(|editor, window, cx| {
9464 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9465 });
9466
9467 // Although we could potentially make the action work when the syntax node
9468 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9469 // did. Maybe we could also expand the excerpt to contain the range?
9470 cx.assert_editor_state(indoc! { "
9471 fn a() {
9472 // what
9473 // a
9474 ˇ // long
9475 // method"});
9476}
9477
9478#[gpui::test]
9479async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9480 init_test(cx, |_| {});
9481
9482 let base_text = r#"
9483 impl A {
9484 // this is an uncommitted comment
9485
9486 fn b() {
9487 c();
9488 }
9489
9490 // this is another uncommitted comment
9491
9492 fn d() {
9493 // e
9494 // f
9495 }
9496 }
9497
9498 fn g() {
9499 // h
9500 }
9501 "#
9502 .unindent();
9503
9504 let text = r#"
9505 ˇimpl A {
9506
9507 fn b() {
9508 c();
9509 }
9510
9511 fn d() {
9512 // e
9513 // f
9514 }
9515 }
9516
9517 fn g() {
9518 // h
9519 }
9520 "#
9521 .unindent();
9522
9523 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9524 cx.set_state(&text);
9525 cx.set_head_text(&base_text);
9526 cx.update_editor(|editor, window, cx| {
9527 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9528 });
9529
9530 cx.assert_state_with_diff(
9531 "
9532 ˇimpl A {
9533 - // this is an uncommitted comment
9534
9535 fn b() {
9536 c();
9537 }
9538
9539 - // this is another uncommitted comment
9540 -
9541 fn d() {
9542 // e
9543 // f
9544 }
9545 }
9546
9547 fn g() {
9548 // h
9549 }
9550 "
9551 .unindent(),
9552 );
9553
9554 let expected_display_text = "
9555 impl A {
9556 // this is an uncommitted comment
9557
9558 fn b() {
9559 ⋯
9560 }
9561
9562 // this is another uncommitted comment
9563
9564 fn d() {
9565 ⋯
9566 }
9567 }
9568
9569 fn g() {
9570 ⋯
9571 }
9572 "
9573 .unindent();
9574
9575 cx.update_editor(|editor, window, cx| {
9576 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9577 assert_eq!(editor.display_text(cx), expected_display_text);
9578 });
9579}
9580
9581#[gpui::test]
9582async fn test_autoindent(cx: &mut TestAppContext) {
9583 init_test(cx, |_| {});
9584
9585 let language = Arc::new(
9586 Language::new(
9587 LanguageConfig {
9588 brackets: BracketPairConfig {
9589 pairs: vec![
9590 BracketPair {
9591 start: "{".to_string(),
9592 end: "}".to_string(),
9593 close: false,
9594 surround: false,
9595 newline: true,
9596 },
9597 BracketPair {
9598 start: "(".to_string(),
9599 end: ")".to_string(),
9600 close: false,
9601 surround: false,
9602 newline: true,
9603 },
9604 ],
9605 ..Default::default()
9606 },
9607 ..Default::default()
9608 },
9609 Some(tree_sitter_rust::LANGUAGE.into()),
9610 )
9611 .with_indents_query(
9612 r#"
9613 (_ "(" ")" @end) @indent
9614 (_ "{" "}" @end) @indent
9615 "#,
9616 )
9617 .unwrap(),
9618 );
9619
9620 let text = "fn a() {}";
9621
9622 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9623 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9625 editor
9626 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9627 .await;
9628
9629 editor.update_in(cx, |editor, window, cx| {
9630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9631 s.select_ranges([5..5, 8..8, 9..9])
9632 });
9633 editor.newline(&Newline, window, cx);
9634 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9635 assert_eq!(
9636 editor.selections.ranges(&editor.display_snapshot(cx)),
9637 &[
9638 Point::new(1, 4)..Point::new(1, 4),
9639 Point::new(3, 4)..Point::new(3, 4),
9640 Point::new(5, 0)..Point::new(5, 0)
9641 ]
9642 );
9643 });
9644}
9645
9646#[gpui::test]
9647async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9648 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9649
9650 let language = Arc::new(
9651 Language::new(
9652 LanguageConfig {
9653 brackets: BracketPairConfig {
9654 pairs: vec![
9655 BracketPair {
9656 start: "{".to_string(),
9657 end: "}".to_string(),
9658 close: false,
9659 surround: false,
9660 newline: true,
9661 },
9662 BracketPair {
9663 start: "(".to_string(),
9664 end: ")".to_string(),
9665 close: false,
9666 surround: false,
9667 newline: true,
9668 },
9669 ],
9670 ..Default::default()
9671 },
9672 ..Default::default()
9673 },
9674 Some(tree_sitter_rust::LANGUAGE.into()),
9675 )
9676 .with_indents_query(
9677 r#"
9678 (_ "(" ")" @end) @indent
9679 (_ "{" "}" @end) @indent
9680 "#,
9681 )
9682 .unwrap(),
9683 );
9684
9685 let text = "fn a() {}";
9686
9687 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9688 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9689 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9690 editor
9691 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9692 .await;
9693
9694 editor.update_in(cx, |editor, window, cx| {
9695 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9696 s.select_ranges([5..5, 8..8, 9..9])
9697 });
9698 editor.newline(&Newline, window, cx);
9699 assert_eq!(
9700 editor.text(cx),
9701 indoc!(
9702 "
9703 fn a(
9704
9705 ) {
9706
9707 }
9708 "
9709 )
9710 );
9711 assert_eq!(
9712 editor.selections.ranges(&editor.display_snapshot(cx)),
9713 &[
9714 Point::new(1, 0)..Point::new(1, 0),
9715 Point::new(3, 0)..Point::new(3, 0),
9716 Point::new(5, 0)..Point::new(5, 0)
9717 ]
9718 );
9719 });
9720}
9721
9722#[gpui::test]
9723async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9724 init_test(cx, |settings| {
9725 settings.defaults.auto_indent = Some(true);
9726 settings.languages.0.insert(
9727 "python".into(),
9728 LanguageSettingsContent {
9729 auto_indent: Some(false),
9730 ..Default::default()
9731 },
9732 );
9733 });
9734
9735 let mut cx = EditorTestContext::new(cx).await;
9736
9737 let injected_language = Arc::new(
9738 Language::new(
9739 LanguageConfig {
9740 brackets: BracketPairConfig {
9741 pairs: vec![
9742 BracketPair {
9743 start: "{".to_string(),
9744 end: "}".to_string(),
9745 close: false,
9746 surround: false,
9747 newline: true,
9748 },
9749 BracketPair {
9750 start: "(".to_string(),
9751 end: ")".to_string(),
9752 close: true,
9753 surround: false,
9754 newline: true,
9755 },
9756 ],
9757 ..Default::default()
9758 },
9759 name: "python".into(),
9760 ..Default::default()
9761 },
9762 Some(tree_sitter_python::LANGUAGE.into()),
9763 )
9764 .with_indents_query(
9765 r#"
9766 (_ "(" ")" @end) @indent
9767 (_ "{" "}" @end) @indent
9768 "#,
9769 )
9770 .unwrap(),
9771 );
9772
9773 let language = Arc::new(
9774 Language::new(
9775 LanguageConfig {
9776 brackets: BracketPairConfig {
9777 pairs: vec![
9778 BracketPair {
9779 start: "{".to_string(),
9780 end: "}".to_string(),
9781 close: false,
9782 surround: false,
9783 newline: true,
9784 },
9785 BracketPair {
9786 start: "(".to_string(),
9787 end: ")".to_string(),
9788 close: true,
9789 surround: false,
9790 newline: true,
9791 },
9792 ],
9793 ..Default::default()
9794 },
9795 name: LanguageName::new("rust"),
9796 ..Default::default()
9797 },
9798 Some(tree_sitter_rust::LANGUAGE.into()),
9799 )
9800 .with_indents_query(
9801 r#"
9802 (_ "(" ")" @end) @indent
9803 (_ "{" "}" @end) @indent
9804 "#,
9805 )
9806 .unwrap()
9807 .with_injection_query(
9808 r#"
9809 (macro_invocation
9810 macro: (identifier) @_macro_name
9811 (token_tree) @injection.content
9812 (#set! injection.language "python"))
9813 "#,
9814 )
9815 .unwrap(),
9816 );
9817
9818 cx.language_registry().add(injected_language);
9819 cx.language_registry().add(language.clone());
9820
9821 cx.update_buffer(|buffer, cx| {
9822 buffer.set_language(Some(language), cx);
9823 });
9824
9825 cx.set_state(r#"struct A {ˇ}"#);
9826
9827 cx.update_editor(|editor, window, cx| {
9828 editor.newline(&Default::default(), window, cx);
9829 });
9830
9831 cx.assert_editor_state(indoc!(
9832 "struct A {
9833 ˇ
9834 }"
9835 ));
9836
9837 cx.set_state(r#"select_biased!(ˇ)"#);
9838
9839 cx.update_editor(|editor, window, cx| {
9840 editor.newline(&Default::default(), window, cx);
9841 editor.handle_input("def ", window, cx);
9842 editor.handle_input("(", window, cx);
9843 editor.newline(&Default::default(), window, cx);
9844 editor.handle_input("a", window, cx);
9845 });
9846
9847 cx.assert_editor_state(indoc!(
9848 "select_biased!(
9849 def (
9850 aˇ
9851 )
9852 )"
9853 ));
9854}
9855
9856#[gpui::test]
9857async fn test_autoindent_selections(cx: &mut TestAppContext) {
9858 init_test(cx, |_| {});
9859
9860 {
9861 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9862 cx.set_state(indoc! {"
9863 impl A {
9864
9865 fn b() {}
9866
9867 «fn c() {
9868
9869 }ˇ»
9870 }
9871 "});
9872
9873 cx.update_editor(|editor, window, cx| {
9874 editor.autoindent(&Default::default(), window, cx);
9875 });
9876
9877 cx.assert_editor_state(indoc! {"
9878 impl A {
9879
9880 fn b() {}
9881
9882 «fn c() {
9883
9884 }ˇ»
9885 }
9886 "});
9887 }
9888
9889 {
9890 let mut cx = EditorTestContext::new_multibuffer(
9891 cx,
9892 [indoc! { "
9893 impl A {
9894 «
9895 // a
9896 fn b(){}
9897 »
9898 «
9899 }
9900 fn c(){}
9901 »
9902 "}],
9903 );
9904
9905 let buffer = cx.update_editor(|editor, _, cx| {
9906 let buffer = editor.buffer().update(cx, |buffer, _| {
9907 buffer.all_buffers().iter().next().unwrap().clone()
9908 });
9909 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9910 buffer
9911 });
9912
9913 cx.run_until_parked();
9914 cx.update_editor(|editor, window, cx| {
9915 editor.select_all(&Default::default(), window, cx);
9916 editor.autoindent(&Default::default(), window, cx)
9917 });
9918 cx.run_until_parked();
9919
9920 cx.update(|_, cx| {
9921 assert_eq!(
9922 buffer.read(cx).text(),
9923 indoc! { "
9924 impl A {
9925
9926 // a
9927 fn b(){}
9928
9929
9930 }
9931 fn c(){}
9932
9933 " }
9934 )
9935 });
9936 }
9937}
9938
9939#[gpui::test]
9940async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9941 init_test(cx, |_| {});
9942
9943 let mut cx = EditorTestContext::new(cx).await;
9944
9945 let language = Arc::new(Language::new(
9946 LanguageConfig {
9947 brackets: BracketPairConfig {
9948 pairs: vec![
9949 BracketPair {
9950 start: "{".to_string(),
9951 end: "}".to_string(),
9952 close: true,
9953 surround: true,
9954 newline: true,
9955 },
9956 BracketPair {
9957 start: "(".to_string(),
9958 end: ")".to_string(),
9959 close: true,
9960 surround: true,
9961 newline: true,
9962 },
9963 BracketPair {
9964 start: "/*".to_string(),
9965 end: " */".to_string(),
9966 close: true,
9967 surround: true,
9968 newline: true,
9969 },
9970 BracketPair {
9971 start: "[".to_string(),
9972 end: "]".to_string(),
9973 close: false,
9974 surround: false,
9975 newline: true,
9976 },
9977 BracketPair {
9978 start: "\"".to_string(),
9979 end: "\"".to_string(),
9980 close: true,
9981 surround: true,
9982 newline: false,
9983 },
9984 BracketPair {
9985 start: "<".to_string(),
9986 end: ">".to_string(),
9987 close: false,
9988 surround: true,
9989 newline: true,
9990 },
9991 ],
9992 ..Default::default()
9993 },
9994 autoclose_before: "})]".to_string(),
9995 ..Default::default()
9996 },
9997 Some(tree_sitter_rust::LANGUAGE.into()),
9998 ));
9999
10000 cx.language_registry().add(language.clone());
10001 cx.update_buffer(|buffer, cx| {
10002 buffer.set_language(Some(language), cx);
10003 });
10004
10005 cx.set_state(
10006 &r#"
10007 🏀ˇ
10008 εˇ
10009 ❤️ˇ
10010 "#
10011 .unindent(),
10012 );
10013
10014 // autoclose multiple nested brackets at multiple cursors
10015 cx.update_editor(|editor, window, cx| {
10016 editor.handle_input("{", window, cx);
10017 editor.handle_input("{", window, cx);
10018 editor.handle_input("{", window, cx);
10019 });
10020 cx.assert_editor_state(
10021 &"
10022 🏀{{{ˇ}}}
10023 ε{{{ˇ}}}
10024 ❤️{{{ˇ}}}
10025 "
10026 .unindent(),
10027 );
10028
10029 // insert a different closing bracket
10030 cx.update_editor(|editor, window, cx| {
10031 editor.handle_input(")", window, cx);
10032 });
10033 cx.assert_editor_state(
10034 &"
10035 🏀{{{)ˇ}}}
10036 ε{{{)ˇ}}}
10037 ❤️{{{)ˇ}}}
10038 "
10039 .unindent(),
10040 );
10041
10042 // skip over the auto-closed brackets when typing a closing bracket
10043 cx.update_editor(|editor, window, cx| {
10044 editor.move_right(&MoveRight, window, cx);
10045 editor.handle_input("}", window, cx);
10046 editor.handle_input("}", window, cx);
10047 editor.handle_input("}", window, cx);
10048 });
10049 cx.assert_editor_state(
10050 &"
10051 🏀{{{)}}}}ˇ
10052 ε{{{)}}}}ˇ
10053 ❤️{{{)}}}}ˇ
10054 "
10055 .unindent(),
10056 );
10057
10058 // autoclose multi-character pairs
10059 cx.set_state(
10060 &"
10061 ˇ
10062 ˇ
10063 "
10064 .unindent(),
10065 );
10066 cx.update_editor(|editor, window, cx| {
10067 editor.handle_input("/", window, cx);
10068 editor.handle_input("*", window, cx);
10069 });
10070 cx.assert_editor_state(
10071 &"
10072 /*ˇ */
10073 /*ˇ */
10074 "
10075 .unindent(),
10076 );
10077
10078 // one cursor autocloses a multi-character pair, one cursor
10079 // does not autoclose.
10080 cx.set_state(
10081 &"
10082 /ˇ
10083 ˇ
10084 "
10085 .unindent(),
10086 );
10087 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10088 cx.assert_editor_state(
10089 &"
10090 /*ˇ */
10091 *ˇ
10092 "
10093 .unindent(),
10094 );
10095
10096 // Don't autoclose if the next character isn't whitespace and isn't
10097 // listed in the language's "autoclose_before" section.
10098 cx.set_state("ˇa b");
10099 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10100 cx.assert_editor_state("{ˇa b");
10101
10102 // Don't autoclose if `close` is false for the bracket pair
10103 cx.set_state("ˇ");
10104 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10105 cx.assert_editor_state("[ˇ");
10106
10107 // Surround with brackets if text is selected
10108 cx.set_state("«aˇ» b");
10109 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10110 cx.assert_editor_state("{«aˇ»} b");
10111
10112 // Autoclose when not immediately after a word character
10113 cx.set_state("a ˇ");
10114 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10115 cx.assert_editor_state("a \"ˇ\"");
10116
10117 // Autoclose pair where the start and end characters are the same
10118 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10119 cx.assert_editor_state("a \"\"ˇ");
10120
10121 // Don't autoclose when immediately after a word character
10122 cx.set_state("aˇ");
10123 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10124 cx.assert_editor_state("a\"ˇ");
10125
10126 // Do autoclose when after a non-word character
10127 cx.set_state("{ˇ");
10128 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10129 cx.assert_editor_state("{\"ˇ\"");
10130
10131 // Non identical pairs autoclose regardless of preceding character
10132 cx.set_state("aˇ");
10133 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10134 cx.assert_editor_state("a{ˇ}");
10135
10136 // Don't autoclose pair if autoclose is disabled
10137 cx.set_state("ˇ");
10138 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10139 cx.assert_editor_state("<ˇ");
10140
10141 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10142 cx.set_state("«aˇ» b");
10143 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10144 cx.assert_editor_state("<«aˇ»> b");
10145}
10146
10147#[gpui::test]
10148async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10149 init_test(cx, |settings| {
10150 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10151 });
10152
10153 let mut cx = EditorTestContext::new(cx).await;
10154
10155 let language = Arc::new(Language::new(
10156 LanguageConfig {
10157 brackets: BracketPairConfig {
10158 pairs: vec![
10159 BracketPair {
10160 start: "{".to_string(),
10161 end: "}".to_string(),
10162 close: true,
10163 surround: true,
10164 newline: true,
10165 },
10166 BracketPair {
10167 start: "(".to_string(),
10168 end: ")".to_string(),
10169 close: true,
10170 surround: true,
10171 newline: true,
10172 },
10173 BracketPair {
10174 start: "[".to_string(),
10175 end: "]".to_string(),
10176 close: false,
10177 surround: false,
10178 newline: true,
10179 },
10180 ],
10181 ..Default::default()
10182 },
10183 autoclose_before: "})]".to_string(),
10184 ..Default::default()
10185 },
10186 Some(tree_sitter_rust::LANGUAGE.into()),
10187 ));
10188
10189 cx.language_registry().add(language.clone());
10190 cx.update_buffer(|buffer, cx| {
10191 buffer.set_language(Some(language), cx);
10192 });
10193
10194 cx.set_state(
10195 &"
10196 ˇ
10197 ˇ
10198 ˇ
10199 "
10200 .unindent(),
10201 );
10202
10203 // ensure only matching closing brackets are skipped over
10204 cx.update_editor(|editor, window, cx| {
10205 editor.handle_input("}", window, cx);
10206 editor.move_left(&MoveLeft, window, cx);
10207 editor.handle_input(")", window, cx);
10208 editor.move_left(&MoveLeft, window, cx);
10209 });
10210 cx.assert_editor_state(
10211 &"
10212 ˇ)}
10213 ˇ)}
10214 ˇ)}
10215 "
10216 .unindent(),
10217 );
10218
10219 // skip-over closing brackets at multiple cursors
10220 cx.update_editor(|editor, window, cx| {
10221 editor.handle_input(")", window, cx);
10222 editor.handle_input("}", window, cx);
10223 });
10224 cx.assert_editor_state(
10225 &"
10226 )}ˇ
10227 )}ˇ
10228 )}ˇ
10229 "
10230 .unindent(),
10231 );
10232
10233 // ignore non-close brackets
10234 cx.update_editor(|editor, window, cx| {
10235 editor.handle_input("]", window, cx);
10236 editor.move_left(&MoveLeft, window, cx);
10237 editor.handle_input("]", window, cx);
10238 });
10239 cx.assert_editor_state(
10240 &"
10241 )}]ˇ]
10242 )}]ˇ]
10243 )}]ˇ]
10244 "
10245 .unindent(),
10246 );
10247}
10248
10249#[gpui::test]
10250async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10251 init_test(cx, |_| {});
10252
10253 let mut cx = EditorTestContext::new(cx).await;
10254
10255 let html_language = Arc::new(
10256 Language::new(
10257 LanguageConfig {
10258 name: "HTML".into(),
10259 brackets: BracketPairConfig {
10260 pairs: vec![
10261 BracketPair {
10262 start: "<".into(),
10263 end: ">".into(),
10264 close: true,
10265 ..Default::default()
10266 },
10267 BracketPair {
10268 start: "{".into(),
10269 end: "}".into(),
10270 close: true,
10271 ..Default::default()
10272 },
10273 BracketPair {
10274 start: "(".into(),
10275 end: ")".into(),
10276 close: true,
10277 ..Default::default()
10278 },
10279 ],
10280 ..Default::default()
10281 },
10282 autoclose_before: "})]>".into(),
10283 ..Default::default()
10284 },
10285 Some(tree_sitter_html::LANGUAGE.into()),
10286 )
10287 .with_injection_query(
10288 r#"
10289 (script_element
10290 (raw_text) @injection.content
10291 (#set! injection.language "javascript"))
10292 "#,
10293 )
10294 .unwrap(),
10295 );
10296
10297 let javascript_language = Arc::new(Language::new(
10298 LanguageConfig {
10299 name: "JavaScript".into(),
10300 brackets: BracketPairConfig {
10301 pairs: vec![
10302 BracketPair {
10303 start: "/*".into(),
10304 end: " */".into(),
10305 close: true,
10306 ..Default::default()
10307 },
10308 BracketPair {
10309 start: "{".into(),
10310 end: "}".into(),
10311 close: true,
10312 ..Default::default()
10313 },
10314 BracketPair {
10315 start: "(".into(),
10316 end: ")".into(),
10317 close: true,
10318 ..Default::default()
10319 },
10320 ],
10321 ..Default::default()
10322 },
10323 autoclose_before: "})]>".into(),
10324 ..Default::default()
10325 },
10326 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10327 ));
10328
10329 cx.language_registry().add(html_language.clone());
10330 cx.language_registry().add(javascript_language);
10331 cx.executor().run_until_parked();
10332
10333 cx.update_buffer(|buffer, cx| {
10334 buffer.set_language(Some(html_language), cx);
10335 });
10336
10337 cx.set_state(
10338 &r#"
10339 <body>ˇ
10340 <script>
10341 var x = 1;ˇ
10342 </script>
10343 </body>ˇ
10344 "#
10345 .unindent(),
10346 );
10347
10348 // Precondition: different languages are active at different locations.
10349 cx.update_editor(|editor, window, cx| {
10350 let snapshot = editor.snapshot(window, cx);
10351 let cursors = editor
10352 .selections
10353 .ranges::<usize>(&editor.display_snapshot(cx));
10354 let languages = cursors
10355 .iter()
10356 .map(|c| snapshot.language_at(c.start).unwrap().name())
10357 .collect::<Vec<_>>();
10358 assert_eq!(
10359 languages,
10360 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10361 );
10362 });
10363
10364 // Angle brackets autoclose in HTML, but not JavaScript.
10365 cx.update_editor(|editor, window, cx| {
10366 editor.handle_input("<", window, cx);
10367 editor.handle_input("a", window, cx);
10368 });
10369 cx.assert_editor_state(
10370 &r#"
10371 <body><aˇ>
10372 <script>
10373 var x = 1;<aˇ
10374 </script>
10375 </body><aˇ>
10376 "#
10377 .unindent(),
10378 );
10379
10380 // Curly braces and parens autoclose in both HTML and JavaScript.
10381 cx.update_editor(|editor, window, cx| {
10382 editor.handle_input(" b=", window, cx);
10383 editor.handle_input("{", window, cx);
10384 editor.handle_input("c", window, cx);
10385 editor.handle_input("(", window, cx);
10386 });
10387 cx.assert_editor_state(
10388 &r#"
10389 <body><a b={c(ˇ)}>
10390 <script>
10391 var x = 1;<a b={c(ˇ)}
10392 </script>
10393 </body><a b={c(ˇ)}>
10394 "#
10395 .unindent(),
10396 );
10397
10398 // Brackets that were already autoclosed are skipped.
10399 cx.update_editor(|editor, window, cx| {
10400 editor.handle_input(")", window, cx);
10401 editor.handle_input("d", window, cx);
10402 editor.handle_input("}", window, cx);
10403 });
10404 cx.assert_editor_state(
10405 &r#"
10406 <body><a b={c()d}ˇ>
10407 <script>
10408 var x = 1;<a b={c()d}ˇ
10409 </script>
10410 </body><a b={c()d}ˇ>
10411 "#
10412 .unindent(),
10413 );
10414 cx.update_editor(|editor, window, cx| {
10415 editor.handle_input(">", window, cx);
10416 });
10417 cx.assert_editor_state(
10418 &r#"
10419 <body><a b={c()d}>ˇ
10420 <script>
10421 var x = 1;<a b={c()d}>ˇ
10422 </script>
10423 </body><a b={c()d}>ˇ
10424 "#
10425 .unindent(),
10426 );
10427
10428 // Reset
10429 cx.set_state(
10430 &r#"
10431 <body>ˇ
10432 <script>
10433 var x = 1;ˇ
10434 </script>
10435 </body>ˇ
10436 "#
10437 .unindent(),
10438 );
10439
10440 cx.update_editor(|editor, window, cx| {
10441 editor.handle_input("<", window, cx);
10442 });
10443 cx.assert_editor_state(
10444 &r#"
10445 <body><ˇ>
10446 <script>
10447 var x = 1;<ˇ
10448 </script>
10449 </body><ˇ>
10450 "#
10451 .unindent(),
10452 );
10453
10454 // When backspacing, the closing angle brackets are removed.
10455 cx.update_editor(|editor, window, cx| {
10456 editor.backspace(&Backspace, window, cx);
10457 });
10458 cx.assert_editor_state(
10459 &r#"
10460 <body>ˇ
10461 <script>
10462 var x = 1;ˇ
10463 </script>
10464 </body>ˇ
10465 "#
10466 .unindent(),
10467 );
10468
10469 // Block comments autoclose in JavaScript, but not HTML.
10470 cx.update_editor(|editor, window, cx| {
10471 editor.handle_input("/", window, cx);
10472 editor.handle_input("*", window, cx);
10473 });
10474 cx.assert_editor_state(
10475 &r#"
10476 <body>/*ˇ
10477 <script>
10478 var x = 1;/*ˇ */
10479 </script>
10480 </body>/*ˇ
10481 "#
10482 .unindent(),
10483 );
10484}
10485
10486#[gpui::test]
10487async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10488 init_test(cx, |_| {});
10489
10490 let mut cx = EditorTestContext::new(cx).await;
10491
10492 let rust_language = Arc::new(
10493 Language::new(
10494 LanguageConfig {
10495 name: "Rust".into(),
10496 brackets: serde_json::from_value(json!([
10497 { "start": "{", "end": "}", "close": true, "newline": true },
10498 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10499 ]))
10500 .unwrap(),
10501 autoclose_before: "})]>".into(),
10502 ..Default::default()
10503 },
10504 Some(tree_sitter_rust::LANGUAGE.into()),
10505 )
10506 .with_override_query("(string_literal) @string")
10507 .unwrap(),
10508 );
10509
10510 cx.language_registry().add(rust_language.clone());
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(rust_language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 let x = ˇ
10518 "#
10519 .unindent(),
10520 );
10521
10522 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10523 cx.update_editor(|editor, window, cx| {
10524 editor.handle_input("\"", window, cx);
10525 });
10526 cx.assert_editor_state(
10527 &r#"
10528 let x = "ˇ"
10529 "#
10530 .unindent(),
10531 );
10532
10533 // Inserting another quotation mark. The cursor moves across the existing
10534 // automatically-inserted quotation mark.
10535 cx.update_editor(|editor, window, cx| {
10536 editor.handle_input("\"", window, cx);
10537 });
10538 cx.assert_editor_state(
10539 &r#"
10540 let x = ""ˇ
10541 "#
10542 .unindent(),
10543 );
10544
10545 // Reset
10546 cx.set_state(
10547 &r#"
10548 let x = ˇ
10549 "#
10550 .unindent(),
10551 );
10552
10553 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10554 cx.update_editor(|editor, window, cx| {
10555 editor.handle_input("\"", window, cx);
10556 editor.handle_input(" ", window, cx);
10557 editor.move_left(&Default::default(), window, cx);
10558 editor.handle_input("\\", window, cx);
10559 editor.handle_input("\"", window, cx);
10560 });
10561 cx.assert_editor_state(
10562 &r#"
10563 let x = "\"ˇ "
10564 "#
10565 .unindent(),
10566 );
10567
10568 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10569 // mark. Nothing is inserted.
10570 cx.update_editor(|editor, window, cx| {
10571 editor.move_right(&Default::default(), window, cx);
10572 editor.handle_input("\"", window, cx);
10573 });
10574 cx.assert_editor_state(
10575 &r#"
10576 let x = "\" "ˇ
10577 "#
10578 .unindent(),
10579 );
10580}
10581
10582#[gpui::test]
10583async fn test_surround_with_pair(cx: &mut TestAppContext) {
10584 init_test(cx, |_| {});
10585
10586 let language = Arc::new(Language::new(
10587 LanguageConfig {
10588 brackets: BracketPairConfig {
10589 pairs: vec![
10590 BracketPair {
10591 start: "{".to_string(),
10592 end: "}".to_string(),
10593 close: true,
10594 surround: true,
10595 newline: true,
10596 },
10597 BracketPair {
10598 start: "/* ".to_string(),
10599 end: "*/".to_string(),
10600 close: true,
10601 surround: true,
10602 ..Default::default()
10603 },
10604 ],
10605 ..Default::default()
10606 },
10607 ..Default::default()
10608 },
10609 Some(tree_sitter_rust::LANGUAGE.into()),
10610 ));
10611
10612 let text = r#"
10613 a
10614 b
10615 c
10616 "#
10617 .unindent();
10618
10619 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10620 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10621 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10622 editor
10623 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10624 .await;
10625
10626 editor.update_in(cx, |editor, window, cx| {
10627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10628 s.select_display_ranges([
10629 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10631 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10632 ])
10633 });
10634
10635 editor.handle_input("{", window, cx);
10636 editor.handle_input("{", window, cx);
10637 editor.handle_input("{", window, cx);
10638 assert_eq!(
10639 editor.text(cx),
10640 "
10641 {{{a}}}
10642 {{{b}}}
10643 {{{c}}}
10644 "
10645 .unindent()
10646 );
10647 assert_eq!(
10648 editor.selections.display_ranges(cx),
10649 [
10650 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10651 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10652 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10653 ]
10654 );
10655
10656 editor.undo(&Undo, window, cx);
10657 editor.undo(&Undo, window, cx);
10658 editor.undo(&Undo, window, cx);
10659 assert_eq!(
10660 editor.text(cx),
10661 "
10662 a
10663 b
10664 c
10665 "
10666 .unindent()
10667 );
10668 assert_eq!(
10669 editor.selections.display_ranges(cx),
10670 [
10671 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10672 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10673 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10674 ]
10675 );
10676
10677 // Ensure inserting the first character of a multi-byte bracket pair
10678 // doesn't surround the selections with the bracket.
10679 editor.handle_input("/", window, cx);
10680 assert_eq!(
10681 editor.text(cx),
10682 "
10683 /
10684 /
10685 /
10686 "
10687 .unindent()
10688 );
10689 assert_eq!(
10690 editor.selections.display_ranges(cx),
10691 [
10692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10693 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10694 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10695 ]
10696 );
10697
10698 editor.undo(&Undo, window, cx);
10699 assert_eq!(
10700 editor.text(cx),
10701 "
10702 a
10703 b
10704 c
10705 "
10706 .unindent()
10707 );
10708 assert_eq!(
10709 editor.selections.display_ranges(cx),
10710 [
10711 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10713 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10714 ]
10715 );
10716
10717 // Ensure inserting the last character of a multi-byte bracket pair
10718 // doesn't surround the selections with the bracket.
10719 editor.handle_input("*", window, cx);
10720 assert_eq!(
10721 editor.text(cx),
10722 "
10723 *
10724 *
10725 *
10726 "
10727 .unindent()
10728 );
10729 assert_eq!(
10730 editor.selections.display_ranges(cx),
10731 [
10732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10733 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10734 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10735 ]
10736 );
10737 });
10738}
10739
10740#[gpui::test]
10741async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10742 init_test(cx, |_| {});
10743
10744 let language = Arc::new(Language::new(
10745 LanguageConfig {
10746 brackets: BracketPairConfig {
10747 pairs: vec![BracketPair {
10748 start: "{".to_string(),
10749 end: "}".to_string(),
10750 close: true,
10751 surround: true,
10752 newline: true,
10753 }],
10754 ..Default::default()
10755 },
10756 autoclose_before: "}".to_string(),
10757 ..Default::default()
10758 },
10759 Some(tree_sitter_rust::LANGUAGE.into()),
10760 ));
10761
10762 let text = r#"
10763 a
10764 b
10765 c
10766 "#
10767 .unindent();
10768
10769 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10770 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10771 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10772 editor
10773 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10774 .await;
10775
10776 editor.update_in(cx, |editor, window, cx| {
10777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10778 s.select_ranges([
10779 Point::new(0, 1)..Point::new(0, 1),
10780 Point::new(1, 1)..Point::new(1, 1),
10781 Point::new(2, 1)..Point::new(2, 1),
10782 ])
10783 });
10784
10785 editor.handle_input("{", window, cx);
10786 editor.handle_input("{", window, cx);
10787 editor.handle_input("_", window, cx);
10788 assert_eq!(
10789 editor.text(cx),
10790 "
10791 a{{_}}
10792 b{{_}}
10793 c{{_}}
10794 "
10795 .unindent()
10796 );
10797 assert_eq!(
10798 editor
10799 .selections
10800 .ranges::<Point>(&editor.display_snapshot(cx)),
10801 [
10802 Point::new(0, 4)..Point::new(0, 4),
10803 Point::new(1, 4)..Point::new(1, 4),
10804 Point::new(2, 4)..Point::new(2, 4)
10805 ]
10806 );
10807
10808 editor.backspace(&Default::default(), window, cx);
10809 editor.backspace(&Default::default(), window, cx);
10810 assert_eq!(
10811 editor.text(cx),
10812 "
10813 a{}
10814 b{}
10815 c{}
10816 "
10817 .unindent()
10818 );
10819 assert_eq!(
10820 editor
10821 .selections
10822 .ranges::<Point>(&editor.display_snapshot(cx)),
10823 [
10824 Point::new(0, 2)..Point::new(0, 2),
10825 Point::new(1, 2)..Point::new(1, 2),
10826 Point::new(2, 2)..Point::new(2, 2)
10827 ]
10828 );
10829
10830 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10831 assert_eq!(
10832 editor.text(cx),
10833 "
10834 a
10835 b
10836 c
10837 "
10838 .unindent()
10839 );
10840 assert_eq!(
10841 editor
10842 .selections
10843 .ranges::<Point>(&editor.display_snapshot(cx)),
10844 [
10845 Point::new(0, 1)..Point::new(0, 1),
10846 Point::new(1, 1)..Point::new(1, 1),
10847 Point::new(2, 1)..Point::new(2, 1)
10848 ]
10849 );
10850 });
10851}
10852
10853#[gpui::test]
10854async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10855 init_test(cx, |settings| {
10856 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10857 });
10858
10859 let mut cx = EditorTestContext::new(cx).await;
10860
10861 let language = Arc::new(Language::new(
10862 LanguageConfig {
10863 brackets: BracketPairConfig {
10864 pairs: vec![
10865 BracketPair {
10866 start: "{".to_string(),
10867 end: "}".to_string(),
10868 close: true,
10869 surround: true,
10870 newline: true,
10871 },
10872 BracketPair {
10873 start: "(".to_string(),
10874 end: ")".to_string(),
10875 close: true,
10876 surround: true,
10877 newline: true,
10878 },
10879 BracketPair {
10880 start: "[".to_string(),
10881 end: "]".to_string(),
10882 close: false,
10883 surround: true,
10884 newline: true,
10885 },
10886 ],
10887 ..Default::default()
10888 },
10889 autoclose_before: "})]".to_string(),
10890 ..Default::default()
10891 },
10892 Some(tree_sitter_rust::LANGUAGE.into()),
10893 ));
10894
10895 cx.language_registry().add(language.clone());
10896 cx.update_buffer(|buffer, cx| {
10897 buffer.set_language(Some(language), cx);
10898 });
10899
10900 cx.set_state(
10901 &"
10902 {(ˇ)}
10903 [[ˇ]]
10904 {(ˇ)}
10905 "
10906 .unindent(),
10907 );
10908
10909 cx.update_editor(|editor, window, cx| {
10910 editor.backspace(&Default::default(), window, cx);
10911 editor.backspace(&Default::default(), window, cx);
10912 });
10913
10914 cx.assert_editor_state(
10915 &"
10916 ˇ
10917 ˇ]]
10918 ˇ
10919 "
10920 .unindent(),
10921 );
10922
10923 cx.update_editor(|editor, window, cx| {
10924 editor.handle_input("{", window, cx);
10925 editor.handle_input("{", window, cx);
10926 editor.move_right(&MoveRight, window, cx);
10927 editor.move_right(&MoveRight, window, cx);
10928 editor.move_left(&MoveLeft, window, cx);
10929 editor.move_left(&MoveLeft, window, cx);
10930 editor.backspace(&Default::default(), window, cx);
10931 });
10932
10933 cx.assert_editor_state(
10934 &"
10935 {ˇ}
10936 {ˇ}]]
10937 {ˇ}
10938 "
10939 .unindent(),
10940 );
10941
10942 cx.update_editor(|editor, window, cx| {
10943 editor.backspace(&Default::default(), window, cx);
10944 });
10945
10946 cx.assert_editor_state(
10947 &"
10948 ˇ
10949 ˇ]]
10950 ˇ
10951 "
10952 .unindent(),
10953 );
10954}
10955
10956#[gpui::test]
10957async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10958 init_test(cx, |_| {});
10959
10960 let language = Arc::new(Language::new(
10961 LanguageConfig::default(),
10962 Some(tree_sitter_rust::LANGUAGE.into()),
10963 ));
10964
10965 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10968 editor
10969 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10970 .await;
10971
10972 editor.update_in(cx, |editor, window, cx| {
10973 editor.set_auto_replace_emoji_shortcode(true);
10974
10975 editor.handle_input("Hello ", window, cx);
10976 editor.handle_input(":wave", window, cx);
10977 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10978
10979 editor.handle_input(":", window, cx);
10980 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10981
10982 editor.handle_input(" :smile", window, cx);
10983 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10984
10985 editor.handle_input(":", window, cx);
10986 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10987
10988 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10989 editor.handle_input(":wave", window, cx);
10990 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10991
10992 editor.handle_input(":", window, cx);
10993 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10994
10995 editor.handle_input(":1", window, cx);
10996 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10997
10998 editor.handle_input(":", window, cx);
10999 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11000
11001 // Ensure shortcode does not get replaced when it is part of a word
11002 editor.handle_input(" Test:wave", window, cx);
11003 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11004
11005 editor.handle_input(":", window, cx);
11006 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11007
11008 editor.set_auto_replace_emoji_shortcode(false);
11009
11010 // Ensure shortcode does not get replaced when auto replace is off
11011 editor.handle_input(" :wave", window, cx);
11012 assert_eq!(
11013 editor.text(cx),
11014 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11015 );
11016
11017 editor.handle_input(":", window, cx);
11018 assert_eq!(
11019 editor.text(cx),
11020 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11021 );
11022 });
11023}
11024
11025#[gpui::test]
11026async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11027 init_test(cx, |_| {});
11028
11029 let (text, insertion_ranges) = marked_text_ranges(
11030 indoc! {"
11031 ˇ
11032 "},
11033 false,
11034 );
11035
11036 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11037 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11038
11039 _ = editor.update_in(cx, |editor, window, cx| {
11040 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11041
11042 editor
11043 .insert_snippet(&insertion_ranges, snippet, window, cx)
11044 .unwrap();
11045
11046 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11047 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11048 assert_eq!(editor.text(cx), expected_text);
11049 assert_eq!(
11050 editor
11051 .selections
11052 .ranges::<usize>(&editor.display_snapshot(cx)),
11053 selection_ranges
11054 );
11055 }
11056
11057 assert(
11058 editor,
11059 cx,
11060 indoc! {"
11061 type «» =•
11062 "},
11063 );
11064
11065 assert!(editor.context_menu_visible(), "There should be a matches");
11066 });
11067}
11068
11069#[gpui::test]
11070async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11071 init_test(cx, |_| {});
11072
11073 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11074 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11075 assert_eq!(editor.text(cx), expected_text);
11076 assert_eq!(
11077 editor
11078 .selections
11079 .ranges::<usize>(&editor.display_snapshot(cx)),
11080 selection_ranges
11081 );
11082 }
11083
11084 let (text, insertion_ranges) = marked_text_ranges(
11085 indoc! {"
11086 ˇ
11087 "},
11088 false,
11089 );
11090
11091 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11092 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11093
11094 _ = editor.update_in(cx, |editor, window, cx| {
11095 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11096
11097 editor
11098 .insert_snippet(&insertion_ranges, snippet, window, cx)
11099 .unwrap();
11100
11101 assert_state(
11102 editor,
11103 cx,
11104 indoc! {"
11105 type «» = ;•
11106 "},
11107 );
11108
11109 assert!(
11110 editor.context_menu_visible(),
11111 "Context menu should be visible for placeholder choices"
11112 );
11113
11114 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11115
11116 assert_state(
11117 editor,
11118 cx,
11119 indoc! {"
11120 type = «»;•
11121 "},
11122 );
11123
11124 assert!(
11125 !editor.context_menu_visible(),
11126 "Context menu should be hidden after moving to next tabstop"
11127 );
11128
11129 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11130
11131 assert_state(
11132 editor,
11133 cx,
11134 indoc! {"
11135 type = ; ˇ
11136 "},
11137 );
11138
11139 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11140
11141 assert_state(
11142 editor,
11143 cx,
11144 indoc! {"
11145 type = ; ˇ
11146 "},
11147 );
11148 });
11149
11150 _ = editor.update_in(cx, |editor, window, cx| {
11151 editor.select_all(&SelectAll, window, cx);
11152 editor.backspace(&Backspace, window, cx);
11153
11154 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11155 let insertion_ranges = editor
11156 .selections
11157 .all(&editor.display_snapshot(cx))
11158 .iter()
11159 .map(|s| s.range())
11160 .collect::<Vec<_>>();
11161
11162 editor
11163 .insert_snippet(&insertion_ranges, snippet, window, cx)
11164 .unwrap();
11165
11166 assert_state(editor, cx, "fn «» = value;•");
11167
11168 assert!(
11169 editor.context_menu_visible(),
11170 "Context menu should be visible for placeholder choices"
11171 );
11172
11173 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11174
11175 assert_state(editor, cx, "fn = «valueˇ»;•");
11176
11177 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11178
11179 assert_state(editor, cx, "fn «» = value;•");
11180
11181 assert!(
11182 editor.context_menu_visible(),
11183 "Context menu should be visible again after returning to first tabstop"
11184 );
11185
11186 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11187
11188 assert_state(editor, cx, "fn «» = value;•");
11189 });
11190}
11191
11192#[gpui::test]
11193async fn test_snippets(cx: &mut TestAppContext) {
11194 init_test(cx, |_| {});
11195
11196 let mut cx = EditorTestContext::new(cx).await;
11197
11198 cx.set_state(indoc! {"
11199 a.ˇ b
11200 a.ˇ b
11201 a.ˇ b
11202 "});
11203
11204 cx.update_editor(|editor, window, cx| {
11205 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11206 let insertion_ranges = editor
11207 .selections
11208 .all(&editor.display_snapshot(cx))
11209 .iter()
11210 .map(|s| s.range())
11211 .collect::<Vec<_>>();
11212 editor
11213 .insert_snippet(&insertion_ranges, snippet, window, cx)
11214 .unwrap();
11215 });
11216
11217 cx.assert_editor_state(indoc! {"
11218 a.f(«oneˇ», two, «threeˇ») b
11219 a.f(«oneˇ», two, «threeˇ») b
11220 a.f(«oneˇ», two, «threeˇ») b
11221 "});
11222
11223 // Can't move earlier than the first tab stop
11224 cx.update_editor(|editor, window, cx| {
11225 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11226 });
11227 cx.assert_editor_state(indoc! {"
11228 a.f(«oneˇ», two, «threeˇ») b
11229 a.f(«oneˇ», two, «threeˇ») b
11230 a.f(«oneˇ», two, «threeˇ») b
11231 "});
11232
11233 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11234 cx.assert_editor_state(indoc! {"
11235 a.f(one, «twoˇ», three) b
11236 a.f(one, «twoˇ», three) b
11237 a.f(one, «twoˇ», three) b
11238 "});
11239
11240 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11241 cx.assert_editor_state(indoc! {"
11242 a.f(«oneˇ», two, «threeˇ») b
11243 a.f(«oneˇ», two, «threeˇ») b
11244 a.f(«oneˇ», two, «threeˇ») b
11245 "});
11246
11247 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11248 cx.assert_editor_state(indoc! {"
11249 a.f(one, «twoˇ», three) b
11250 a.f(one, «twoˇ», three) b
11251 a.f(one, «twoˇ», three) b
11252 "});
11253 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11254 cx.assert_editor_state(indoc! {"
11255 a.f(one, two, three)ˇ b
11256 a.f(one, two, three)ˇ b
11257 a.f(one, two, three)ˇ b
11258 "});
11259
11260 // As soon as the last tab stop is reached, snippet state is gone
11261 cx.update_editor(|editor, window, cx| {
11262 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11263 });
11264 cx.assert_editor_state(indoc! {"
11265 a.f(one, two, three)ˇ b
11266 a.f(one, two, three)ˇ b
11267 a.f(one, two, three)ˇ b
11268 "});
11269}
11270
11271#[gpui::test]
11272async fn test_snippet_indentation(cx: &mut TestAppContext) {
11273 init_test(cx, |_| {});
11274
11275 let mut cx = EditorTestContext::new(cx).await;
11276
11277 cx.update_editor(|editor, window, cx| {
11278 let snippet = Snippet::parse(indoc! {"
11279 /*
11280 * Multiline comment with leading indentation
11281 *
11282 * $1
11283 */
11284 $0"})
11285 .unwrap();
11286 let insertion_ranges = editor
11287 .selections
11288 .all(&editor.display_snapshot(cx))
11289 .iter()
11290 .map(|s| s.range())
11291 .collect::<Vec<_>>();
11292 editor
11293 .insert_snippet(&insertion_ranges, snippet, window, cx)
11294 .unwrap();
11295 });
11296
11297 cx.assert_editor_state(indoc! {"
11298 /*
11299 * Multiline comment with leading indentation
11300 *
11301 * ˇ
11302 */
11303 "});
11304
11305 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11306 cx.assert_editor_state(indoc! {"
11307 /*
11308 * Multiline comment with leading indentation
11309 *
11310 *•
11311 */
11312 ˇ"});
11313}
11314
11315#[gpui::test]
11316async fn test_document_format_during_save(cx: &mut TestAppContext) {
11317 init_test(cx, |_| {});
11318
11319 let fs = FakeFs::new(cx.executor());
11320 fs.insert_file(path!("/file.rs"), Default::default()).await;
11321
11322 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11323
11324 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11325 language_registry.add(rust_lang());
11326 let mut fake_servers = language_registry.register_fake_lsp(
11327 "Rust",
11328 FakeLspAdapter {
11329 capabilities: lsp::ServerCapabilities {
11330 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11331 ..Default::default()
11332 },
11333 ..Default::default()
11334 },
11335 );
11336
11337 let buffer = project
11338 .update(cx, |project, cx| {
11339 project.open_local_buffer(path!("/file.rs"), cx)
11340 })
11341 .await
11342 .unwrap();
11343
11344 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11345 let (editor, cx) = cx.add_window_view(|window, cx| {
11346 build_editor_with_project(project.clone(), buffer, window, cx)
11347 });
11348 editor.update_in(cx, |editor, window, cx| {
11349 editor.set_text("one\ntwo\nthree\n", window, cx)
11350 });
11351 assert!(cx.read(|cx| editor.is_dirty(cx)));
11352
11353 cx.executor().start_waiting();
11354 let fake_server = fake_servers.next().await.unwrap();
11355
11356 {
11357 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11358 move |params, _| async move {
11359 assert_eq!(
11360 params.text_document.uri,
11361 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11362 );
11363 assert_eq!(params.options.tab_size, 4);
11364 Ok(Some(vec![lsp::TextEdit::new(
11365 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11366 ", ".to_string(),
11367 )]))
11368 },
11369 );
11370 let save = editor
11371 .update_in(cx, |editor, window, cx| {
11372 editor.save(
11373 SaveOptions {
11374 format: true,
11375 autosave: false,
11376 },
11377 project.clone(),
11378 window,
11379 cx,
11380 )
11381 })
11382 .unwrap();
11383 cx.executor().start_waiting();
11384 save.await;
11385
11386 assert_eq!(
11387 editor.update(cx, |editor, cx| editor.text(cx)),
11388 "one, two\nthree\n"
11389 );
11390 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11391 }
11392
11393 {
11394 editor.update_in(cx, |editor, window, cx| {
11395 editor.set_text("one\ntwo\nthree\n", window, cx)
11396 });
11397 assert!(cx.read(|cx| editor.is_dirty(cx)));
11398
11399 // Ensure we can still save even if formatting hangs.
11400 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11401 move |params, _| async move {
11402 assert_eq!(
11403 params.text_document.uri,
11404 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11405 );
11406 futures::future::pending::<()>().await;
11407 unreachable!()
11408 },
11409 );
11410 let save = editor
11411 .update_in(cx, |editor, window, cx| {
11412 editor.save(
11413 SaveOptions {
11414 format: true,
11415 autosave: false,
11416 },
11417 project.clone(),
11418 window,
11419 cx,
11420 )
11421 })
11422 .unwrap();
11423 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11424 cx.executor().start_waiting();
11425 save.await;
11426 assert_eq!(
11427 editor.update(cx, |editor, cx| editor.text(cx)),
11428 "one\ntwo\nthree\n"
11429 );
11430 }
11431
11432 // Set rust language override and assert overridden tabsize is sent to language server
11433 update_test_language_settings(cx, |settings| {
11434 settings.languages.0.insert(
11435 "Rust".into(),
11436 LanguageSettingsContent {
11437 tab_size: NonZeroU32::new(8),
11438 ..Default::default()
11439 },
11440 );
11441 });
11442
11443 {
11444 editor.update_in(cx, |editor, window, cx| {
11445 editor.set_text("somehting_new\n", window, cx)
11446 });
11447 assert!(cx.read(|cx| editor.is_dirty(cx)));
11448 let _formatting_request_signal = fake_server
11449 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11450 assert_eq!(
11451 params.text_document.uri,
11452 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11453 );
11454 assert_eq!(params.options.tab_size, 8);
11455 Ok(Some(vec![]))
11456 });
11457 let save = editor
11458 .update_in(cx, |editor, window, cx| {
11459 editor.save(
11460 SaveOptions {
11461 format: true,
11462 autosave: false,
11463 },
11464 project.clone(),
11465 window,
11466 cx,
11467 )
11468 })
11469 .unwrap();
11470 cx.executor().start_waiting();
11471 save.await;
11472 }
11473}
11474
11475#[gpui::test]
11476async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11477 init_test(cx, |settings| {
11478 settings.defaults.ensure_final_newline_on_save = Some(false);
11479 });
11480
11481 let fs = FakeFs::new(cx.executor());
11482 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11483
11484 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11485
11486 let buffer = project
11487 .update(cx, |project, cx| {
11488 project.open_local_buffer(path!("/file.txt"), cx)
11489 })
11490 .await
11491 .unwrap();
11492
11493 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11494 let (editor, cx) = cx.add_window_view(|window, cx| {
11495 build_editor_with_project(project.clone(), buffer, window, cx)
11496 });
11497 editor.update_in(cx, |editor, window, cx| {
11498 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11499 s.select_ranges([0..0])
11500 });
11501 });
11502 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11503
11504 editor.update_in(cx, |editor, window, cx| {
11505 editor.handle_input("\n", window, cx)
11506 });
11507 cx.run_until_parked();
11508 save(&editor, &project, cx).await;
11509 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11510
11511 editor.update_in(cx, |editor, window, cx| {
11512 editor.undo(&Default::default(), window, cx);
11513 });
11514 save(&editor, &project, cx).await;
11515 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11516
11517 editor.update_in(cx, |editor, window, cx| {
11518 editor.redo(&Default::default(), window, cx);
11519 });
11520 cx.run_until_parked();
11521 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11522
11523 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11524 let save = editor
11525 .update_in(cx, |editor, window, cx| {
11526 editor.save(
11527 SaveOptions {
11528 format: true,
11529 autosave: false,
11530 },
11531 project.clone(),
11532 window,
11533 cx,
11534 )
11535 })
11536 .unwrap();
11537 cx.executor().start_waiting();
11538 save.await;
11539 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11540 }
11541}
11542
11543#[gpui::test]
11544async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11545 init_test(cx, |_| {});
11546
11547 let cols = 4;
11548 let rows = 10;
11549 let sample_text_1 = sample_text(rows, cols, 'a');
11550 assert_eq!(
11551 sample_text_1,
11552 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11553 );
11554 let sample_text_2 = sample_text(rows, cols, 'l');
11555 assert_eq!(
11556 sample_text_2,
11557 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11558 );
11559 let sample_text_3 = sample_text(rows, cols, 'v');
11560 assert_eq!(
11561 sample_text_3,
11562 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11563 );
11564
11565 let fs = FakeFs::new(cx.executor());
11566 fs.insert_tree(
11567 path!("/a"),
11568 json!({
11569 "main.rs": sample_text_1,
11570 "other.rs": sample_text_2,
11571 "lib.rs": sample_text_3,
11572 }),
11573 )
11574 .await;
11575
11576 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11577 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11578 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11579
11580 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11581 language_registry.add(rust_lang());
11582 let mut fake_servers = language_registry.register_fake_lsp(
11583 "Rust",
11584 FakeLspAdapter {
11585 capabilities: lsp::ServerCapabilities {
11586 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11587 ..Default::default()
11588 },
11589 ..Default::default()
11590 },
11591 );
11592
11593 let worktree = project.update(cx, |project, cx| {
11594 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11595 assert_eq!(worktrees.len(), 1);
11596 worktrees.pop().unwrap()
11597 });
11598 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11599
11600 let buffer_1 = project
11601 .update(cx, |project, cx| {
11602 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11603 })
11604 .await
11605 .unwrap();
11606 let buffer_2 = project
11607 .update(cx, |project, cx| {
11608 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11609 })
11610 .await
11611 .unwrap();
11612 let buffer_3 = project
11613 .update(cx, |project, cx| {
11614 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11615 })
11616 .await
11617 .unwrap();
11618
11619 let multi_buffer = cx.new(|cx| {
11620 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11621 multi_buffer.push_excerpts(
11622 buffer_1.clone(),
11623 [
11624 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11625 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11626 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11627 ],
11628 cx,
11629 );
11630 multi_buffer.push_excerpts(
11631 buffer_2.clone(),
11632 [
11633 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11634 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11635 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11636 ],
11637 cx,
11638 );
11639 multi_buffer.push_excerpts(
11640 buffer_3.clone(),
11641 [
11642 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11643 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11644 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11645 ],
11646 cx,
11647 );
11648 multi_buffer
11649 });
11650 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11651 Editor::new(
11652 EditorMode::full(),
11653 multi_buffer,
11654 Some(project.clone()),
11655 window,
11656 cx,
11657 )
11658 });
11659
11660 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11661 editor.change_selections(
11662 SelectionEffects::scroll(Autoscroll::Next),
11663 window,
11664 cx,
11665 |s| s.select_ranges(Some(1..2)),
11666 );
11667 editor.insert("|one|two|three|", window, cx);
11668 });
11669 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11670 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11671 editor.change_selections(
11672 SelectionEffects::scroll(Autoscroll::Next),
11673 window,
11674 cx,
11675 |s| s.select_ranges(Some(60..70)),
11676 );
11677 editor.insert("|four|five|six|", window, cx);
11678 });
11679 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11680
11681 // First two buffers should be edited, but not the third one.
11682 assert_eq!(
11683 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11684 "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}",
11685 );
11686 buffer_1.update(cx, |buffer, _| {
11687 assert!(buffer.is_dirty());
11688 assert_eq!(
11689 buffer.text(),
11690 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11691 )
11692 });
11693 buffer_2.update(cx, |buffer, _| {
11694 assert!(buffer.is_dirty());
11695 assert_eq!(
11696 buffer.text(),
11697 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11698 )
11699 });
11700 buffer_3.update(cx, |buffer, _| {
11701 assert!(!buffer.is_dirty());
11702 assert_eq!(buffer.text(), sample_text_3,)
11703 });
11704 cx.executor().run_until_parked();
11705
11706 cx.executor().start_waiting();
11707 let save = multi_buffer_editor
11708 .update_in(cx, |editor, window, cx| {
11709 editor.save(
11710 SaveOptions {
11711 format: true,
11712 autosave: false,
11713 },
11714 project.clone(),
11715 window,
11716 cx,
11717 )
11718 })
11719 .unwrap();
11720
11721 let fake_server = fake_servers.next().await.unwrap();
11722 fake_server
11723 .server
11724 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11725 Ok(Some(vec![lsp::TextEdit::new(
11726 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11727 format!("[{} formatted]", params.text_document.uri),
11728 )]))
11729 })
11730 .detach();
11731 save.await;
11732
11733 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11734 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11735 assert_eq!(
11736 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11737 uri!(
11738 "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}"
11739 ),
11740 );
11741 buffer_1.update(cx, |buffer, _| {
11742 assert!(!buffer.is_dirty());
11743 assert_eq!(
11744 buffer.text(),
11745 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11746 )
11747 });
11748 buffer_2.update(cx, |buffer, _| {
11749 assert!(!buffer.is_dirty());
11750 assert_eq!(
11751 buffer.text(),
11752 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11753 )
11754 });
11755 buffer_3.update(cx, |buffer, _| {
11756 assert!(!buffer.is_dirty());
11757 assert_eq!(buffer.text(), sample_text_3,)
11758 });
11759}
11760
11761#[gpui::test]
11762async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11763 init_test(cx, |_| {});
11764
11765 let fs = FakeFs::new(cx.executor());
11766 fs.insert_tree(
11767 path!("/dir"),
11768 json!({
11769 "file1.rs": "fn main() { println!(\"hello\"); }",
11770 "file2.rs": "fn test() { println!(\"test\"); }",
11771 "file3.rs": "fn other() { println!(\"other\"); }\n",
11772 }),
11773 )
11774 .await;
11775
11776 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11777 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11778 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11779
11780 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11781 language_registry.add(rust_lang());
11782
11783 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11784 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11785
11786 // Open three buffers
11787 let buffer_1 = project
11788 .update(cx, |project, cx| {
11789 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11790 })
11791 .await
11792 .unwrap();
11793 let buffer_2 = project
11794 .update(cx, |project, cx| {
11795 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11796 })
11797 .await
11798 .unwrap();
11799 let buffer_3 = project
11800 .update(cx, |project, cx| {
11801 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11802 })
11803 .await
11804 .unwrap();
11805
11806 // Create a multi-buffer with all three buffers
11807 let multi_buffer = cx.new(|cx| {
11808 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11809 multi_buffer.push_excerpts(
11810 buffer_1.clone(),
11811 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11812 cx,
11813 );
11814 multi_buffer.push_excerpts(
11815 buffer_2.clone(),
11816 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11817 cx,
11818 );
11819 multi_buffer.push_excerpts(
11820 buffer_3.clone(),
11821 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11822 cx,
11823 );
11824 multi_buffer
11825 });
11826
11827 let editor = cx.new_window_entity(|window, cx| {
11828 Editor::new(
11829 EditorMode::full(),
11830 multi_buffer,
11831 Some(project.clone()),
11832 window,
11833 cx,
11834 )
11835 });
11836
11837 // Edit only the first buffer
11838 editor.update_in(cx, |editor, window, cx| {
11839 editor.change_selections(
11840 SelectionEffects::scroll(Autoscroll::Next),
11841 window,
11842 cx,
11843 |s| s.select_ranges(Some(10..10)),
11844 );
11845 editor.insert("// edited", window, cx);
11846 });
11847
11848 // Verify that only buffer 1 is dirty
11849 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11850 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11851 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11852
11853 // Get write counts after file creation (files were created with initial content)
11854 // We expect each file to have been written once during creation
11855 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11856 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11857 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11858
11859 // Perform autosave
11860 let save_task = editor.update_in(cx, |editor, window, cx| {
11861 editor.save(
11862 SaveOptions {
11863 format: true,
11864 autosave: true,
11865 },
11866 project.clone(),
11867 window,
11868 cx,
11869 )
11870 });
11871 save_task.await.unwrap();
11872
11873 // Only the dirty buffer should have been saved
11874 assert_eq!(
11875 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11876 1,
11877 "Buffer 1 was dirty, so it should have been written once during autosave"
11878 );
11879 assert_eq!(
11880 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11881 0,
11882 "Buffer 2 was clean, so it should not have been written during autosave"
11883 );
11884 assert_eq!(
11885 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11886 0,
11887 "Buffer 3 was clean, so it should not have been written during autosave"
11888 );
11889
11890 // Verify buffer states after autosave
11891 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11892 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11893 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11894
11895 // Now perform a manual save (format = true)
11896 let save_task = editor.update_in(cx, |editor, window, cx| {
11897 editor.save(
11898 SaveOptions {
11899 format: true,
11900 autosave: false,
11901 },
11902 project.clone(),
11903 window,
11904 cx,
11905 )
11906 });
11907 save_task.await.unwrap();
11908
11909 // During manual save, clean buffers don't get written to disk
11910 // They just get did_save called for language server notifications
11911 assert_eq!(
11912 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11913 1,
11914 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11915 );
11916 assert_eq!(
11917 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11918 0,
11919 "Buffer 2 should not have been written at all"
11920 );
11921 assert_eq!(
11922 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11923 0,
11924 "Buffer 3 should not have been written at all"
11925 );
11926}
11927
11928async fn setup_range_format_test(
11929 cx: &mut TestAppContext,
11930) -> (
11931 Entity<Project>,
11932 Entity<Editor>,
11933 &mut gpui::VisualTestContext,
11934 lsp::FakeLanguageServer,
11935) {
11936 init_test(cx, |_| {});
11937
11938 let fs = FakeFs::new(cx.executor());
11939 fs.insert_file(path!("/file.rs"), Default::default()).await;
11940
11941 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11942
11943 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11944 language_registry.add(rust_lang());
11945 let mut fake_servers = language_registry.register_fake_lsp(
11946 "Rust",
11947 FakeLspAdapter {
11948 capabilities: lsp::ServerCapabilities {
11949 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11950 ..lsp::ServerCapabilities::default()
11951 },
11952 ..FakeLspAdapter::default()
11953 },
11954 );
11955
11956 let buffer = project
11957 .update(cx, |project, cx| {
11958 project.open_local_buffer(path!("/file.rs"), cx)
11959 })
11960 .await
11961 .unwrap();
11962
11963 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11964 let (editor, cx) = cx.add_window_view(|window, cx| {
11965 build_editor_with_project(project.clone(), buffer, window, cx)
11966 });
11967
11968 cx.executor().start_waiting();
11969 let fake_server = fake_servers.next().await.unwrap();
11970
11971 (project, editor, cx, fake_server)
11972}
11973
11974#[gpui::test]
11975async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11976 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11977
11978 editor.update_in(cx, |editor, window, cx| {
11979 editor.set_text("one\ntwo\nthree\n", window, cx)
11980 });
11981 assert!(cx.read(|cx| editor.is_dirty(cx)));
11982
11983 let save = editor
11984 .update_in(cx, |editor, window, cx| {
11985 editor.save(
11986 SaveOptions {
11987 format: true,
11988 autosave: false,
11989 },
11990 project.clone(),
11991 window,
11992 cx,
11993 )
11994 })
11995 .unwrap();
11996 fake_server
11997 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11998 assert_eq!(
11999 params.text_document.uri,
12000 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12001 );
12002 assert_eq!(params.options.tab_size, 4);
12003 Ok(Some(vec![lsp::TextEdit::new(
12004 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12005 ", ".to_string(),
12006 )]))
12007 })
12008 .next()
12009 .await;
12010 cx.executor().start_waiting();
12011 save.await;
12012 assert_eq!(
12013 editor.update(cx, |editor, cx| editor.text(cx)),
12014 "one, two\nthree\n"
12015 );
12016 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12017}
12018
12019#[gpui::test]
12020async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12021 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12022
12023 editor.update_in(cx, |editor, window, cx| {
12024 editor.set_text("one\ntwo\nthree\n", window, cx)
12025 });
12026 assert!(cx.read(|cx| editor.is_dirty(cx)));
12027
12028 // Test that save still works when formatting hangs
12029 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12030 move |params, _| async move {
12031 assert_eq!(
12032 params.text_document.uri,
12033 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12034 );
12035 futures::future::pending::<()>().await;
12036 unreachable!()
12037 },
12038 );
12039 let save = editor
12040 .update_in(cx, |editor, window, cx| {
12041 editor.save(
12042 SaveOptions {
12043 format: true,
12044 autosave: false,
12045 },
12046 project.clone(),
12047 window,
12048 cx,
12049 )
12050 })
12051 .unwrap();
12052 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12053 cx.executor().start_waiting();
12054 save.await;
12055 assert_eq!(
12056 editor.update(cx, |editor, cx| editor.text(cx)),
12057 "one\ntwo\nthree\n"
12058 );
12059 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12060}
12061
12062#[gpui::test]
12063async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12064 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12065
12066 // Buffer starts clean, no formatting should be requested
12067 let save = editor
12068 .update_in(cx, |editor, window, cx| {
12069 editor.save(
12070 SaveOptions {
12071 format: false,
12072 autosave: false,
12073 },
12074 project.clone(),
12075 window,
12076 cx,
12077 )
12078 })
12079 .unwrap();
12080 let _pending_format_request = fake_server
12081 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12082 panic!("Should not be invoked");
12083 })
12084 .next();
12085 cx.executor().start_waiting();
12086 save.await;
12087 cx.run_until_parked();
12088}
12089
12090#[gpui::test]
12091async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12092 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12093
12094 // Set Rust language override and assert overridden tabsize is sent to language server
12095 update_test_language_settings(cx, |settings| {
12096 settings.languages.0.insert(
12097 "Rust".into(),
12098 LanguageSettingsContent {
12099 tab_size: NonZeroU32::new(8),
12100 ..Default::default()
12101 },
12102 );
12103 });
12104
12105 editor.update_in(cx, |editor, window, cx| {
12106 editor.set_text("something_new\n", window, cx)
12107 });
12108 assert!(cx.read(|cx| editor.is_dirty(cx)));
12109 let save = editor
12110 .update_in(cx, |editor, window, cx| {
12111 editor.save(
12112 SaveOptions {
12113 format: true,
12114 autosave: false,
12115 },
12116 project.clone(),
12117 window,
12118 cx,
12119 )
12120 })
12121 .unwrap();
12122 fake_server
12123 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12124 assert_eq!(
12125 params.text_document.uri,
12126 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12127 );
12128 assert_eq!(params.options.tab_size, 8);
12129 Ok(Some(Vec::new()))
12130 })
12131 .next()
12132 .await;
12133 save.await;
12134}
12135
12136#[gpui::test]
12137async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12138 init_test(cx, |settings| {
12139 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12140 settings::LanguageServerFormatterSpecifier::Current,
12141 )))
12142 });
12143
12144 let fs = FakeFs::new(cx.executor());
12145 fs.insert_file(path!("/file.rs"), Default::default()).await;
12146
12147 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12148
12149 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12150 language_registry.add(Arc::new(Language::new(
12151 LanguageConfig {
12152 name: "Rust".into(),
12153 matcher: LanguageMatcher {
12154 path_suffixes: vec!["rs".to_string()],
12155 ..Default::default()
12156 },
12157 ..LanguageConfig::default()
12158 },
12159 Some(tree_sitter_rust::LANGUAGE.into()),
12160 )));
12161 update_test_language_settings(cx, |settings| {
12162 // Enable Prettier formatting for the same buffer, and ensure
12163 // LSP is called instead of Prettier.
12164 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12165 });
12166 let mut fake_servers = language_registry.register_fake_lsp(
12167 "Rust",
12168 FakeLspAdapter {
12169 capabilities: lsp::ServerCapabilities {
12170 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12171 ..Default::default()
12172 },
12173 ..Default::default()
12174 },
12175 );
12176
12177 let buffer = project
12178 .update(cx, |project, cx| {
12179 project.open_local_buffer(path!("/file.rs"), cx)
12180 })
12181 .await
12182 .unwrap();
12183
12184 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12185 let (editor, cx) = cx.add_window_view(|window, cx| {
12186 build_editor_with_project(project.clone(), buffer, window, cx)
12187 });
12188 editor.update_in(cx, |editor, window, cx| {
12189 editor.set_text("one\ntwo\nthree\n", window, cx)
12190 });
12191
12192 cx.executor().start_waiting();
12193 let fake_server = fake_servers.next().await.unwrap();
12194
12195 let format = editor
12196 .update_in(cx, |editor, window, cx| {
12197 editor.perform_format(
12198 project.clone(),
12199 FormatTrigger::Manual,
12200 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12201 window,
12202 cx,
12203 )
12204 })
12205 .unwrap();
12206 fake_server
12207 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12208 assert_eq!(
12209 params.text_document.uri,
12210 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12211 );
12212 assert_eq!(params.options.tab_size, 4);
12213 Ok(Some(vec![lsp::TextEdit::new(
12214 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12215 ", ".to_string(),
12216 )]))
12217 })
12218 .next()
12219 .await;
12220 cx.executor().start_waiting();
12221 format.await;
12222 assert_eq!(
12223 editor.update(cx, |editor, cx| editor.text(cx)),
12224 "one, two\nthree\n"
12225 );
12226
12227 editor.update_in(cx, |editor, window, cx| {
12228 editor.set_text("one\ntwo\nthree\n", window, cx)
12229 });
12230 // Ensure we don't lock if formatting hangs.
12231 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12232 move |params, _| async move {
12233 assert_eq!(
12234 params.text_document.uri,
12235 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12236 );
12237 futures::future::pending::<()>().await;
12238 unreachable!()
12239 },
12240 );
12241 let format = editor
12242 .update_in(cx, |editor, window, cx| {
12243 editor.perform_format(
12244 project,
12245 FormatTrigger::Manual,
12246 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12247 window,
12248 cx,
12249 )
12250 })
12251 .unwrap();
12252 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12253 cx.executor().start_waiting();
12254 format.await;
12255 assert_eq!(
12256 editor.update(cx, |editor, cx| editor.text(cx)),
12257 "one\ntwo\nthree\n"
12258 );
12259}
12260
12261#[gpui::test]
12262async fn test_multiple_formatters(cx: &mut TestAppContext) {
12263 init_test(cx, |settings| {
12264 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12265 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12266 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12267 Formatter::CodeAction("code-action-1".into()),
12268 Formatter::CodeAction("code-action-2".into()),
12269 ]))
12270 });
12271
12272 let fs = FakeFs::new(cx.executor());
12273 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12274 .await;
12275
12276 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12277 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12278 language_registry.add(rust_lang());
12279
12280 let mut fake_servers = language_registry.register_fake_lsp(
12281 "Rust",
12282 FakeLspAdapter {
12283 capabilities: lsp::ServerCapabilities {
12284 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12285 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12286 commands: vec!["the-command-for-code-action-1".into()],
12287 ..Default::default()
12288 }),
12289 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12290 ..Default::default()
12291 },
12292 ..Default::default()
12293 },
12294 );
12295
12296 let buffer = project
12297 .update(cx, |project, cx| {
12298 project.open_local_buffer(path!("/file.rs"), cx)
12299 })
12300 .await
12301 .unwrap();
12302
12303 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12304 let (editor, cx) = cx.add_window_view(|window, cx| {
12305 build_editor_with_project(project.clone(), buffer, window, cx)
12306 });
12307
12308 cx.executor().start_waiting();
12309
12310 let fake_server = fake_servers.next().await.unwrap();
12311 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12312 move |_params, _| async move {
12313 Ok(Some(vec![lsp::TextEdit::new(
12314 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12315 "applied-formatting\n".to_string(),
12316 )]))
12317 },
12318 );
12319 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12320 move |params, _| async move {
12321 let requested_code_actions = params.context.only.expect("Expected code action request");
12322 assert_eq!(requested_code_actions.len(), 1);
12323
12324 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12325 let code_action = match requested_code_actions[0].as_str() {
12326 "code-action-1" => lsp::CodeAction {
12327 kind: Some("code-action-1".into()),
12328 edit: Some(lsp::WorkspaceEdit::new(
12329 [(
12330 uri,
12331 vec![lsp::TextEdit::new(
12332 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12333 "applied-code-action-1-edit\n".to_string(),
12334 )],
12335 )]
12336 .into_iter()
12337 .collect(),
12338 )),
12339 command: Some(lsp::Command {
12340 command: "the-command-for-code-action-1".into(),
12341 ..Default::default()
12342 }),
12343 ..Default::default()
12344 },
12345 "code-action-2" => lsp::CodeAction {
12346 kind: Some("code-action-2".into()),
12347 edit: Some(lsp::WorkspaceEdit::new(
12348 [(
12349 uri,
12350 vec![lsp::TextEdit::new(
12351 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12352 "applied-code-action-2-edit\n".to_string(),
12353 )],
12354 )]
12355 .into_iter()
12356 .collect(),
12357 )),
12358 ..Default::default()
12359 },
12360 req => panic!("Unexpected code action request: {:?}", req),
12361 };
12362 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12363 code_action,
12364 )]))
12365 },
12366 );
12367
12368 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12369 move |params, _| async move { Ok(params) }
12370 });
12371
12372 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12373 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12374 let fake = fake_server.clone();
12375 let lock = command_lock.clone();
12376 move |params, _| {
12377 assert_eq!(params.command, "the-command-for-code-action-1");
12378 let fake = fake.clone();
12379 let lock = lock.clone();
12380 async move {
12381 lock.lock().await;
12382 fake.server
12383 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12384 label: None,
12385 edit: lsp::WorkspaceEdit {
12386 changes: Some(
12387 [(
12388 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12389 vec![lsp::TextEdit {
12390 range: lsp::Range::new(
12391 lsp::Position::new(0, 0),
12392 lsp::Position::new(0, 0),
12393 ),
12394 new_text: "applied-code-action-1-command\n".into(),
12395 }],
12396 )]
12397 .into_iter()
12398 .collect(),
12399 ),
12400 ..Default::default()
12401 },
12402 })
12403 .await
12404 .into_response()
12405 .unwrap();
12406 Ok(Some(json!(null)))
12407 }
12408 }
12409 });
12410
12411 cx.executor().start_waiting();
12412 editor
12413 .update_in(cx, |editor, window, cx| {
12414 editor.perform_format(
12415 project.clone(),
12416 FormatTrigger::Manual,
12417 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12418 window,
12419 cx,
12420 )
12421 })
12422 .unwrap()
12423 .await;
12424 editor.update(cx, |editor, cx| {
12425 assert_eq!(
12426 editor.text(cx),
12427 r#"
12428 applied-code-action-2-edit
12429 applied-code-action-1-command
12430 applied-code-action-1-edit
12431 applied-formatting
12432 one
12433 two
12434 three
12435 "#
12436 .unindent()
12437 );
12438 });
12439
12440 editor.update_in(cx, |editor, window, cx| {
12441 editor.undo(&Default::default(), window, cx);
12442 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12443 });
12444
12445 // Perform a manual edit while waiting for an LSP command
12446 // that's being run as part of a formatting code action.
12447 let lock_guard = command_lock.lock().await;
12448 let format = editor
12449 .update_in(cx, |editor, window, cx| {
12450 editor.perform_format(
12451 project.clone(),
12452 FormatTrigger::Manual,
12453 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12454 window,
12455 cx,
12456 )
12457 })
12458 .unwrap();
12459 cx.run_until_parked();
12460 editor.update(cx, |editor, cx| {
12461 assert_eq!(
12462 editor.text(cx),
12463 r#"
12464 applied-code-action-1-edit
12465 applied-formatting
12466 one
12467 two
12468 three
12469 "#
12470 .unindent()
12471 );
12472
12473 editor.buffer.update(cx, |buffer, cx| {
12474 let ix = buffer.len(cx);
12475 buffer.edit([(ix..ix, "edited\n")], None, cx);
12476 });
12477 });
12478
12479 // Allow the LSP command to proceed. Because the buffer was edited,
12480 // the second code action will not be run.
12481 drop(lock_guard);
12482 format.await;
12483 editor.update_in(cx, |editor, window, cx| {
12484 assert_eq!(
12485 editor.text(cx),
12486 r#"
12487 applied-code-action-1-command
12488 applied-code-action-1-edit
12489 applied-formatting
12490 one
12491 two
12492 three
12493 edited
12494 "#
12495 .unindent()
12496 );
12497
12498 // The manual edit is undone first, because it is the last thing the user did
12499 // (even though the command completed afterwards).
12500 editor.undo(&Default::default(), window, cx);
12501 assert_eq!(
12502 editor.text(cx),
12503 r#"
12504 applied-code-action-1-command
12505 applied-code-action-1-edit
12506 applied-formatting
12507 one
12508 two
12509 three
12510 "#
12511 .unindent()
12512 );
12513
12514 // All the formatting (including the command, which completed after the manual edit)
12515 // is undone together.
12516 editor.undo(&Default::default(), window, cx);
12517 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12518 });
12519}
12520
12521#[gpui::test]
12522async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12523 init_test(cx, |settings| {
12524 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12525 settings::LanguageServerFormatterSpecifier::Current,
12526 )]))
12527 });
12528
12529 let fs = FakeFs::new(cx.executor());
12530 fs.insert_file(path!("/file.ts"), Default::default()).await;
12531
12532 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12533
12534 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12535 language_registry.add(Arc::new(Language::new(
12536 LanguageConfig {
12537 name: "TypeScript".into(),
12538 matcher: LanguageMatcher {
12539 path_suffixes: vec!["ts".to_string()],
12540 ..Default::default()
12541 },
12542 ..LanguageConfig::default()
12543 },
12544 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12545 )));
12546 update_test_language_settings(cx, |settings| {
12547 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12548 });
12549 let mut fake_servers = language_registry.register_fake_lsp(
12550 "TypeScript",
12551 FakeLspAdapter {
12552 capabilities: lsp::ServerCapabilities {
12553 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12554 ..Default::default()
12555 },
12556 ..Default::default()
12557 },
12558 );
12559
12560 let buffer = project
12561 .update(cx, |project, cx| {
12562 project.open_local_buffer(path!("/file.ts"), cx)
12563 })
12564 .await
12565 .unwrap();
12566
12567 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12568 let (editor, cx) = cx.add_window_view(|window, cx| {
12569 build_editor_with_project(project.clone(), buffer, window, cx)
12570 });
12571 editor.update_in(cx, |editor, window, cx| {
12572 editor.set_text(
12573 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12574 window,
12575 cx,
12576 )
12577 });
12578
12579 cx.executor().start_waiting();
12580 let fake_server = fake_servers.next().await.unwrap();
12581
12582 let format = editor
12583 .update_in(cx, |editor, window, cx| {
12584 editor.perform_code_action_kind(
12585 project.clone(),
12586 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12587 window,
12588 cx,
12589 )
12590 })
12591 .unwrap();
12592 fake_server
12593 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12594 assert_eq!(
12595 params.text_document.uri,
12596 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12597 );
12598 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12599 lsp::CodeAction {
12600 title: "Organize Imports".to_string(),
12601 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12602 edit: Some(lsp::WorkspaceEdit {
12603 changes: Some(
12604 [(
12605 params.text_document.uri.clone(),
12606 vec![lsp::TextEdit::new(
12607 lsp::Range::new(
12608 lsp::Position::new(1, 0),
12609 lsp::Position::new(2, 0),
12610 ),
12611 "".to_string(),
12612 )],
12613 )]
12614 .into_iter()
12615 .collect(),
12616 ),
12617 ..Default::default()
12618 }),
12619 ..Default::default()
12620 },
12621 )]))
12622 })
12623 .next()
12624 .await;
12625 cx.executor().start_waiting();
12626 format.await;
12627 assert_eq!(
12628 editor.update(cx, |editor, cx| editor.text(cx)),
12629 "import { a } from 'module';\n\nconst x = a;\n"
12630 );
12631
12632 editor.update_in(cx, |editor, window, cx| {
12633 editor.set_text(
12634 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12635 window,
12636 cx,
12637 )
12638 });
12639 // Ensure we don't lock if code action hangs.
12640 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12641 move |params, _| async move {
12642 assert_eq!(
12643 params.text_document.uri,
12644 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12645 );
12646 futures::future::pending::<()>().await;
12647 unreachable!()
12648 },
12649 );
12650 let format = editor
12651 .update_in(cx, |editor, window, cx| {
12652 editor.perform_code_action_kind(
12653 project,
12654 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12655 window,
12656 cx,
12657 )
12658 })
12659 .unwrap();
12660 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12661 cx.executor().start_waiting();
12662 format.await;
12663 assert_eq!(
12664 editor.update(cx, |editor, cx| editor.text(cx)),
12665 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12666 );
12667}
12668
12669#[gpui::test]
12670async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12671 init_test(cx, |_| {});
12672
12673 let mut cx = EditorLspTestContext::new_rust(
12674 lsp::ServerCapabilities {
12675 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12676 ..Default::default()
12677 },
12678 cx,
12679 )
12680 .await;
12681
12682 cx.set_state(indoc! {"
12683 one.twoˇ
12684 "});
12685
12686 // The format request takes a long time. When it completes, it inserts
12687 // a newline and an indent before the `.`
12688 cx.lsp
12689 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12690 let executor = cx.background_executor().clone();
12691 async move {
12692 executor.timer(Duration::from_millis(100)).await;
12693 Ok(Some(vec![lsp::TextEdit {
12694 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12695 new_text: "\n ".into(),
12696 }]))
12697 }
12698 });
12699
12700 // Submit a format request.
12701 let format_1 = cx
12702 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12703 .unwrap();
12704 cx.executor().run_until_parked();
12705
12706 // Submit a second format request.
12707 let format_2 = cx
12708 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12709 .unwrap();
12710 cx.executor().run_until_parked();
12711
12712 // Wait for both format requests to complete
12713 cx.executor().advance_clock(Duration::from_millis(200));
12714 cx.executor().start_waiting();
12715 format_1.await.unwrap();
12716 cx.executor().start_waiting();
12717 format_2.await.unwrap();
12718
12719 // The formatting edits only happens once.
12720 cx.assert_editor_state(indoc! {"
12721 one
12722 .twoˇ
12723 "});
12724}
12725
12726#[gpui::test]
12727async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12728 init_test(cx, |settings| {
12729 settings.defaults.formatter = Some(FormatterList::default())
12730 });
12731
12732 let mut cx = EditorLspTestContext::new_rust(
12733 lsp::ServerCapabilities {
12734 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12735 ..Default::default()
12736 },
12737 cx,
12738 )
12739 .await;
12740
12741 // Record which buffer changes have been sent to the language server
12742 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12743 cx.lsp
12744 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12745 let buffer_changes = buffer_changes.clone();
12746 move |params, _| {
12747 buffer_changes.lock().extend(
12748 params
12749 .content_changes
12750 .into_iter()
12751 .map(|e| (e.range.unwrap(), e.text)),
12752 );
12753 }
12754 });
12755 // Handle formatting requests to the language server.
12756 cx.lsp
12757 .set_request_handler::<lsp::request::Formatting, _, _>({
12758 let buffer_changes = buffer_changes.clone();
12759 move |_, _| {
12760 let buffer_changes = buffer_changes.clone();
12761 // Insert blank lines between each line of the buffer.
12762 async move {
12763 // When formatting is requested, trailing whitespace has already been stripped,
12764 // and the trailing newline has already been added.
12765 assert_eq!(
12766 &buffer_changes.lock()[1..],
12767 &[
12768 (
12769 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12770 "".into()
12771 ),
12772 (
12773 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12774 "".into()
12775 ),
12776 (
12777 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12778 "\n".into()
12779 ),
12780 ]
12781 );
12782
12783 Ok(Some(vec![
12784 lsp::TextEdit {
12785 range: lsp::Range::new(
12786 lsp::Position::new(1, 0),
12787 lsp::Position::new(1, 0),
12788 ),
12789 new_text: "\n".into(),
12790 },
12791 lsp::TextEdit {
12792 range: lsp::Range::new(
12793 lsp::Position::new(2, 0),
12794 lsp::Position::new(2, 0),
12795 ),
12796 new_text: "\n".into(),
12797 },
12798 ]))
12799 }
12800 }
12801 });
12802
12803 // Set up a buffer white some trailing whitespace and no trailing newline.
12804 cx.set_state(
12805 &[
12806 "one ", //
12807 "twoˇ", //
12808 "three ", //
12809 "four", //
12810 ]
12811 .join("\n"),
12812 );
12813 cx.run_until_parked();
12814
12815 // Submit a format request.
12816 let format = cx
12817 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12818 .unwrap();
12819
12820 cx.run_until_parked();
12821 // After formatting the buffer, the trailing whitespace is stripped,
12822 // a newline is appended, and the edits provided by the language server
12823 // have been applied.
12824 format.await.unwrap();
12825
12826 cx.assert_editor_state(
12827 &[
12828 "one", //
12829 "", //
12830 "twoˇ", //
12831 "", //
12832 "three", //
12833 "four", //
12834 "", //
12835 ]
12836 .join("\n"),
12837 );
12838
12839 // Undoing the formatting undoes the trailing whitespace removal, the
12840 // trailing newline, and the LSP edits.
12841 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12842 cx.assert_editor_state(
12843 &[
12844 "one ", //
12845 "twoˇ", //
12846 "three ", //
12847 "four", //
12848 ]
12849 .join("\n"),
12850 );
12851}
12852
12853#[gpui::test]
12854async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12855 cx: &mut TestAppContext,
12856) {
12857 init_test(cx, |_| {});
12858
12859 cx.update(|cx| {
12860 cx.update_global::<SettingsStore, _>(|settings, cx| {
12861 settings.update_user_settings(cx, |settings| {
12862 settings.editor.auto_signature_help = Some(true);
12863 });
12864 });
12865 });
12866
12867 let mut cx = EditorLspTestContext::new_rust(
12868 lsp::ServerCapabilities {
12869 signature_help_provider: Some(lsp::SignatureHelpOptions {
12870 ..Default::default()
12871 }),
12872 ..Default::default()
12873 },
12874 cx,
12875 )
12876 .await;
12877
12878 let language = Language::new(
12879 LanguageConfig {
12880 name: "Rust".into(),
12881 brackets: BracketPairConfig {
12882 pairs: vec![
12883 BracketPair {
12884 start: "{".to_string(),
12885 end: "}".to_string(),
12886 close: true,
12887 surround: true,
12888 newline: true,
12889 },
12890 BracketPair {
12891 start: "(".to_string(),
12892 end: ")".to_string(),
12893 close: true,
12894 surround: true,
12895 newline: true,
12896 },
12897 BracketPair {
12898 start: "/*".to_string(),
12899 end: " */".to_string(),
12900 close: true,
12901 surround: true,
12902 newline: true,
12903 },
12904 BracketPair {
12905 start: "[".to_string(),
12906 end: "]".to_string(),
12907 close: false,
12908 surround: false,
12909 newline: true,
12910 },
12911 BracketPair {
12912 start: "\"".to_string(),
12913 end: "\"".to_string(),
12914 close: true,
12915 surround: true,
12916 newline: false,
12917 },
12918 BracketPair {
12919 start: "<".to_string(),
12920 end: ">".to_string(),
12921 close: false,
12922 surround: true,
12923 newline: true,
12924 },
12925 ],
12926 ..Default::default()
12927 },
12928 autoclose_before: "})]".to_string(),
12929 ..Default::default()
12930 },
12931 Some(tree_sitter_rust::LANGUAGE.into()),
12932 );
12933 let language = Arc::new(language);
12934
12935 cx.language_registry().add(language.clone());
12936 cx.update_buffer(|buffer, cx| {
12937 buffer.set_language(Some(language), cx);
12938 });
12939
12940 cx.set_state(
12941 &r#"
12942 fn main() {
12943 sampleˇ
12944 }
12945 "#
12946 .unindent(),
12947 );
12948
12949 cx.update_editor(|editor, window, cx| {
12950 editor.handle_input("(", window, cx);
12951 });
12952 cx.assert_editor_state(
12953 &"
12954 fn main() {
12955 sample(ˇ)
12956 }
12957 "
12958 .unindent(),
12959 );
12960
12961 let mocked_response = lsp::SignatureHelp {
12962 signatures: vec![lsp::SignatureInformation {
12963 label: "fn sample(param1: u8, param2: u8)".to_string(),
12964 documentation: None,
12965 parameters: Some(vec![
12966 lsp::ParameterInformation {
12967 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12968 documentation: None,
12969 },
12970 lsp::ParameterInformation {
12971 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12972 documentation: None,
12973 },
12974 ]),
12975 active_parameter: None,
12976 }],
12977 active_signature: Some(0),
12978 active_parameter: Some(0),
12979 };
12980 handle_signature_help_request(&mut cx, mocked_response).await;
12981
12982 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12983 .await;
12984
12985 cx.editor(|editor, _, _| {
12986 let signature_help_state = editor.signature_help_state.popover().cloned();
12987 let signature = signature_help_state.unwrap();
12988 assert_eq!(
12989 signature.signatures[signature.current_signature].label,
12990 "fn sample(param1: u8, param2: u8)"
12991 );
12992 });
12993}
12994
12995#[gpui::test]
12996async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12997 init_test(cx, |_| {});
12998
12999 cx.update(|cx| {
13000 cx.update_global::<SettingsStore, _>(|settings, cx| {
13001 settings.update_user_settings(cx, |settings| {
13002 settings.editor.auto_signature_help = Some(false);
13003 settings.editor.show_signature_help_after_edits = Some(false);
13004 });
13005 });
13006 });
13007
13008 let mut cx = EditorLspTestContext::new_rust(
13009 lsp::ServerCapabilities {
13010 signature_help_provider: Some(lsp::SignatureHelpOptions {
13011 ..Default::default()
13012 }),
13013 ..Default::default()
13014 },
13015 cx,
13016 )
13017 .await;
13018
13019 let language = Language::new(
13020 LanguageConfig {
13021 name: "Rust".into(),
13022 brackets: BracketPairConfig {
13023 pairs: vec![
13024 BracketPair {
13025 start: "{".to_string(),
13026 end: "}".to_string(),
13027 close: true,
13028 surround: true,
13029 newline: true,
13030 },
13031 BracketPair {
13032 start: "(".to_string(),
13033 end: ")".to_string(),
13034 close: true,
13035 surround: true,
13036 newline: true,
13037 },
13038 BracketPair {
13039 start: "/*".to_string(),
13040 end: " */".to_string(),
13041 close: true,
13042 surround: true,
13043 newline: true,
13044 },
13045 BracketPair {
13046 start: "[".to_string(),
13047 end: "]".to_string(),
13048 close: false,
13049 surround: false,
13050 newline: true,
13051 },
13052 BracketPair {
13053 start: "\"".to_string(),
13054 end: "\"".to_string(),
13055 close: true,
13056 surround: true,
13057 newline: false,
13058 },
13059 BracketPair {
13060 start: "<".to_string(),
13061 end: ">".to_string(),
13062 close: false,
13063 surround: true,
13064 newline: true,
13065 },
13066 ],
13067 ..Default::default()
13068 },
13069 autoclose_before: "})]".to_string(),
13070 ..Default::default()
13071 },
13072 Some(tree_sitter_rust::LANGUAGE.into()),
13073 );
13074 let language = Arc::new(language);
13075
13076 cx.language_registry().add(language.clone());
13077 cx.update_buffer(|buffer, cx| {
13078 buffer.set_language(Some(language), cx);
13079 });
13080
13081 // Ensure that signature_help is not called when no signature help is enabled.
13082 cx.set_state(
13083 &r#"
13084 fn main() {
13085 sampleˇ
13086 }
13087 "#
13088 .unindent(),
13089 );
13090 cx.update_editor(|editor, window, cx| {
13091 editor.handle_input("(", window, cx);
13092 });
13093 cx.assert_editor_state(
13094 &"
13095 fn main() {
13096 sample(ˇ)
13097 }
13098 "
13099 .unindent(),
13100 );
13101 cx.editor(|editor, _, _| {
13102 assert!(editor.signature_help_state.task().is_none());
13103 });
13104
13105 let mocked_response = lsp::SignatureHelp {
13106 signatures: vec![lsp::SignatureInformation {
13107 label: "fn sample(param1: u8, param2: u8)".to_string(),
13108 documentation: None,
13109 parameters: Some(vec![
13110 lsp::ParameterInformation {
13111 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13112 documentation: None,
13113 },
13114 lsp::ParameterInformation {
13115 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13116 documentation: None,
13117 },
13118 ]),
13119 active_parameter: None,
13120 }],
13121 active_signature: Some(0),
13122 active_parameter: Some(0),
13123 };
13124
13125 // Ensure that signature_help is called when enabled afte edits
13126 cx.update(|_, cx| {
13127 cx.update_global::<SettingsStore, _>(|settings, cx| {
13128 settings.update_user_settings(cx, |settings| {
13129 settings.editor.auto_signature_help = Some(false);
13130 settings.editor.show_signature_help_after_edits = Some(true);
13131 });
13132 });
13133 });
13134 cx.set_state(
13135 &r#"
13136 fn main() {
13137 sampleˇ
13138 }
13139 "#
13140 .unindent(),
13141 );
13142 cx.update_editor(|editor, window, cx| {
13143 editor.handle_input("(", window, cx);
13144 });
13145 cx.assert_editor_state(
13146 &"
13147 fn main() {
13148 sample(ˇ)
13149 }
13150 "
13151 .unindent(),
13152 );
13153 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13154 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13155 .await;
13156 cx.update_editor(|editor, _, _| {
13157 let signature_help_state = editor.signature_help_state.popover().cloned();
13158 assert!(signature_help_state.is_some());
13159 let signature = signature_help_state.unwrap();
13160 assert_eq!(
13161 signature.signatures[signature.current_signature].label,
13162 "fn sample(param1: u8, param2: u8)"
13163 );
13164 editor.signature_help_state = SignatureHelpState::default();
13165 });
13166
13167 // Ensure that signature_help is called when auto signature help override is enabled
13168 cx.update(|_, cx| {
13169 cx.update_global::<SettingsStore, _>(|settings, cx| {
13170 settings.update_user_settings(cx, |settings| {
13171 settings.editor.auto_signature_help = Some(true);
13172 settings.editor.show_signature_help_after_edits = Some(false);
13173 });
13174 });
13175 });
13176 cx.set_state(
13177 &r#"
13178 fn main() {
13179 sampleˇ
13180 }
13181 "#
13182 .unindent(),
13183 );
13184 cx.update_editor(|editor, window, cx| {
13185 editor.handle_input("(", window, cx);
13186 });
13187 cx.assert_editor_state(
13188 &"
13189 fn main() {
13190 sample(ˇ)
13191 }
13192 "
13193 .unindent(),
13194 );
13195 handle_signature_help_request(&mut cx, mocked_response).await;
13196 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13197 .await;
13198 cx.editor(|editor, _, _| {
13199 let signature_help_state = editor.signature_help_state.popover().cloned();
13200 assert!(signature_help_state.is_some());
13201 let signature = signature_help_state.unwrap();
13202 assert_eq!(
13203 signature.signatures[signature.current_signature].label,
13204 "fn sample(param1: u8, param2: u8)"
13205 );
13206 });
13207}
13208
13209#[gpui::test]
13210async fn test_signature_help(cx: &mut TestAppContext) {
13211 init_test(cx, |_| {});
13212 cx.update(|cx| {
13213 cx.update_global::<SettingsStore, _>(|settings, cx| {
13214 settings.update_user_settings(cx, |settings| {
13215 settings.editor.auto_signature_help = Some(true);
13216 });
13217 });
13218 });
13219
13220 let mut cx = EditorLspTestContext::new_rust(
13221 lsp::ServerCapabilities {
13222 signature_help_provider: Some(lsp::SignatureHelpOptions {
13223 ..Default::default()
13224 }),
13225 ..Default::default()
13226 },
13227 cx,
13228 )
13229 .await;
13230
13231 // A test that directly calls `show_signature_help`
13232 cx.update_editor(|editor, window, cx| {
13233 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13234 });
13235
13236 let mocked_response = lsp::SignatureHelp {
13237 signatures: vec![lsp::SignatureInformation {
13238 label: "fn sample(param1: u8, param2: u8)".to_string(),
13239 documentation: None,
13240 parameters: Some(vec![
13241 lsp::ParameterInformation {
13242 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13243 documentation: None,
13244 },
13245 lsp::ParameterInformation {
13246 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13247 documentation: None,
13248 },
13249 ]),
13250 active_parameter: None,
13251 }],
13252 active_signature: Some(0),
13253 active_parameter: Some(0),
13254 };
13255 handle_signature_help_request(&mut cx, mocked_response).await;
13256
13257 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13258 .await;
13259
13260 cx.editor(|editor, _, _| {
13261 let signature_help_state = editor.signature_help_state.popover().cloned();
13262 assert!(signature_help_state.is_some());
13263 let signature = signature_help_state.unwrap();
13264 assert_eq!(
13265 signature.signatures[signature.current_signature].label,
13266 "fn sample(param1: u8, param2: u8)"
13267 );
13268 });
13269
13270 // When exiting outside from inside the brackets, `signature_help` is closed.
13271 cx.set_state(indoc! {"
13272 fn main() {
13273 sample(ˇ);
13274 }
13275
13276 fn sample(param1: u8, param2: u8) {}
13277 "});
13278
13279 cx.update_editor(|editor, window, cx| {
13280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13281 s.select_ranges([0..0])
13282 });
13283 });
13284
13285 let mocked_response = lsp::SignatureHelp {
13286 signatures: Vec::new(),
13287 active_signature: None,
13288 active_parameter: None,
13289 };
13290 handle_signature_help_request(&mut cx, mocked_response).await;
13291
13292 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13293 .await;
13294
13295 cx.editor(|editor, _, _| {
13296 assert!(!editor.signature_help_state.is_shown());
13297 });
13298
13299 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13300 cx.set_state(indoc! {"
13301 fn main() {
13302 sample(ˇ);
13303 }
13304
13305 fn sample(param1: u8, param2: u8) {}
13306 "});
13307
13308 let mocked_response = lsp::SignatureHelp {
13309 signatures: vec![lsp::SignatureInformation {
13310 label: "fn sample(param1: u8, param2: u8)".to_string(),
13311 documentation: None,
13312 parameters: Some(vec![
13313 lsp::ParameterInformation {
13314 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13315 documentation: None,
13316 },
13317 lsp::ParameterInformation {
13318 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13319 documentation: None,
13320 },
13321 ]),
13322 active_parameter: None,
13323 }],
13324 active_signature: Some(0),
13325 active_parameter: Some(0),
13326 };
13327 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13328 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13329 .await;
13330 cx.editor(|editor, _, _| {
13331 assert!(editor.signature_help_state.is_shown());
13332 });
13333
13334 // Restore the popover with more parameter input
13335 cx.set_state(indoc! {"
13336 fn main() {
13337 sample(param1, param2ˇ);
13338 }
13339
13340 fn sample(param1: u8, param2: u8) {}
13341 "});
13342
13343 let mocked_response = lsp::SignatureHelp {
13344 signatures: vec![lsp::SignatureInformation {
13345 label: "fn sample(param1: u8, param2: u8)".to_string(),
13346 documentation: None,
13347 parameters: Some(vec![
13348 lsp::ParameterInformation {
13349 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13350 documentation: None,
13351 },
13352 lsp::ParameterInformation {
13353 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13354 documentation: None,
13355 },
13356 ]),
13357 active_parameter: None,
13358 }],
13359 active_signature: Some(0),
13360 active_parameter: Some(1),
13361 };
13362 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13363 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13364 .await;
13365
13366 // When selecting a range, the popover is gone.
13367 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13368 cx.update_editor(|editor, window, cx| {
13369 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13370 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13371 })
13372 });
13373 cx.assert_editor_state(indoc! {"
13374 fn main() {
13375 sample(param1, «ˇparam2»);
13376 }
13377
13378 fn sample(param1: u8, param2: u8) {}
13379 "});
13380 cx.editor(|editor, _, _| {
13381 assert!(!editor.signature_help_state.is_shown());
13382 });
13383
13384 // When unselecting again, the popover is back if within the brackets.
13385 cx.update_editor(|editor, window, cx| {
13386 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13387 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13388 })
13389 });
13390 cx.assert_editor_state(indoc! {"
13391 fn main() {
13392 sample(param1, ˇparam2);
13393 }
13394
13395 fn sample(param1: u8, param2: u8) {}
13396 "});
13397 handle_signature_help_request(&mut cx, mocked_response).await;
13398 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13399 .await;
13400 cx.editor(|editor, _, _| {
13401 assert!(editor.signature_help_state.is_shown());
13402 });
13403
13404 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13405 cx.update_editor(|editor, window, cx| {
13406 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13407 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13408 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13409 })
13410 });
13411 cx.assert_editor_state(indoc! {"
13412 fn main() {
13413 sample(param1, ˇparam2);
13414 }
13415
13416 fn sample(param1: u8, param2: u8) {}
13417 "});
13418
13419 let mocked_response = lsp::SignatureHelp {
13420 signatures: vec![lsp::SignatureInformation {
13421 label: "fn sample(param1: u8, param2: u8)".to_string(),
13422 documentation: None,
13423 parameters: Some(vec![
13424 lsp::ParameterInformation {
13425 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13426 documentation: None,
13427 },
13428 lsp::ParameterInformation {
13429 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13430 documentation: None,
13431 },
13432 ]),
13433 active_parameter: None,
13434 }],
13435 active_signature: Some(0),
13436 active_parameter: Some(1),
13437 };
13438 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13439 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13440 .await;
13441 cx.update_editor(|editor, _, cx| {
13442 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13443 });
13444 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13445 .await;
13446 cx.update_editor(|editor, window, cx| {
13447 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13448 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13449 })
13450 });
13451 cx.assert_editor_state(indoc! {"
13452 fn main() {
13453 sample(param1, «ˇparam2»);
13454 }
13455
13456 fn sample(param1: u8, param2: u8) {}
13457 "});
13458 cx.update_editor(|editor, window, cx| {
13459 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13460 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13461 })
13462 });
13463 cx.assert_editor_state(indoc! {"
13464 fn main() {
13465 sample(param1, ˇparam2);
13466 }
13467
13468 fn sample(param1: u8, param2: u8) {}
13469 "});
13470 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13471 .await;
13472}
13473
13474#[gpui::test]
13475async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13476 init_test(cx, |_| {});
13477
13478 let mut cx = EditorLspTestContext::new_rust(
13479 lsp::ServerCapabilities {
13480 signature_help_provider: Some(lsp::SignatureHelpOptions {
13481 ..Default::default()
13482 }),
13483 ..Default::default()
13484 },
13485 cx,
13486 )
13487 .await;
13488
13489 cx.set_state(indoc! {"
13490 fn main() {
13491 overloadedˇ
13492 }
13493 "});
13494
13495 cx.update_editor(|editor, window, cx| {
13496 editor.handle_input("(", window, cx);
13497 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13498 });
13499
13500 // Mock response with 3 signatures
13501 let mocked_response = lsp::SignatureHelp {
13502 signatures: vec![
13503 lsp::SignatureInformation {
13504 label: "fn overloaded(x: i32)".to_string(),
13505 documentation: None,
13506 parameters: Some(vec![lsp::ParameterInformation {
13507 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13508 documentation: None,
13509 }]),
13510 active_parameter: None,
13511 },
13512 lsp::SignatureInformation {
13513 label: "fn overloaded(x: i32, y: i32)".to_string(),
13514 documentation: None,
13515 parameters: Some(vec![
13516 lsp::ParameterInformation {
13517 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13518 documentation: None,
13519 },
13520 lsp::ParameterInformation {
13521 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13522 documentation: None,
13523 },
13524 ]),
13525 active_parameter: None,
13526 },
13527 lsp::SignatureInformation {
13528 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13529 documentation: None,
13530 parameters: Some(vec![
13531 lsp::ParameterInformation {
13532 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13533 documentation: None,
13534 },
13535 lsp::ParameterInformation {
13536 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13537 documentation: None,
13538 },
13539 lsp::ParameterInformation {
13540 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13541 documentation: None,
13542 },
13543 ]),
13544 active_parameter: None,
13545 },
13546 ],
13547 active_signature: Some(1),
13548 active_parameter: Some(0),
13549 };
13550 handle_signature_help_request(&mut cx, mocked_response).await;
13551
13552 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13553 .await;
13554
13555 // Verify we have multiple signatures and the right one is selected
13556 cx.editor(|editor, _, _| {
13557 let popover = editor.signature_help_state.popover().cloned().unwrap();
13558 assert_eq!(popover.signatures.len(), 3);
13559 // active_signature was 1, so that should be the current
13560 assert_eq!(popover.current_signature, 1);
13561 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13562 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13563 assert_eq!(
13564 popover.signatures[2].label,
13565 "fn overloaded(x: i32, y: i32, z: i32)"
13566 );
13567 });
13568
13569 // Test navigation functionality
13570 cx.update_editor(|editor, window, cx| {
13571 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13572 });
13573
13574 cx.editor(|editor, _, _| {
13575 let popover = editor.signature_help_state.popover().cloned().unwrap();
13576 assert_eq!(popover.current_signature, 2);
13577 });
13578
13579 // Test wrap around
13580 cx.update_editor(|editor, window, cx| {
13581 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13582 });
13583
13584 cx.editor(|editor, _, _| {
13585 let popover = editor.signature_help_state.popover().cloned().unwrap();
13586 assert_eq!(popover.current_signature, 0);
13587 });
13588
13589 // Test previous navigation
13590 cx.update_editor(|editor, window, cx| {
13591 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13592 });
13593
13594 cx.editor(|editor, _, _| {
13595 let popover = editor.signature_help_state.popover().cloned().unwrap();
13596 assert_eq!(popover.current_signature, 2);
13597 });
13598}
13599
13600#[gpui::test]
13601async fn test_completion_mode(cx: &mut TestAppContext) {
13602 init_test(cx, |_| {});
13603 let mut cx = EditorLspTestContext::new_rust(
13604 lsp::ServerCapabilities {
13605 completion_provider: Some(lsp::CompletionOptions {
13606 resolve_provider: Some(true),
13607 ..Default::default()
13608 }),
13609 ..Default::default()
13610 },
13611 cx,
13612 )
13613 .await;
13614
13615 struct Run {
13616 run_description: &'static str,
13617 initial_state: String,
13618 buffer_marked_text: String,
13619 completion_label: &'static str,
13620 completion_text: &'static str,
13621 expected_with_insert_mode: String,
13622 expected_with_replace_mode: String,
13623 expected_with_replace_subsequence_mode: String,
13624 expected_with_replace_suffix_mode: String,
13625 }
13626
13627 let runs = [
13628 Run {
13629 run_description: "Start of word matches completion text",
13630 initial_state: "before ediˇ after".into(),
13631 buffer_marked_text: "before <edi|> after".into(),
13632 completion_label: "editor",
13633 completion_text: "editor",
13634 expected_with_insert_mode: "before editorˇ after".into(),
13635 expected_with_replace_mode: "before editorˇ after".into(),
13636 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13637 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13638 },
13639 Run {
13640 run_description: "Accept same text at the middle of the word",
13641 initial_state: "before ediˇtor after".into(),
13642 buffer_marked_text: "before <edi|tor> after".into(),
13643 completion_label: "editor",
13644 completion_text: "editor",
13645 expected_with_insert_mode: "before editorˇtor after".into(),
13646 expected_with_replace_mode: "before editorˇ after".into(),
13647 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13648 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13649 },
13650 Run {
13651 run_description: "End of word matches completion text -- cursor at end",
13652 initial_state: "before torˇ after".into(),
13653 buffer_marked_text: "before <tor|> after".into(),
13654 completion_label: "editor",
13655 completion_text: "editor",
13656 expected_with_insert_mode: "before editorˇ after".into(),
13657 expected_with_replace_mode: "before editorˇ after".into(),
13658 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13659 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13660 },
13661 Run {
13662 run_description: "End of word matches completion text -- cursor at start",
13663 initial_state: "before ˇtor after".into(),
13664 buffer_marked_text: "before <|tor> after".into(),
13665 completion_label: "editor",
13666 completion_text: "editor",
13667 expected_with_insert_mode: "before editorˇtor after".into(),
13668 expected_with_replace_mode: "before editorˇ after".into(),
13669 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13670 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13671 },
13672 Run {
13673 run_description: "Prepend text containing whitespace",
13674 initial_state: "pˇfield: bool".into(),
13675 buffer_marked_text: "<p|field>: bool".into(),
13676 completion_label: "pub ",
13677 completion_text: "pub ",
13678 expected_with_insert_mode: "pub ˇfield: bool".into(),
13679 expected_with_replace_mode: "pub ˇ: bool".into(),
13680 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13681 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13682 },
13683 Run {
13684 run_description: "Add element to start of list",
13685 initial_state: "[element_ˇelement_2]".into(),
13686 buffer_marked_text: "[<element_|element_2>]".into(),
13687 completion_label: "element_1",
13688 completion_text: "element_1",
13689 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13690 expected_with_replace_mode: "[element_1ˇ]".into(),
13691 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13692 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13693 },
13694 Run {
13695 run_description: "Add element to start of list -- first and second elements are equal",
13696 initial_state: "[elˇelement]".into(),
13697 buffer_marked_text: "[<el|element>]".into(),
13698 completion_label: "element",
13699 completion_text: "element",
13700 expected_with_insert_mode: "[elementˇelement]".into(),
13701 expected_with_replace_mode: "[elementˇ]".into(),
13702 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13703 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13704 },
13705 Run {
13706 run_description: "Ends with matching suffix",
13707 initial_state: "SubˇError".into(),
13708 buffer_marked_text: "<Sub|Error>".into(),
13709 completion_label: "SubscriptionError",
13710 completion_text: "SubscriptionError",
13711 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13712 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13713 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13714 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13715 },
13716 Run {
13717 run_description: "Suffix is a subsequence -- contiguous",
13718 initial_state: "SubˇErr".into(),
13719 buffer_marked_text: "<Sub|Err>".into(),
13720 completion_label: "SubscriptionError",
13721 completion_text: "SubscriptionError",
13722 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13723 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13724 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13725 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13726 },
13727 Run {
13728 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13729 initial_state: "Suˇscrirr".into(),
13730 buffer_marked_text: "<Su|scrirr>".into(),
13731 completion_label: "SubscriptionError",
13732 completion_text: "SubscriptionError",
13733 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13734 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13735 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13736 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13737 },
13738 Run {
13739 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13740 initial_state: "foo(indˇix)".into(),
13741 buffer_marked_text: "foo(<ind|ix>)".into(),
13742 completion_label: "node_index",
13743 completion_text: "node_index",
13744 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13745 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13746 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13747 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13748 },
13749 Run {
13750 run_description: "Replace range ends before cursor - should extend to cursor",
13751 initial_state: "before editˇo after".into(),
13752 buffer_marked_text: "before <{ed}>it|o after".into(),
13753 completion_label: "editor",
13754 completion_text: "editor",
13755 expected_with_insert_mode: "before editorˇo after".into(),
13756 expected_with_replace_mode: "before editorˇo after".into(),
13757 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13758 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13759 },
13760 Run {
13761 run_description: "Uses label for suffix matching",
13762 initial_state: "before ediˇtor after".into(),
13763 buffer_marked_text: "before <edi|tor> after".into(),
13764 completion_label: "editor",
13765 completion_text: "editor()",
13766 expected_with_insert_mode: "before editor()ˇtor after".into(),
13767 expected_with_replace_mode: "before editor()ˇ after".into(),
13768 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13769 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13770 },
13771 Run {
13772 run_description: "Case insensitive subsequence and suffix matching",
13773 initial_state: "before EDiˇtoR after".into(),
13774 buffer_marked_text: "before <EDi|toR> after".into(),
13775 completion_label: "editor",
13776 completion_text: "editor",
13777 expected_with_insert_mode: "before editorˇtoR after".into(),
13778 expected_with_replace_mode: "before editorˇ after".into(),
13779 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13780 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13781 },
13782 ];
13783
13784 for run in runs {
13785 let run_variations = [
13786 (LspInsertMode::Insert, run.expected_with_insert_mode),
13787 (LspInsertMode::Replace, run.expected_with_replace_mode),
13788 (
13789 LspInsertMode::ReplaceSubsequence,
13790 run.expected_with_replace_subsequence_mode,
13791 ),
13792 (
13793 LspInsertMode::ReplaceSuffix,
13794 run.expected_with_replace_suffix_mode,
13795 ),
13796 ];
13797
13798 for (lsp_insert_mode, expected_text) in run_variations {
13799 eprintln!(
13800 "run = {:?}, mode = {lsp_insert_mode:.?}",
13801 run.run_description,
13802 );
13803
13804 update_test_language_settings(&mut cx, |settings| {
13805 settings.defaults.completions = Some(CompletionSettingsContent {
13806 lsp_insert_mode: Some(lsp_insert_mode),
13807 words: Some(WordsCompletionMode::Disabled),
13808 words_min_length: Some(0),
13809 ..Default::default()
13810 });
13811 });
13812
13813 cx.set_state(&run.initial_state);
13814 cx.update_editor(|editor, window, cx| {
13815 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13816 });
13817
13818 let counter = Arc::new(AtomicUsize::new(0));
13819 handle_completion_request_with_insert_and_replace(
13820 &mut cx,
13821 &run.buffer_marked_text,
13822 vec![(run.completion_label, run.completion_text)],
13823 counter.clone(),
13824 )
13825 .await;
13826 cx.condition(|editor, _| editor.context_menu_visible())
13827 .await;
13828 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13829
13830 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13831 editor
13832 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13833 .unwrap()
13834 });
13835 cx.assert_editor_state(&expected_text);
13836 handle_resolve_completion_request(&mut cx, None).await;
13837 apply_additional_edits.await.unwrap();
13838 }
13839 }
13840}
13841
13842#[gpui::test]
13843async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13844 init_test(cx, |_| {});
13845 let mut cx = EditorLspTestContext::new_rust(
13846 lsp::ServerCapabilities {
13847 completion_provider: Some(lsp::CompletionOptions {
13848 resolve_provider: Some(true),
13849 ..Default::default()
13850 }),
13851 ..Default::default()
13852 },
13853 cx,
13854 )
13855 .await;
13856
13857 let initial_state = "SubˇError";
13858 let buffer_marked_text = "<Sub|Error>";
13859 let completion_text = "SubscriptionError";
13860 let expected_with_insert_mode = "SubscriptionErrorˇError";
13861 let expected_with_replace_mode = "SubscriptionErrorˇ";
13862
13863 update_test_language_settings(&mut cx, |settings| {
13864 settings.defaults.completions = Some(CompletionSettingsContent {
13865 words: Some(WordsCompletionMode::Disabled),
13866 words_min_length: Some(0),
13867 // set the opposite here to ensure that the action is overriding the default behavior
13868 lsp_insert_mode: Some(LspInsertMode::Insert),
13869 ..Default::default()
13870 });
13871 });
13872
13873 cx.set_state(initial_state);
13874 cx.update_editor(|editor, window, cx| {
13875 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13876 });
13877
13878 let counter = Arc::new(AtomicUsize::new(0));
13879 handle_completion_request_with_insert_and_replace(
13880 &mut cx,
13881 buffer_marked_text,
13882 vec![(completion_text, completion_text)],
13883 counter.clone(),
13884 )
13885 .await;
13886 cx.condition(|editor, _| editor.context_menu_visible())
13887 .await;
13888 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13889
13890 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13891 editor
13892 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13893 .unwrap()
13894 });
13895 cx.assert_editor_state(expected_with_replace_mode);
13896 handle_resolve_completion_request(&mut cx, None).await;
13897 apply_additional_edits.await.unwrap();
13898
13899 update_test_language_settings(&mut cx, |settings| {
13900 settings.defaults.completions = Some(CompletionSettingsContent {
13901 words: Some(WordsCompletionMode::Disabled),
13902 words_min_length: Some(0),
13903 // set the opposite here to ensure that the action is overriding the default behavior
13904 lsp_insert_mode: Some(LspInsertMode::Replace),
13905 ..Default::default()
13906 });
13907 });
13908
13909 cx.set_state(initial_state);
13910 cx.update_editor(|editor, window, cx| {
13911 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13912 });
13913 handle_completion_request_with_insert_and_replace(
13914 &mut cx,
13915 buffer_marked_text,
13916 vec![(completion_text, completion_text)],
13917 counter.clone(),
13918 )
13919 .await;
13920 cx.condition(|editor, _| editor.context_menu_visible())
13921 .await;
13922 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13923
13924 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13925 editor
13926 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13927 .unwrap()
13928 });
13929 cx.assert_editor_state(expected_with_insert_mode);
13930 handle_resolve_completion_request(&mut cx, None).await;
13931 apply_additional_edits.await.unwrap();
13932}
13933
13934#[gpui::test]
13935async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13936 init_test(cx, |_| {});
13937 let mut cx = EditorLspTestContext::new_rust(
13938 lsp::ServerCapabilities {
13939 completion_provider: Some(lsp::CompletionOptions {
13940 resolve_provider: Some(true),
13941 ..Default::default()
13942 }),
13943 ..Default::default()
13944 },
13945 cx,
13946 )
13947 .await;
13948
13949 // scenario: surrounding text matches completion text
13950 let completion_text = "to_offset";
13951 let initial_state = indoc! {"
13952 1. buf.to_offˇsuffix
13953 2. buf.to_offˇsuf
13954 3. buf.to_offˇfix
13955 4. buf.to_offˇ
13956 5. into_offˇensive
13957 6. ˇsuffix
13958 7. let ˇ //
13959 8. aaˇzz
13960 9. buf.to_off«zzzzzˇ»suffix
13961 10. buf.«ˇzzzzz»suffix
13962 11. to_off«ˇzzzzz»
13963
13964 buf.to_offˇsuffix // newest cursor
13965 "};
13966 let completion_marked_buffer = indoc! {"
13967 1. buf.to_offsuffix
13968 2. buf.to_offsuf
13969 3. buf.to_offfix
13970 4. buf.to_off
13971 5. into_offensive
13972 6. suffix
13973 7. let //
13974 8. aazz
13975 9. buf.to_offzzzzzsuffix
13976 10. buf.zzzzzsuffix
13977 11. to_offzzzzz
13978
13979 buf.<to_off|suffix> // newest cursor
13980 "};
13981 let expected = indoc! {"
13982 1. buf.to_offsetˇ
13983 2. buf.to_offsetˇsuf
13984 3. buf.to_offsetˇfix
13985 4. buf.to_offsetˇ
13986 5. into_offsetˇensive
13987 6. to_offsetˇsuffix
13988 7. let to_offsetˇ //
13989 8. aato_offsetˇzz
13990 9. buf.to_offsetˇ
13991 10. buf.to_offsetˇsuffix
13992 11. to_offsetˇ
13993
13994 buf.to_offsetˇ // newest cursor
13995 "};
13996 cx.set_state(initial_state);
13997 cx.update_editor(|editor, window, cx| {
13998 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13999 });
14000 handle_completion_request_with_insert_and_replace(
14001 &mut cx,
14002 completion_marked_buffer,
14003 vec![(completion_text, completion_text)],
14004 Arc::new(AtomicUsize::new(0)),
14005 )
14006 .await;
14007 cx.condition(|editor, _| editor.context_menu_visible())
14008 .await;
14009 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14010 editor
14011 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14012 .unwrap()
14013 });
14014 cx.assert_editor_state(expected);
14015 handle_resolve_completion_request(&mut cx, None).await;
14016 apply_additional_edits.await.unwrap();
14017
14018 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14019 let completion_text = "foo_and_bar";
14020 let initial_state = indoc! {"
14021 1. ooanbˇ
14022 2. zooanbˇ
14023 3. ooanbˇz
14024 4. zooanbˇz
14025 5. ooanˇ
14026 6. oanbˇ
14027
14028 ooanbˇ
14029 "};
14030 let completion_marked_buffer = indoc! {"
14031 1. ooanb
14032 2. zooanb
14033 3. ooanbz
14034 4. zooanbz
14035 5. ooan
14036 6. oanb
14037
14038 <ooanb|>
14039 "};
14040 let expected = indoc! {"
14041 1. foo_and_barˇ
14042 2. zfoo_and_barˇ
14043 3. foo_and_barˇz
14044 4. zfoo_and_barˇz
14045 5. ooanfoo_and_barˇ
14046 6. oanbfoo_and_barˇ
14047
14048 foo_and_barˇ
14049 "};
14050 cx.set_state(initial_state);
14051 cx.update_editor(|editor, window, cx| {
14052 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14053 });
14054 handle_completion_request_with_insert_and_replace(
14055 &mut cx,
14056 completion_marked_buffer,
14057 vec![(completion_text, completion_text)],
14058 Arc::new(AtomicUsize::new(0)),
14059 )
14060 .await;
14061 cx.condition(|editor, _| editor.context_menu_visible())
14062 .await;
14063 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14064 editor
14065 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14066 .unwrap()
14067 });
14068 cx.assert_editor_state(expected);
14069 handle_resolve_completion_request(&mut cx, None).await;
14070 apply_additional_edits.await.unwrap();
14071
14072 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14073 // (expects the same as if it was inserted at the end)
14074 let completion_text = "foo_and_bar";
14075 let initial_state = indoc! {"
14076 1. ooˇanb
14077 2. zooˇanb
14078 3. ooˇanbz
14079 4. zooˇanbz
14080
14081 ooˇanb
14082 "};
14083 let completion_marked_buffer = indoc! {"
14084 1. ooanb
14085 2. zooanb
14086 3. ooanbz
14087 4. zooanbz
14088
14089 <oo|anb>
14090 "};
14091 let expected = indoc! {"
14092 1. foo_and_barˇ
14093 2. zfoo_and_barˇ
14094 3. foo_and_barˇz
14095 4. zfoo_and_barˇz
14096
14097 foo_and_barˇ
14098 "};
14099 cx.set_state(initial_state);
14100 cx.update_editor(|editor, window, cx| {
14101 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14102 });
14103 handle_completion_request_with_insert_and_replace(
14104 &mut cx,
14105 completion_marked_buffer,
14106 vec![(completion_text, completion_text)],
14107 Arc::new(AtomicUsize::new(0)),
14108 )
14109 .await;
14110 cx.condition(|editor, _| editor.context_menu_visible())
14111 .await;
14112 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14113 editor
14114 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14115 .unwrap()
14116 });
14117 cx.assert_editor_state(expected);
14118 handle_resolve_completion_request(&mut cx, None).await;
14119 apply_additional_edits.await.unwrap();
14120}
14121
14122// This used to crash
14123#[gpui::test]
14124async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14125 init_test(cx, |_| {});
14126
14127 let buffer_text = indoc! {"
14128 fn main() {
14129 10.satu;
14130
14131 //
14132 // separate cursors so they open in different excerpts (manually reproducible)
14133 //
14134
14135 10.satu20;
14136 }
14137 "};
14138 let multibuffer_text_with_selections = indoc! {"
14139 fn main() {
14140 10.satuˇ;
14141
14142 //
14143
14144 //
14145
14146 10.satuˇ20;
14147 }
14148 "};
14149 let expected_multibuffer = indoc! {"
14150 fn main() {
14151 10.saturating_sub()ˇ;
14152
14153 //
14154
14155 //
14156
14157 10.saturating_sub()ˇ;
14158 }
14159 "};
14160
14161 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14162 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14163
14164 let fs = FakeFs::new(cx.executor());
14165 fs.insert_tree(
14166 path!("/a"),
14167 json!({
14168 "main.rs": buffer_text,
14169 }),
14170 )
14171 .await;
14172
14173 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14174 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14175 language_registry.add(rust_lang());
14176 let mut fake_servers = language_registry.register_fake_lsp(
14177 "Rust",
14178 FakeLspAdapter {
14179 capabilities: lsp::ServerCapabilities {
14180 completion_provider: Some(lsp::CompletionOptions {
14181 resolve_provider: None,
14182 ..lsp::CompletionOptions::default()
14183 }),
14184 ..lsp::ServerCapabilities::default()
14185 },
14186 ..FakeLspAdapter::default()
14187 },
14188 );
14189 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14190 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14191 let buffer = project
14192 .update(cx, |project, cx| {
14193 project.open_local_buffer(path!("/a/main.rs"), cx)
14194 })
14195 .await
14196 .unwrap();
14197
14198 let multi_buffer = cx.new(|cx| {
14199 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14200 multi_buffer.push_excerpts(
14201 buffer.clone(),
14202 [ExcerptRange::new(0..first_excerpt_end)],
14203 cx,
14204 );
14205 multi_buffer.push_excerpts(
14206 buffer.clone(),
14207 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14208 cx,
14209 );
14210 multi_buffer
14211 });
14212
14213 let editor = workspace
14214 .update(cx, |_, window, cx| {
14215 cx.new(|cx| {
14216 Editor::new(
14217 EditorMode::Full {
14218 scale_ui_elements_with_buffer_font_size: false,
14219 show_active_line_background: false,
14220 sizing_behavior: SizingBehavior::Default,
14221 },
14222 multi_buffer.clone(),
14223 Some(project.clone()),
14224 window,
14225 cx,
14226 )
14227 })
14228 })
14229 .unwrap();
14230
14231 let pane = workspace
14232 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14233 .unwrap();
14234 pane.update_in(cx, |pane, window, cx| {
14235 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14236 });
14237
14238 let fake_server = fake_servers.next().await.unwrap();
14239
14240 editor.update_in(cx, |editor, window, cx| {
14241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14242 s.select_ranges([
14243 Point::new(1, 11)..Point::new(1, 11),
14244 Point::new(7, 11)..Point::new(7, 11),
14245 ])
14246 });
14247
14248 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14249 });
14250
14251 editor.update_in(cx, |editor, window, cx| {
14252 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14253 });
14254
14255 fake_server
14256 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14257 let completion_item = lsp::CompletionItem {
14258 label: "saturating_sub()".into(),
14259 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14260 lsp::InsertReplaceEdit {
14261 new_text: "saturating_sub()".to_owned(),
14262 insert: lsp::Range::new(
14263 lsp::Position::new(7, 7),
14264 lsp::Position::new(7, 11),
14265 ),
14266 replace: lsp::Range::new(
14267 lsp::Position::new(7, 7),
14268 lsp::Position::new(7, 13),
14269 ),
14270 },
14271 )),
14272 ..lsp::CompletionItem::default()
14273 };
14274
14275 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14276 })
14277 .next()
14278 .await
14279 .unwrap();
14280
14281 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14282 .await;
14283
14284 editor
14285 .update_in(cx, |editor, window, cx| {
14286 editor
14287 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14288 .unwrap()
14289 })
14290 .await
14291 .unwrap();
14292
14293 editor.update(cx, |editor, cx| {
14294 assert_text_with_selections(editor, expected_multibuffer, cx);
14295 })
14296}
14297
14298#[gpui::test]
14299async fn test_completion(cx: &mut TestAppContext) {
14300 init_test(cx, |_| {});
14301
14302 let mut cx = EditorLspTestContext::new_rust(
14303 lsp::ServerCapabilities {
14304 completion_provider: Some(lsp::CompletionOptions {
14305 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14306 resolve_provider: Some(true),
14307 ..Default::default()
14308 }),
14309 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14310 ..Default::default()
14311 },
14312 cx,
14313 )
14314 .await;
14315 let counter = Arc::new(AtomicUsize::new(0));
14316
14317 cx.set_state(indoc! {"
14318 oneˇ
14319 two
14320 three
14321 "});
14322 cx.simulate_keystroke(".");
14323 handle_completion_request(
14324 indoc! {"
14325 one.|<>
14326 two
14327 three
14328 "},
14329 vec!["first_completion", "second_completion"],
14330 true,
14331 counter.clone(),
14332 &mut cx,
14333 )
14334 .await;
14335 cx.condition(|editor, _| editor.context_menu_visible())
14336 .await;
14337 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14338
14339 let _handler = handle_signature_help_request(
14340 &mut cx,
14341 lsp::SignatureHelp {
14342 signatures: vec![lsp::SignatureInformation {
14343 label: "test signature".to_string(),
14344 documentation: None,
14345 parameters: Some(vec![lsp::ParameterInformation {
14346 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14347 documentation: None,
14348 }]),
14349 active_parameter: None,
14350 }],
14351 active_signature: None,
14352 active_parameter: None,
14353 },
14354 );
14355 cx.update_editor(|editor, window, cx| {
14356 assert!(
14357 !editor.signature_help_state.is_shown(),
14358 "No signature help was called for"
14359 );
14360 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14361 });
14362 cx.run_until_parked();
14363 cx.update_editor(|editor, _, _| {
14364 assert!(
14365 !editor.signature_help_state.is_shown(),
14366 "No signature help should be shown when completions menu is open"
14367 );
14368 });
14369
14370 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14371 editor.context_menu_next(&Default::default(), window, cx);
14372 editor
14373 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14374 .unwrap()
14375 });
14376 cx.assert_editor_state(indoc! {"
14377 one.second_completionˇ
14378 two
14379 three
14380 "});
14381
14382 handle_resolve_completion_request(
14383 &mut cx,
14384 Some(vec![
14385 (
14386 //This overlaps with the primary completion edit which is
14387 //misbehavior from the LSP spec, test that we filter it out
14388 indoc! {"
14389 one.second_ˇcompletion
14390 two
14391 threeˇ
14392 "},
14393 "overlapping additional edit",
14394 ),
14395 (
14396 indoc! {"
14397 one.second_completion
14398 two
14399 threeˇ
14400 "},
14401 "\nadditional edit",
14402 ),
14403 ]),
14404 )
14405 .await;
14406 apply_additional_edits.await.unwrap();
14407 cx.assert_editor_state(indoc! {"
14408 one.second_completionˇ
14409 two
14410 three
14411 additional edit
14412 "});
14413
14414 cx.set_state(indoc! {"
14415 one.second_completion
14416 twoˇ
14417 threeˇ
14418 additional edit
14419 "});
14420 cx.simulate_keystroke(" ");
14421 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14422 cx.simulate_keystroke("s");
14423 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14424
14425 cx.assert_editor_state(indoc! {"
14426 one.second_completion
14427 two sˇ
14428 three sˇ
14429 additional edit
14430 "});
14431 handle_completion_request(
14432 indoc! {"
14433 one.second_completion
14434 two s
14435 three <s|>
14436 additional edit
14437 "},
14438 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14439 true,
14440 counter.clone(),
14441 &mut cx,
14442 )
14443 .await;
14444 cx.condition(|editor, _| editor.context_menu_visible())
14445 .await;
14446 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14447
14448 cx.simulate_keystroke("i");
14449
14450 handle_completion_request(
14451 indoc! {"
14452 one.second_completion
14453 two si
14454 three <si|>
14455 additional edit
14456 "},
14457 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14458 true,
14459 counter.clone(),
14460 &mut cx,
14461 )
14462 .await;
14463 cx.condition(|editor, _| editor.context_menu_visible())
14464 .await;
14465 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14466
14467 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14468 editor
14469 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14470 .unwrap()
14471 });
14472 cx.assert_editor_state(indoc! {"
14473 one.second_completion
14474 two sixth_completionˇ
14475 three sixth_completionˇ
14476 additional edit
14477 "});
14478
14479 apply_additional_edits.await.unwrap();
14480
14481 update_test_language_settings(&mut cx, |settings| {
14482 settings.defaults.show_completions_on_input = Some(false);
14483 });
14484 cx.set_state("editorˇ");
14485 cx.simulate_keystroke(".");
14486 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14487 cx.simulate_keystrokes("c l o");
14488 cx.assert_editor_state("editor.cloˇ");
14489 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14490 cx.update_editor(|editor, window, cx| {
14491 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14492 });
14493 handle_completion_request(
14494 "editor.<clo|>",
14495 vec!["close", "clobber"],
14496 true,
14497 counter.clone(),
14498 &mut cx,
14499 )
14500 .await;
14501 cx.condition(|editor, _| editor.context_menu_visible())
14502 .await;
14503 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14504
14505 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14506 editor
14507 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14508 .unwrap()
14509 });
14510 cx.assert_editor_state("editor.clobberˇ");
14511 handle_resolve_completion_request(&mut cx, None).await;
14512 apply_additional_edits.await.unwrap();
14513}
14514
14515#[gpui::test]
14516async fn test_completion_reuse(cx: &mut TestAppContext) {
14517 init_test(cx, |_| {});
14518
14519 let mut cx = EditorLspTestContext::new_rust(
14520 lsp::ServerCapabilities {
14521 completion_provider: Some(lsp::CompletionOptions {
14522 trigger_characters: Some(vec![".".to_string()]),
14523 ..Default::default()
14524 }),
14525 ..Default::default()
14526 },
14527 cx,
14528 )
14529 .await;
14530
14531 let counter = Arc::new(AtomicUsize::new(0));
14532 cx.set_state("objˇ");
14533 cx.simulate_keystroke(".");
14534
14535 // Initial completion request returns complete results
14536 let is_incomplete = false;
14537 handle_completion_request(
14538 "obj.|<>",
14539 vec!["a", "ab", "abc"],
14540 is_incomplete,
14541 counter.clone(),
14542 &mut cx,
14543 )
14544 .await;
14545 cx.run_until_parked();
14546 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14547 cx.assert_editor_state("obj.ˇ");
14548 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14549
14550 // Type "a" - filters existing completions
14551 cx.simulate_keystroke("a");
14552 cx.run_until_parked();
14553 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14554 cx.assert_editor_state("obj.aˇ");
14555 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14556
14557 // Type "b" - filters existing completions
14558 cx.simulate_keystroke("b");
14559 cx.run_until_parked();
14560 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14561 cx.assert_editor_state("obj.abˇ");
14562 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14563
14564 // Type "c" - filters existing completions
14565 cx.simulate_keystroke("c");
14566 cx.run_until_parked();
14567 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14568 cx.assert_editor_state("obj.abcˇ");
14569 check_displayed_completions(vec!["abc"], &mut cx);
14570
14571 // Backspace to delete "c" - filters existing completions
14572 cx.update_editor(|editor, window, cx| {
14573 editor.backspace(&Backspace, window, cx);
14574 });
14575 cx.run_until_parked();
14576 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14577 cx.assert_editor_state("obj.abˇ");
14578 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14579
14580 // Moving cursor to the left dismisses menu.
14581 cx.update_editor(|editor, window, cx| {
14582 editor.move_left(&MoveLeft, window, cx);
14583 });
14584 cx.run_until_parked();
14585 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14586 cx.assert_editor_state("obj.aˇb");
14587 cx.update_editor(|editor, _, _| {
14588 assert_eq!(editor.context_menu_visible(), false);
14589 });
14590
14591 // Type "b" - new request
14592 cx.simulate_keystroke("b");
14593 let is_incomplete = false;
14594 handle_completion_request(
14595 "obj.<ab|>a",
14596 vec!["ab", "abc"],
14597 is_incomplete,
14598 counter.clone(),
14599 &mut cx,
14600 )
14601 .await;
14602 cx.run_until_parked();
14603 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14604 cx.assert_editor_state("obj.abˇb");
14605 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14606
14607 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14608 cx.update_editor(|editor, window, cx| {
14609 editor.backspace(&Backspace, window, cx);
14610 });
14611 let is_incomplete = false;
14612 handle_completion_request(
14613 "obj.<a|>b",
14614 vec!["a", "ab", "abc"],
14615 is_incomplete,
14616 counter.clone(),
14617 &mut cx,
14618 )
14619 .await;
14620 cx.run_until_parked();
14621 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14622 cx.assert_editor_state("obj.aˇb");
14623 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14624
14625 // Backspace to delete "a" - dismisses menu.
14626 cx.update_editor(|editor, window, cx| {
14627 editor.backspace(&Backspace, window, cx);
14628 });
14629 cx.run_until_parked();
14630 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14631 cx.assert_editor_state("obj.ˇb");
14632 cx.update_editor(|editor, _, _| {
14633 assert_eq!(editor.context_menu_visible(), false);
14634 });
14635}
14636
14637#[gpui::test]
14638async fn test_word_completion(cx: &mut TestAppContext) {
14639 let lsp_fetch_timeout_ms = 10;
14640 init_test(cx, |language_settings| {
14641 language_settings.defaults.completions = Some(CompletionSettingsContent {
14642 words_min_length: Some(0),
14643 lsp_fetch_timeout_ms: Some(10),
14644 lsp_insert_mode: Some(LspInsertMode::Insert),
14645 ..Default::default()
14646 });
14647 });
14648
14649 let mut cx = EditorLspTestContext::new_rust(
14650 lsp::ServerCapabilities {
14651 completion_provider: Some(lsp::CompletionOptions {
14652 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14653 ..lsp::CompletionOptions::default()
14654 }),
14655 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14656 ..lsp::ServerCapabilities::default()
14657 },
14658 cx,
14659 )
14660 .await;
14661
14662 let throttle_completions = Arc::new(AtomicBool::new(false));
14663
14664 let lsp_throttle_completions = throttle_completions.clone();
14665 let _completion_requests_handler =
14666 cx.lsp
14667 .server
14668 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14669 let lsp_throttle_completions = lsp_throttle_completions.clone();
14670 let cx = cx.clone();
14671 async move {
14672 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14673 cx.background_executor()
14674 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14675 .await;
14676 }
14677 Ok(Some(lsp::CompletionResponse::Array(vec![
14678 lsp::CompletionItem {
14679 label: "first".into(),
14680 ..lsp::CompletionItem::default()
14681 },
14682 lsp::CompletionItem {
14683 label: "last".into(),
14684 ..lsp::CompletionItem::default()
14685 },
14686 ])))
14687 }
14688 });
14689
14690 cx.set_state(indoc! {"
14691 oneˇ
14692 two
14693 three
14694 "});
14695 cx.simulate_keystroke(".");
14696 cx.executor().run_until_parked();
14697 cx.condition(|editor, _| editor.context_menu_visible())
14698 .await;
14699 cx.update_editor(|editor, window, cx| {
14700 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14701 {
14702 assert_eq!(
14703 completion_menu_entries(menu),
14704 &["first", "last"],
14705 "When LSP server is fast to reply, no fallback word completions are used"
14706 );
14707 } else {
14708 panic!("expected completion menu to be open");
14709 }
14710 editor.cancel(&Cancel, window, cx);
14711 });
14712 cx.executor().run_until_parked();
14713 cx.condition(|editor, _| !editor.context_menu_visible())
14714 .await;
14715
14716 throttle_completions.store(true, atomic::Ordering::Release);
14717 cx.simulate_keystroke(".");
14718 cx.executor()
14719 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14720 cx.executor().run_until_parked();
14721 cx.condition(|editor, _| editor.context_menu_visible())
14722 .await;
14723 cx.update_editor(|editor, _, _| {
14724 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14725 {
14726 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14727 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14728 } else {
14729 panic!("expected completion menu to be open");
14730 }
14731 });
14732}
14733
14734#[gpui::test]
14735async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14736 init_test(cx, |language_settings| {
14737 language_settings.defaults.completions = Some(CompletionSettingsContent {
14738 words: Some(WordsCompletionMode::Enabled),
14739 words_min_length: Some(0),
14740 lsp_insert_mode: Some(LspInsertMode::Insert),
14741 ..Default::default()
14742 });
14743 });
14744
14745 let mut cx = EditorLspTestContext::new_rust(
14746 lsp::ServerCapabilities {
14747 completion_provider: Some(lsp::CompletionOptions {
14748 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14749 ..lsp::CompletionOptions::default()
14750 }),
14751 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14752 ..lsp::ServerCapabilities::default()
14753 },
14754 cx,
14755 )
14756 .await;
14757
14758 let _completion_requests_handler =
14759 cx.lsp
14760 .server
14761 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14762 Ok(Some(lsp::CompletionResponse::Array(vec![
14763 lsp::CompletionItem {
14764 label: "first".into(),
14765 ..lsp::CompletionItem::default()
14766 },
14767 lsp::CompletionItem {
14768 label: "last".into(),
14769 ..lsp::CompletionItem::default()
14770 },
14771 ])))
14772 });
14773
14774 cx.set_state(indoc! {"ˇ
14775 first
14776 last
14777 second
14778 "});
14779 cx.simulate_keystroke(".");
14780 cx.executor().run_until_parked();
14781 cx.condition(|editor, _| editor.context_menu_visible())
14782 .await;
14783 cx.update_editor(|editor, _, _| {
14784 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14785 {
14786 assert_eq!(
14787 completion_menu_entries(menu),
14788 &["first", "last", "second"],
14789 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14790 );
14791 } else {
14792 panic!("expected completion menu to be open");
14793 }
14794 });
14795}
14796
14797#[gpui::test]
14798async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14799 init_test(cx, |language_settings| {
14800 language_settings.defaults.completions = Some(CompletionSettingsContent {
14801 words: Some(WordsCompletionMode::Disabled),
14802 words_min_length: Some(0),
14803 lsp_insert_mode: Some(LspInsertMode::Insert),
14804 ..Default::default()
14805 });
14806 });
14807
14808 let mut cx = EditorLspTestContext::new_rust(
14809 lsp::ServerCapabilities {
14810 completion_provider: Some(lsp::CompletionOptions {
14811 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14812 ..lsp::CompletionOptions::default()
14813 }),
14814 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14815 ..lsp::ServerCapabilities::default()
14816 },
14817 cx,
14818 )
14819 .await;
14820
14821 let _completion_requests_handler =
14822 cx.lsp
14823 .server
14824 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14825 panic!("LSP completions should not be queried when dealing with word completions")
14826 });
14827
14828 cx.set_state(indoc! {"ˇ
14829 first
14830 last
14831 second
14832 "});
14833 cx.update_editor(|editor, window, cx| {
14834 editor.show_word_completions(&ShowWordCompletions, window, cx);
14835 });
14836 cx.executor().run_until_parked();
14837 cx.condition(|editor, _| editor.context_menu_visible())
14838 .await;
14839 cx.update_editor(|editor, _, _| {
14840 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14841 {
14842 assert_eq!(
14843 completion_menu_entries(menu),
14844 &["first", "last", "second"],
14845 "`ShowWordCompletions` action should show word completions"
14846 );
14847 } else {
14848 panic!("expected completion menu to be open");
14849 }
14850 });
14851
14852 cx.simulate_keystroke("l");
14853 cx.executor().run_until_parked();
14854 cx.condition(|editor, _| editor.context_menu_visible())
14855 .await;
14856 cx.update_editor(|editor, _, _| {
14857 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14858 {
14859 assert_eq!(
14860 completion_menu_entries(menu),
14861 &["last"],
14862 "After showing word completions, further editing should filter them and not query the LSP"
14863 );
14864 } else {
14865 panic!("expected completion menu to be open");
14866 }
14867 });
14868}
14869
14870#[gpui::test]
14871async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14872 init_test(cx, |language_settings| {
14873 language_settings.defaults.completions = Some(CompletionSettingsContent {
14874 words_min_length: Some(0),
14875 lsp: Some(false),
14876 lsp_insert_mode: Some(LspInsertMode::Insert),
14877 ..Default::default()
14878 });
14879 });
14880
14881 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14882
14883 cx.set_state(indoc! {"ˇ
14884 0_usize
14885 let
14886 33
14887 4.5f32
14888 "});
14889 cx.update_editor(|editor, window, cx| {
14890 editor.show_completions(&ShowCompletions::default(), window, cx);
14891 });
14892 cx.executor().run_until_parked();
14893 cx.condition(|editor, _| editor.context_menu_visible())
14894 .await;
14895 cx.update_editor(|editor, window, cx| {
14896 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14897 {
14898 assert_eq!(
14899 completion_menu_entries(menu),
14900 &["let"],
14901 "With no digits in the completion query, no digits should be in the word completions"
14902 );
14903 } else {
14904 panic!("expected completion menu to be open");
14905 }
14906 editor.cancel(&Cancel, window, cx);
14907 });
14908
14909 cx.set_state(indoc! {"3ˇ
14910 0_usize
14911 let
14912 3
14913 33.35f32
14914 "});
14915 cx.update_editor(|editor, window, cx| {
14916 editor.show_completions(&ShowCompletions::default(), window, cx);
14917 });
14918 cx.executor().run_until_parked();
14919 cx.condition(|editor, _| editor.context_menu_visible())
14920 .await;
14921 cx.update_editor(|editor, _, _| {
14922 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14923 {
14924 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14925 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14926 } else {
14927 panic!("expected completion menu to be open");
14928 }
14929 });
14930}
14931
14932#[gpui::test]
14933async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14934 init_test(cx, |language_settings| {
14935 language_settings.defaults.completions = Some(CompletionSettingsContent {
14936 words: Some(WordsCompletionMode::Enabled),
14937 words_min_length: Some(3),
14938 lsp_insert_mode: Some(LspInsertMode::Insert),
14939 ..Default::default()
14940 });
14941 });
14942
14943 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14944 cx.set_state(indoc! {"ˇ
14945 wow
14946 wowen
14947 wowser
14948 "});
14949 cx.simulate_keystroke("w");
14950 cx.executor().run_until_parked();
14951 cx.update_editor(|editor, _, _| {
14952 if editor.context_menu.borrow_mut().is_some() {
14953 panic!(
14954 "expected completion menu to be hidden, as words completion threshold is not met"
14955 );
14956 }
14957 });
14958
14959 cx.update_editor(|editor, window, cx| {
14960 editor.show_word_completions(&ShowWordCompletions, window, cx);
14961 });
14962 cx.executor().run_until_parked();
14963 cx.update_editor(|editor, window, cx| {
14964 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14965 {
14966 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14967 } else {
14968 panic!("expected completion menu to be open after the word completions are called with an action");
14969 }
14970
14971 editor.cancel(&Cancel, window, cx);
14972 });
14973 cx.update_editor(|editor, _, _| {
14974 if editor.context_menu.borrow_mut().is_some() {
14975 panic!("expected completion menu to be hidden after canceling");
14976 }
14977 });
14978
14979 cx.simulate_keystroke("o");
14980 cx.executor().run_until_parked();
14981 cx.update_editor(|editor, _, _| {
14982 if editor.context_menu.borrow_mut().is_some() {
14983 panic!(
14984 "expected completion menu to be hidden, as words completion threshold is not met still"
14985 );
14986 }
14987 });
14988
14989 cx.simulate_keystroke("w");
14990 cx.executor().run_until_parked();
14991 cx.update_editor(|editor, _, _| {
14992 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14993 {
14994 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14995 } else {
14996 panic!("expected completion menu to be open after the word completions threshold is met");
14997 }
14998 });
14999}
15000
15001#[gpui::test]
15002async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15003 init_test(cx, |language_settings| {
15004 language_settings.defaults.completions = Some(CompletionSettingsContent {
15005 words: Some(WordsCompletionMode::Enabled),
15006 words_min_length: Some(0),
15007 lsp_insert_mode: Some(LspInsertMode::Insert),
15008 ..Default::default()
15009 });
15010 });
15011
15012 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15013 cx.update_editor(|editor, _, _| {
15014 editor.disable_word_completions();
15015 });
15016 cx.set_state(indoc! {"ˇ
15017 wow
15018 wowen
15019 wowser
15020 "});
15021 cx.simulate_keystroke("w");
15022 cx.executor().run_until_parked();
15023 cx.update_editor(|editor, _, _| {
15024 if editor.context_menu.borrow_mut().is_some() {
15025 panic!(
15026 "expected completion menu to be hidden, as words completion are disabled for this editor"
15027 );
15028 }
15029 });
15030
15031 cx.update_editor(|editor, window, cx| {
15032 editor.show_word_completions(&ShowWordCompletions, window, cx);
15033 });
15034 cx.executor().run_until_parked();
15035 cx.update_editor(|editor, _, _| {
15036 if editor.context_menu.borrow_mut().is_some() {
15037 panic!(
15038 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15039 );
15040 }
15041 });
15042}
15043
15044fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15045 let position = || lsp::Position {
15046 line: params.text_document_position.position.line,
15047 character: params.text_document_position.position.character,
15048 };
15049 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15050 range: lsp::Range {
15051 start: position(),
15052 end: position(),
15053 },
15054 new_text: text.to_string(),
15055 }))
15056}
15057
15058#[gpui::test]
15059async fn test_multiline_completion(cx: &mut TestAppContext) {
15060 init_test(cx, |_| {});
15061
15062 let fs = FakeFs::new(cx.executor());
15063 fs.insert_tree(
15064 path!("/a"),
15065 json!({
15066 "main.ts": "a",
15067 }),
15068 )
15069 .await;
15070
15071 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15072 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15073 let typescript_language = Arc::new(Language::new(
15074 LanguageConfig {
15075 name: "TypeScript".into(),
15076 matcher: LanguageMatcher {
15077 path_suffixes: vec!["ts".to_string()],
15078 ..LanguageMatcher::default()
15079 },
15080 line_comments: vec!["// ".into()],
15081 ..LanguageConfig::default()
15082 },
15083 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15084 ));
15085 language_registry.add(typescript_language.clone());
15086 let mut fake_servers = language_registry.register_fake_lsp(
15087 "TypeScript",
15088 FakeLspAdapter {
15089 capabilities: lsp::ServerCapabilities {
15090 completion_provider: Some(lsp::CompletionOptions {
15091 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15092 ..lsp::CompletionOptions::default()
15093 }),
15094 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15095 ..lsp::ServerCapabilities::default()
15096 },
15097 // Emulate vtsls label generation
15098 label_for_completion: Some(Box::new(|item, _| {
15099 let text = if let Some(description) = item
15100 .label_details
15101 .as_ref()
15102 .and_then(|label_details| label_details.description.as_ref())
15103 {
15104 format!("{} {}", item.label, description)
15105 } else if let Some(detail) = &item.detail {
15106 format!("{} {}", item.label, detail)
15107 } else {
15108 item.label.clone()
15109 };
15110 Some(language::CodeLabel::plain(text, None))
15111 })),
15112 ..FakeLspAdapter::default()
15113 },
15114 );
15115 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15116 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15117 let worktree_id = workspace
15118 .update(cx, |workspace, _window, cx| {
15119 workspace.project().update(cx, |project, cx| {
15120 project.worktrees(cx).next().unwrap().read(cx).id()
15121 })
15122 })
15123 .unwrap();
15124 let _buffer = project
15125 .update(cx, |project, cx| {
15126 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15127 })
15128 .await
15129 .unwrap();
15130 let editor = workspace
15131 .update(cx, |workspace, window, cx| {
15132 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15133 })
15134 .unwrap()
15135 .await
15136 .unwrap()
15137 .downcast::<Editor>()
15138 .unwrap();
15139 let fake_server = fake_servers.next().await.unwrap();
15140
15141 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15142 let multiline_label_2 = "a\nb\nc\n";
15143 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15144 let multiline_description = "d\ne\nf\n";
15145 let multiline_detail_2 = "g\nh\ni\n";
15146
15147 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15148 move |params, _| async move {
15149 Ok(Some(lsp::CompletionResponse::Array(vec![
15150 lsp::CompletionItem {
15151 label: multiline_label.to_string(),
15152 text_edit: gen_text_edit(¶ms, "new_text_1"),
15153 ..lsp::CompletionItem::default()
15154 },
15155 lsp::CompletionItem {
15156 label: "single line label 1".to_string(),
15157 detail: Some(multiline_detail.to_string()),
15158 text_edit: gen_text_edit(¶ms, "new_text_2"),
15159 ..lsp::CompletionItem::default()
15160 },
15161 lsp::CompletionItem {
15162 label: "single line label 2".to_string(),
15163 label_details: Some(lsp::CompletionItemLabelDetails {
15164 description: Some(multiline_description.to_string()),
15165 detail: None,
15166 }),
15167 text_edit: gen_text_edit(¶ms, "new_text_2"),
15168 ..lsp::CompletionItem::default()
15169 },
15170 lsp::CompletionItem {
15171 label: multiline_label_2.to_string(),
15172 detail: Some(multiline_detail_2.to_string()),
15173 text_edit: gen_text_edit(¶ms, "new_text_3"),
15174 ..lsp::CompletionItem::default()
15175 },
15176 lsp::CompletionItem {
15177 label: "Label with many spaces and \t but without newlines".to_string(),
15178 detail: Some(
15179 "Details with many spaces and \t but without newlines".to_string(),
15180 ),
15181 text_edit: gen_text_edit(¶ms, "new_text_4"),
15182 ..lsp::CompletionItem::default()
15183 },
15184 ])))
15185 },
15186 );
15187
15188 editor.update_in(cx, |editor, window, cx| {
15189 cx.focus_self(window);
15190 editor.move_to_end(&MoveToEnd, window, cx);
15191 editor.handle_input(".", window, cx);
15192 });
15193 cx.run_until_parked();
15194 completion_handle.next().await.unwrap();
15195
15196 editor.update(cx, |editor, _| {
15197 assert!(editor.context_menu_visible());
15198 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15199 {
15200 let completion_labels = menu
15201 .completions
15202 .borrow()
15203 .iter()
15204 .map(|c| c.label.text.clone())
15205 .collect::<Vec<_>>();
15206 assert_eq!(
15207 completion_labels,
15208 &[
15209 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15210 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15211 "single line label 2 d e f ",
15212 "a b c g h i ",
15213 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15214 ],
15215 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15216 );
15217
15218 for completion in menu
15219 .completions
15220 .borrow()
15221 .iter() {
15222 assert_eq!(
15223 completion.label.filter_range,
15224 0..completion.label.text.len(),
15225 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15226 );
15227 }
15228 } else {
15229 panic!("expected completion menu to be open");
15230 }
15231 });
15232}
15233
15234#[gpui::test]
15235async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15236 init_test(cx, |_| {});
15237 let mut cx = EditorLspTestContext::new_rust(
15238 lsp::ServerCapabilities {
15239 completion_provider: Some(lsp::CompletionOptions {
15240 trigger_characters: Some(vec![".".to_string()]),
15241 ..Default::default()
15242 }),
15243 ..Default::default()
15244 },
15245 cx,
15246 )
15247 .await;
15248 cx.lsp
15249 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15250 Ok(Some(lsp::CompletionResponse::Array(vec![
15251 lsp::CompletionItem {
15252 label: "first".into(),
15253 ..Default::default()
15254 },
15255 lsp::CompletionItem {
15256 label: "last".into(),
15257 ..Default::default()
15258 },
15259 ])))
15260 });
15261 cx.set_state("variableˇ");
15262 cx.simulate_keystroke(".");
15263 cx.executor().run_until_parked();
15264
15265 cx.update_editor(|editor, _, _| {
15266 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15267 {
15268 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15269 } else {
15270 panic!("expected completion menu to be open");
15271 }
15272 });
15273
15274 cx.update_editor(|editor, window, cx| {
15275 editor.move_page_down(&MovePageDown::default(), window, cx);
15276 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15277 {
15278 assert!(
15279 menu.selected_item == 1,
15280 "expected PageDown to select the last item from the context menu"
15281 );
15282 } else {
15283 panic!("expected completion menu to stay open after PageDown");
15284 }
15285 });
15286
15287 cx.update_editor(|editor, window, cx| {
15288 editor.move_page_up(&MovePageUp::default(), window, cx);
15289 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15290 {
15291 assert!(
15292 menu.selected_item == 0,
15293 "expected PageUp to select the first item from the context menu"
15294 );
15295 } else {
15296 panic!("expected completion menu to stay open after PageUp");
15297 }
15298 });
15299}
15300
15301#[gpui::test]
15302async fn test_as_is_completions(cx: &mut TestAppContext) {
15303 init_test(cx, |_| {});
15304 let mut cx = EditorLspTestContext::new_rust(
15305 lsp::ServerCapabilities {
15306 completion_provider: Some(lsp::CompletionOptions {
15307 ..Default::default()
15308 }),
15309 ..Default::default()
15310 },
15311 cx,
15312 )
15313 .await;
15314 cx.lsp
15315 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15316 Ok(Some(lsp::CompletionResponse::Array(vec![
15317 lsp::CompletionItem {
15318 label: "unsafe".into(),
15319 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15320 range: lsp::Range {
15321 start: lsp::Position {
15322 line: 1,
15323 character: 2,
15324 },
15325 end: lsp::Position {
15326 line: 1,
15327 character: 3,
15328 },
15329 },
15330 new_text: "unsafe".to_string(),
15331 })),
15332 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15333 ..Default::default()
15334 },
15335 ])))
15336 });
15337 cx.set_state("fn a() {}\n nˇ");
15338 cx.executor().run_until_parked();
15339 cx.update_editor(|editor, window, cx| {
15340 editor.show_completions(
15341 &ShowCompletions {
15342 trigger: Some("\n".into()),
15343 },
15344 window,
15345 cx,
15346 );
15347 });
15348 cx.executor().run_until_parked();
15349
15350 cx.update_editor(|editor, window, cx| {
15351 editor.confirm_completion(&Default::default(), window, cx)
15352 });
15353 cx.executor().run_until_parked();
15354 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15355}
15356
15357#[gpui::test]
15358async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15359 init_test(cx, |_| {});
15360 let language =
15361 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15362 let mut cx = EditorLspTestContext::new(
15363 language,
15364 lsp::ServerCapabilities {
15365 completion_provider: Some(lsp::CompletionOptions {
15366 ..lsp::CompletionOptions::default()
15367 }),
15368 ..lsp::ServerCapabilities::default()
15369 },
15370 cx,
15371 )
15372 .await;
15373
15374 cx.set_state(
15375 "#ifndef BAR_H
15376#define BAR_H
15377
15378#include <stdbool.h>
15379
15380int fn_branch(bool do_branch1, bool do_branch2);
15381
15382#endif // BAR_H
15383ˇ",
15384 );
15385 cx.executor().run_until_parked();
15386 cx.update_editor(|editor, window, cx| {
15387 editor.handle_input("#", window, cx);
15388 });
15389 cx.executor().run_until_parked();
15390 cx.update_editor(|editor, window, cx| {
15391 editor.handle_input("i", window, cx);
15392 });
15393 cx.executor().run_until_parked();
15394 cx.update_editor(|editor, window, cx| {
15395 editor.handle_input("n", window, cx);
15396 });
15397 cx.executor().run_until_parked();
15398 cx.assert_editor_state(
15399 "#ifndef BAR_H
15400#define BAR_H
15401
15402#include <stdbool.h>
15403
15404int fn_branch(bool do_branch1, bool do_branch2);
15405
15406#endif // BAR_H
15407#inˇ",
15408 );
15409
15410 cx.lsp
15411 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15412 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15413 is_incomplete: false,
15414 item_defaults: None,
15415 items: vec![lsp::CompletionItem {
15416 kind: Some(lsp::CompletionItemKind::SNIPPET),
15417 label_details: Some(lsp::CompletionItemLabelDetails {
15418 detail: Some("header".to_string()),
15419 description: None,
15420 }),
15421 label: " include".to_string(),
15422 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15423 range: lsp::Range {
15424 start: lsp::Position {
15425 line: 8,
15426 character: 1,
15427 },
15428 end: lsp::Position {
15429 line: 8,
15430 character: 1,
15431 },
15432 },
15433 new_text: "include \"$0\"".to_string(),
15434 })),
15435 sort_text: Some("40b67681include".to_string()),
15436 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15437 filter_text: Some("include".to_string()),
15438 insert_text: Some("include \"$0\"".to_string()),
15439 ..lsp::CompletionItem::default()
15440 }],
15441 })))
15442 });
15443 cx.update_editor(|editor, window, cx| {
15444 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15445 });
15446 cx.executor().run_until_parked();
15447 cx.update_editor(|editor, window, cx| {
15448 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15449 });
15450 cx.executor().run_until_parked();
15451 cx.assert_editor_state(
15452 "#ifndef BAR_H
15453#define BAR_H
15454
15455#include <stdbool.h>
15456
15457int fn_branch(bool do_branch1, bool do_branch2);
15458
15459#endif // BAR_H
15460#include \"ˇ\"",
15461 );
15462
15463 cx.lsp
15464 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15465 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15466 is_incomplete: true,
15467 item_defaults: None,
15468 items: vec![lsp::CompletionItem {
15469 kind: Some(lsp::CompletionItemKind::FILE),
15470 label: "AGL/".to_string(),
15471 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15472 range: lsp::Range {
15473 start: lsp::Position {
15474 line: 8,
15475 character: 10,
15476 },
15477 end: lsp::Position {
15478 line: 8,
15479 character: 11,
15480 },
15481 },
15482 new_text: "AGL/".to_string(),
15483 })),
15484 sort_text: Some("40b67681AGL/".to_string()),
15485 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15486 filter_text: Some("AGL/".to_string()),
15487 insert_text: Some("AGL/".to_string()),
15488 ..lsp::CompletionItem::default()
15489 }],
15490 })))
15491 });
15492 cx.update_editor(|editor, window, cx| {
15493 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15494 });
15495 cx.executor().run_until_parked();
15496 cx.update_editor(|editor, window, cx| {
15497 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15498 });
15499 cx.executor().run_until_parked();
15500 cx.assert_editor_state(
15501 r##"#ifndef BAR_H
15502#define BAR_H
15503
15504#include <stdbool.h>
15505
15506int fn_branch(bool do_branch1, bool do_branch2);
15507
15508#endif // BAR_H
15509#include "AGL/ˇ"##,
15510 );
15511
15512 cx.update_editor(|editor, window, cx| {
15513 editor.handle_input("\"", window, cx);
15514 });
15515 cx.executor().run_until_parked();
15516 cx.assert_editor_state(
15517 r##"#ifndef BAR_H
15518#define BAR_H
15519
15520#include <stdbool.h>
15521
15522int fn_branch(bool do_branch1, bool do_branch2);
15523
15524#endif // BAR_H
15525#include "AGL/"ˇ"##,
15526 );
15527}
15528
15529#[gpui::test]
15530async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15531 init_test(cx, |_| {});
15532
15533 let mut cx = EditorLspTestContext::new_rust(
15534 lsp::ServerCapabilities {
15535 completion_provider: Some(lsp::CompletionOptions {
15536 trigger_characters: Some(vec![".".to_string()]),
15537 resolve_provider: Some(true),
15538 ..Default::default()
15539 }),
15540 ..Default::default()
15541 },
15542 cx,
15543 )
15544 .await;
15545
15546 cx.set_state("fn main() { let a = 2ˇ; }");
15547 cx.simulate_keystroke(".");
15548 let completion_item = lsp::CompletionItem {
15549 label: "Some".into(),
15550 kind: Some(lsp::CompletionItemKind::SNIPPET),
15551 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15552 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15553 kind: lsp::MarkupKind::Markdown,
15554 value: "```rust\nSome(2)\n```".to_string(),
15555 })),
15556 deprecated: Some(false),
15557 sort_text: Some("Some".to_string()),
15558 filter_text: Some("Some".to_string()),
15559 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15560 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15561 range: lsp::Range {
15562 start: lsp::Position {
15563 line: 0,
15564 character: 22,
15565 },
15566 end: lsp::Position {
15567 line: 0,
15568 character: 22,
15569 },
15570 },
15571 new_text: "Some(2)".to_string(),
15572 })),
15573 additional_text_edits: Some(vec![lsp::TextEdit {
15574 range: lsp::Range {
15575 start: lsp::Position {
15576 line: 0,
15577 character: 20,
15578 },
15579 end: lsp::Position {
15580 line: 0,
15581 character: 22,
15582 },
15583 },
15584 new_text: "".to_string(),
15585 }]),
15586 ..Default::default()
15587 };
15588
15589 let closure_completion_item = completion_item.clone();
15590 let counter = Arc::new(AtomicUsize::new(0));
15591 let counter_clone = counter.clone();
15592 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15593 let task_completion_item = closure_completion_item.clone();
15594 counter_clone.fetch_add(1, atomic::Ordering::Release);
15595 async move {
15596 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15597 is_incomplete: true,
15598 item_defaults: None,
15599 items: vec![task_completion_item],
15600 })))
15601 }
15602 });
15603
15604 cx.condition(|editor, _| editor.context_menu_visible())
15605 .await;
15606 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15607 assert!(request.next().await.is_some());
15608 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15609
15610 cx.simulate_keystrokes("S o m");
15611 cx.condition(|editor, _| editor.context_menu_visible())
15612 .await;
15613 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15614 assert!(request.next().await.is_some());
15615 assert!(request.next().await.is_some());
15616 assert!(request.next().await.is_some());
15617 request.close();
15618 assert!(request.next().await.is_none());
15619 assert_eq!(
15620 counter.load(atomic::Ordering::Acquire),
15621 4,
15622 "With the completions menu open, only one LSP request should happen per input"
15623 );
15624}
15625
15626#[gpui::test]
15627async fn test_toggle_comment(cx: &mut TestAppContext) {
15628 init_test(cx, |_| {});
15629 let mut cx = EditorTestContext::new(cx).await;
15630 let language = Arc::new(Language::new(
15631 LanguageConfig {
15632 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15633 ..Default::default()
15634 },
15635 Some(tree_sitter_rust::LANGUAGE.into()),
15636 ));
15637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15638
15639 // If multiple selections intersect a line, the line is only toggled once.
15640 cx.set_state(indoc! {"
15641 fn a() {
15642 «//b();
15643 ˇ»// «c();
15644 //ˇ» d();
15645 }
15646 "});
15647
15648 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15649
15650 cx.assert_editor_state(indoc! {"
15651 fn a() {
15652 «b();
15653 c();
15654 ˇ» d();
15655 }
15656 "});
15657
15658 // The comment prefix is inserted at the same column for every line in a
15659 // selection.
15660 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15661
15662 cx.assert_editor_state(indoc! {"
15663 fn a() {
15664 // «b();
15665 // c();
15666 ˇ»// d();
15667 }
15668 "});
15669
15670 // If a selection ends at the beginning of a line, that line is not toggled.
15671 cx.set_selections_state(indoc! {"
15672 fn a() {
15673 // b();
15674 «// c();
15675 ˇ» // d();
15676 }
15677 "});
15678
15679 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15680
15681 cx.assert_editor_state(indoc! {"
15682 fn a() {
15683 // b();
15684 «c();
15685 ˇ» // d();
15686 }
15687 "});
15688
15689 // If a selection span a single line and is empty, the line is toggled.
15690 cx.set_state(indoc! {"
15691 fn a() {
15692 a();
15693 b();
15694 ˇ
15695 }
15696 "});
15697
15698 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15699
15700 cx.assert_editor_state(indoc! {"
15701 fn a() {
15702 a();
15703 b();
15704 //•ˇ
15705 }
15706 "});
15707
15708 // If a selection span multiple lines, empty lines are not toggled.
15709 cx.set_state(indoc! {"
15710 fn a() {
15711 «a();
15712
15713 c();ˇ»
15714 }
15715 "});
15716
15717 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15718
15719 cx.assert_editor_state(indoc! {"
15720 fn a() {
15721 // «a();
15722
15723 // c();ˇ»
15724 }
15725 "});
15726
15727 // If a selection includes multiple comment prefixes, all lines are uncommented.
15728 cx.set_state(indoc! {"
15729 fn a() {
15730 «// a();
15731 /// b();
15732 //! c();ˇ»
15733 }
15734 "});
15735
15736 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15737
15738 cx.assert_editor_state(indoc! {"
15739 fn a() {
15740 «a();
15741 b();
15742 c();ˇ»
15743 }
15744 "});
15745}
15746
15747#[gpui::test]
15748async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15749 init_test(cx, |_| {});
15750 let mut cx = EditorTestContext::new(cx).await;
15751 let language = Arc::new(Language::new(
15752 LanguageConfig {
15753 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15754 ..Default::default()
15755 },
15756 Some(tree_sitter_rust::LANGUAGE.into()),
15757 ));
15758 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15759
15760 let toggle_comments = &ToggleComments {
15761 advance_downwards: false,
15762 ignore_indent: true,
15763 };
15764
15765 // If multiple selections intersect a line, the line is only toggled once.
15766 cx.set_state(indoc! {"
15767 fn a() {
15768 // «b();
15769 // c();
15770 // ˇ» d();
15771 }
15772 "});
15773
15774 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15775
15776 cx.assert_editor_state(indoc! {"
15777 fn a() {
15778 «b();
15779 c();
15780 ˇ» d();
15781 }
15782 "});
15783
15784 // The comment prefix is inserted at the beginning of each line
15785 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15786
15787 cx.assert_editor_state(indoc! {"
15788 fn a() {
15789 // «b();
15790 // c();
15791 // ˇ» d();
15792 }
15793 "});
15794
15795 // If a selection ends at the beginning of a line, that line is not toggled.
15796 cx.set_selections_state(indoc! {"
15797 fn a() {
15798 // b();
15799 // «c();
15800 ˇ»// d();
15801 }
15802 "});
15803
15804 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15805
15806 cx.assert_editor_state(indoc! {"
15807 fn a() {
15808 // b();
15809 «c();
15810 ˇ»// d();
15811 }
15812 "});
15813
15814 // If a selection span a single line and is empty, the line is toggled.
15815 cx.set_state(indoc! {"
15816 fn a() {
15817 a();
15818 b();
15819 ˇ
15820 }
15821 "});
15822
15823 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15824
15825 cx.assert_editor_state(indoc! {"
15826 fn a() {
15827 a();
15828 b();
15829 //ˇ
15830 }
15831 "});
15832
15833 // If a selection span multiple lines, empty lines are not toggled.
15834 cx.set_state(indoc! {"
15835 fn a() {
15836 «a();
15837
15838 c();ˇ»
15839 }
15840 "});
15841
15842 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15843
15844 cx.assert_editor_state(indoc! {"
15845 fn a() {
15846 // «a();
15847
15848 // c();ˇ»
15849 }
15850 "});
15851
15852 // If a selection includes multiple comment prefixes, all lines are uncommented.
15853 cx.set_state(indoc! {"
15854 fn a() {
15855 // «a();
15856 /// b();
15857 //! c();ˇ»
15858 }
15859 "});
15860
15861 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15862
15863 cx.assert_editor_state(indoc! {"
15864 fn a() {
15865 «a();
15866 b();
15867 c();ˇ»
15868 }
15869 "});
15870}
15871
15872#[gpui::test]
15873async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15874 init_test(cx, |_| {});
15875
15876 let language = Arc::new(Language::new(
15877 LanguageConfig {
15878 line_comments: vec!["// ".into()],
15879 ..Default::default()
15880 },
15881 Some(tree_sitter_rust::LANGUAGE.into()),
15882 ));
15883
15884 let mut cx = EditorTestContext::new(cx).await;
15885
15886 cx.language_registry().add(language.clone());
15887 cx.update_buffer(|buffer, cx| {
15888 buffer.set_language(Some(language), cx);
15889 });
15890
15891 let toggle_comments = &ToggleComments {
15892 advance_downwards: true,
15893 ignore_indent: false,
15894 };
15895
15896 // Single cursor on one line -> advance
15897 // Cursor moves horizontally 3 characters as well on non-blank line
15898 cx.set_state(indoc!(
15899 "fn a() {
15900 ˇdog();
15901 cat();
15902 }"
15903 ));
15904 cx.update_editor(|editor, window, cx| {
15905 editor.toggle_comments(toggle_comments, window, cx);
15906 });
15907 cx.assert_editor_state(indoc!(
15908 "fn a() {
15909 // dog();
15910 catˇ();
15911 }"
15912 ));
15913
15914 // Single selection on one line -> don't advance
15915 cx.set_state(indoc!(
15916 "fn a() {
15917 «dog()ˇ»;
15918 cat();
15919 }"
15920 ));
15921 cx.update_editor(|editor, window, cx| {
15922 editor.toggle_comments(toggle_comments, window, cx);
15923 });
15924 cx.assert_editor_state(indoc!(
15925 "fn a() {
15926 // «dog()ˇ»;
15927 cat();
15928 }"
15929 ));
15930
15931 // Multiple cursors on one line -> advance
15932 cx.set_state(indoc!(
15933 "fn a() {
15934 ˇdˇog();
15935 cat();
15936 }"
15937 ));
15938 cx.update_editor(|editor, window, cx| {
15939 editor.toggle_comments(toggle_comments, window, cx);
15940 });
15941 cx.assert_editor_state(indoc!(
15942 "fn a() {
15943 // dog();
15944 catˇ(ˇ);
15945 }"
15946 ));
15947
15948 // Multiple cursors on one line, with selection -> don't advance
15949 cx.set_state(indoc!(
15950 "fn a() {
15951 ˇdˇog«()ˇ»;
15952 cat();
15953 }"
15954 ));
15955 cx.update_editor(|editor, window, cx| {
15956 editor.toggle_comments(toggle_comments, window, cx);
15957 });
15958 cx.assert_editor_state(indoc!(
15959 "fn a() {
15960 // ˇdˇog«()ˇ»;
15961 cat();
15962 }"
15963 ));
15964
15965 // Single cursor on one line -> advance
15966 // Cursor moves to column 0 on blank line
15967 cx.set_state(indoc!(
15968 "fn a() {
15969 ˇdog();
15970
15971 cat();
15972 }"
15973 ));
15974 cx.update_editor(|editor, window, cx| {
15975 editor.toggle_comments(toggle_comments, window, cx);
15976 });
15977 cx.assert_editor_state(indoc!(
15978 "fn a() {
15979 // dog();
15980 ˇ
15981 cat();
15982 }"
15983 ));
15984
15985 // Single cursor on one line -> advance
15986 // Cursor starts and ends at column 0
15987 cx.set_state(indoc!(
15988 "fn a() {
15989 ˇ dog();
15990 cat();
15991 }"
15992 ));
15993 cx.update_editor(|editor, window, cx| {
15994 editor.toggle_comments(toggle_comments, window, cx);
15995 });
15996 cx.assert_editor_state(indoc!(
15997 "fn a() {
15998 // dog();
15999 ˇ cat();
16000 }"
16001 ));
16002}
16003
16004#[gpui::test]
16005async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16006 init_test(cx, |_| {});
16007
16008 let mut cx = EditorTestContext::new(cx).await;
16009
16010 let html_language = Arc::new(
16011 Language::new(
16012 LanguageConfig {
16013 name: "HTML".into(),
16014 block_comment: Some(BlockCommentConfig {
16015 start: "<!-- ".into(),
16016 prefix: "".into(),
16017 end: " -->".into(),
16018 tab_size: 0,
16019 }),
16020 ..Default::default()
16021 },
16022 Some(tree_sitter_html::LANGUAGE.into()),
16023 )
16024 .with_injection_query(
16025 r#"
16026 (script_element
16027 (raw_text) @injection.content
16028 (#set! injection.language "javascript"))
16029 "#,
16030 )
16031 .unwrap(),
16032 );
16033
16034 let javascript_language = Arc::new(Language::new(
16035 LanguageConfig {
16036 name: "JavaScript".into(),
16037 line_comments: vec!["// ".into()],
16038 ..Default::default()
16039 },
16040 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16041 ));
16042
16043 cx.language_registry().add(html_language.clone());
16044 cx.language_registry().add(javascript_language);
16045 cx.update_buffer(|buffer, cx| {
16046 buffer.set_language(Some(html_language), cx);
16047 });
16048
16049 // Toggle comments for empty selections
16050 cx.set_state(
16051 &r#"
16052 <p>A</p>ˇ
16053 <p>B</p>ˇ
16054 <p>C</p>ˇ
16055 "#
16056 .unindent(),
16057 );
16058 cx.update_editor(|editor, window, cx| {
16059 editor.toggle_comments(&ToggleComments::default(), window, cx)
16060 });
16061 cx.assert_editor_state(
16062 &r#"
16063 <!-- <p>A</p>ˇ -->
16064 <!-- <p>B</p>ˇ -->
16065 <!-- <p>C</p>ˇ -->
16066 "#
16067 .unindent(),
16068 );
16069 cx.update_editor(|editor, window, cx| {
16070 editor.toggle_comments(&ToggleComments::default(), window, cx)
16071 });
16072 cx.assert_editor_state(
16073 &r#"
16074 <p>A</p>ˇ
16075 <p>B</p>ˇ
16076 <p>C</p>ˇ
16077 "#
16078 .unindent(),
16079 );
16080
16081 // Toggle comments for mixture of empty and non-empty selections, where
16082 // multiple selections occupy a given line.
16083 cx.set_state(
16084 &r#"
16085 <p>A«</p>
16086 <p>ˇ»B</p>ˇ
16087 <p>C«</p>
16088 <p>ˇ»D</p>ˇ
16089 "#
16090 .unindent(),
16091 );
16092
16093 cx.update_editor(|editor, window, cx| {
16094 editor.toggle_comments(&ToggleComments::default(), window, cx)
16095 });
16096 cx.assert_editor_state(
16097 &r#"
16098 <!-- <p>A«</p>
16099 <p>ˇ»B</p>ˇ -->
16100 <!-- <p>C«</p>
16101 <p>ˇ»D</p>ˇ -->
16102 "#
16103 .unindent(),
16104 );
16105 cx.update_editor(|editor, window, cx| {
16106 editor.toggle_comments(&ToggleComments::default(), window, cx)
16107 });
16108 cx.assert_editor_state(
16109 &r#"
16110 <p>A«</p>
16111 <p>ˇ»B</p>ˇ
16112 <p>C«</p>
16113 <p>ˇ»D</p>ˇ
16114 "#
16115 .unindent(),
16116 );
16117
16118 // Toggle comments when different languages are active for different
16119 // selections.
16120 cx.set_state(
16121 &r#"
16122 ˇ<script>
16123 ˇvar x = new Y();
16124 ˇ</script>
16125 "#
16126 .unindent(),
16127 );
16128 cx.executor().run_until_parked();
16129 cx.update_editor(|editor, window, cx| {
16130 editor.toggle_comments(&ToggleComments::default(), window, cx)
16131 });
16132 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16133 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16134 cx.assert_editor_state(
16135 &r#"
16136 <!-- ˇ<script> -->
16137 // ˇvar x = new Y();
16138 <!-- ˇ</script> -->
16139 "#
16140 .unindent(),
16141 );
16142}
16143
16144#[gpui::test]
16145fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16146 init_test(cx, |_| {});
16147
16148 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16149 let multibuffer = cx.new(|cx| {
16150 let mut multibuffer = MultiBuffer::new(ReadWrite);
16151 multibuffer.push_excerpts(
16152 buffer.clone(),
16153 [
16154 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16155 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16156 ],
16157 cx,
16158 );
16159 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16160 multibuffer
16161 });
16162
16163 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16164 editor.update_in(cx, |editor, window, cx| {
16165 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16166 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16167 s.select_ranges([
16168 Point::new(0, 0)..Point::new(0, 0),
16169 Point::new(1, 0)..Point::new(1, 0),
16170 ])
16171 });
16172
16173 editor.handle_input("X", window, cx);
16174 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16175 assert_eq!(
16176 editor.selections.ranges(&editor.display_snapshot(cx)),
16177 [
16178 Point::new(0, 1)..Point::new(0, 1),
16179 Point::new(1, 1)..Point::new(1, 1),
16180 ]
16181 );
16182
16183 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16185 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16186 });
16187 editor.backspace(&Default::default(), window, cx);
16188 assert_eq!(editor.text(cx), "Xa\nbbb");
16189 assert_eq!(
16190 editor.selections.ranges(&editor.display_snapshot(cx)),
16191 [Point::new(1, 0)..Point::new(1, 0)]
16192 );
16193
16194 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16195 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16196 });
16197 editor.backspace(&Default::default(), window, cx);
16198 assert_eq!(editor.text(cx), "X\nbb");
16199 assert_eq!(
16200 editor.selections.ranges(&editor.display_snapshot(cx)),
16201 [Point::new(0, 1)..Point::new(0, 1)]
16202 );
16203 });
16204}
16205
16206#[gpui::test]
16207fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16208 init_test(cx, |_| {});
16209
16210 let markers = vec![('[', ']').into(), ('(', ')').into()];
16211 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16212 indoc! {"
16213 [aaaa
16214 (bbbb]
16215 cccc)",
16216 },
16217 markers.clone(),
16218 );
16219 let excerpt_ranges = markers.into_iter().map(|marker| {
16220 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16221 ExcerptRange::new(context)
16222 });
16223 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16224 let multibuffer = cx.new(|cx| {
16225 let mut multibuffer = MultiBuffer::new(ReadWrite);
16226 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16227 multibuffer
16228 });
16229
16230 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16231 editor.update_in(cx, |editor, window, cx| {
16232 let (expected_text, selection_ranges) = marked_text_ranges(
16233 indoc! {"
16234 aaaa
16235 bˇbbb
16236 bˇbbˇb
16237 cccc"
16238 },
16239 true,
16240 );
16241 assert_eq!(editor.text(cx), expected_text);
16242 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16243 s.select_ranges(selection_ranges)
16244 });
16245
16246 editor.handle_input("X", window, cx);
16247
16248 let (expected_text, expected_selections) = marked_text_ranges(
16249 indoc! {"
16250 aaaa
16251 bXˇbbXb
16252 bXˇbbXˇb
16253 cccc"
16254 },
16255 false,
16256 );
16257 assert_eq!(editor.text(cx), expected_text);
16258 assert_eq!(
16259 editor.selections.ranges(&editor.display_snapshot(cx)),
16260 expected_selections
16261 );
16262
16263 editor.newline(&Newline, window, cx);
16264 let (expected_text, expected_selections) = marked_text_ranges(
16265 indoc! {"
16266 aaaa
16267 bX
16268 ˇbbX
16269 b
16270 bX
16271 ˇbbX
16272 ˇb
16273 cccc"
16274 },
16275 false,
16276 );
16277 assert_eq!(editor.text(cx), expected_text);
16278 assert_eq!(
16279 editor.selections.ranges(&editor.display_snapshot(cx)),
16280 expected_selections
16281 );
16282 });
16283}
16284
16285#[gpui::test]
16286fn test_refresh_selections(cx: &mut TestAppContext) {
16287 init_test(cx, |_| {});
16288
16289 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16290 let mut excerpt1_id = None;
16291 let multibuffer = cx.new(|cx| {
16292 let mut multibuffer = MultiBuffer::new(ReadWrite);
16293 excerpt1_id = multibuffer
16294 .push_excerpts(
16295 buffer.clone(),
16296 [
16297 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16298 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16299 ],
16300 cx,
16301 )
16302 .into_iter()
16303 .next();
16304 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16305 multibuffer
16306 });
16307
16308 let editor = cx.add_window(|window, cx| {
16309 let mut editor = build_editor(multibuffer.clone(), window, cx);
16310 let snapshot = editor.snapshot(window, cx);
16311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16312 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16313 });
16314 editor.begin_selection(
16315 Point::new(2, 1).to_display_point(&snapshot),
16316 true,
16317 1,
16318 window,
16319 cx,
16320 );
16321 assert_eq!(
16322 editor.selections.ranges(&editor.display_snapshot(cx)),
16323 [
16324 Point::new(1, 3)..Point::new(1, 3),
16325 Point::new(2, 1)..Point::new(2, 1),
16326 ]
16327 );
16328 editor
16329 });
16330
16331 // Refreshing selections is a no-op when excerpts haven't changed.
16332 _ = editor.update(cx, |editor, window, cx| {
16333 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16334 assert_eq!(
16335 editor.selections.ranges(&editor.display_snapshot(cx)),
16336 [
16337 Point::new(1, 3)..Point::new(1, 3),
16338 Point::new(2, 1)..Point::new(2, 1),
16339 ]
16340 );
16341 });
16342
16343 multibuffer.update(cx, |multibuffer, cx| {
16344 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16345 });
16346 _ = editor.update(cx, |editor, window, cx| {
16347 // Removing an excerpt causes the first selection to become degenerate.
16348 assert_eq!(
16349 editor.selections.ranges(&editor.display_snapshot(cx)),
16350 [
16351 Point::new(0, 0)..Point::new(0, 0),
16352 Point::new(0, 1)..Point::new(0, 1)
16353 ]
16354 );
16355
16356 // Refreshing selections will relocate the first selection to the original buffer
16357 // location.
16358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16359 assert_eq!(
16360 editor.selections.ranges(&editor.display_snapshot(cx)),
16361 [
16362 Point::new(0, 1)..Point::new(0, 1),
16363 Point::new(0, 3)..Point::new(0, 3)
16364 ]
16365 );
16366 assert!(editor.selections.pending_anchor().is_some());
16367 });
16368}
16369
16370#[gpui::test]
16371fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16372 init_test(cx, |_| {});
16373
16374 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16375 let mut excerpt1_id = None;
16376 let multibuffer = cx.new(|cx| {
16377 let mut multibuffer = MultiBuffer::new(ReadWrite);
16378 excerpt1_id = multibuffer
16379 .push_excerpts(
16380 buffer.clone(),
16381 [
16382 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16383 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16384 ],
16385 cx,
16386 )
16387 .into_iter()
16388 .next();
16389 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16390 multibuffer
16391 });
16392
16393 let editor = cx.add_window(|window, cx| {
16394 let mut editor = build_editor(multibuffer.clone(), window, cx);
16395 let snapshot = editor.snapshot(window, cx);
16396 editor.begin_selection(
16397 Point::new(1, 3).to_display_point(&snapshot),
16398 false,
16399 1,
16400 window,
16401 cx,
16402 );
16403 assert_eq!(
16404 editor.selections.ranges(&editor.display_snapshot(cx)),
16405 [Point::new(1, 3)..Point::new(1, 3)]
16406 );
16407 editor
16408 });
16409
16410 multibuffer.update(cx, |multibuffer, cx| {
16411 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16412 });
16413 _ = editor.update(cx, |editor, window, cx| {
16414 assert_eq!(
16415 editor.selections.ranges(&editor.display_snapshot(cx)),
16416 [Point::new(0, 0)..Point::new(0, 0)]
16417 );
16418
16419 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16421 assert_eq!(
16422 editor.selections.ranges(&editor.display_snapshot(cx)),
16423 [Point::new(0, 3)..Point::new(0, 3)]
16424 );
16425 assert!(editor.selections.pending_anchor().is_some());
16426 });
16427}
16428
16429#[gpui::test]
16430async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16431 init_test(cx, |_| {});
16432
16433 let language = Arc::new(
16434 Language::new(
16435 LanguageConfig {
16436 brackets: BracketPairConfig {
16437 pairs: vec![
16438 BracketPair {
16439 start: "{".to_string(),
16440 end: "}".to_string(),
16441 close: true,
16442 surround: true,
16443 newline: true,
16444 },
16445 BracketPair {
16446 start: "/* ".to_string(),
16447 end: " */".to_string(),
16448 close: true,
16449 surround: true,
16450 newline: true,
16451 },
16452 ],
16453 ..Default::default()
16454 },
16455 ..Default::default()
16456 },
16457 Some(tree_sitter_rust::LANGUAGE.into()),
16458 )
16459 .with_indents_query("")
16460 .unwrap(),
16461 );
16462
16463 let text = concat!(
16464 "{ }\n", //
16465 " x\n", //
16466 " /* */\n", //
16467 "x\n", //
16468 "{{} }\n", //
16469 );
16470
16471 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16472 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16473 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16474 editor
16475 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16476 .await;
16477
16478 editor.update_in(cx, |editor, window, cx| {
16479 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16480 s.select_display_ranges([
16481 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16482 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16483 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16484 ])
16485 });
16486 editor.newline(&Newline, window, cx);
16487
16488 assert_eq!(
16489 editor.buffer().read(cx).read(cx).text(),
16490 concat!(
16491 "{ \n", // Suppress rustfmt
16492 "\n", //
16493 "}\n", //
16494 " x\n", //
16495 " /* \n", //
16496 " \n", //
16497 " */\n", //
16498 "x\n", //
16499 "{{} \n", //
16500 "}\n", //
16501 )
16502 );
16503 });
16504}
16505
16506#[gpui::test]
16507fn test_highlighted_ranges(cx: &mut TestAppContext) {
16508 init_test(cx, |_| {});
16509
16510 let editor = cx.add_window(|window, cx| {
16511 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16512 build_editor(buffer, window, cx)
16513 });
16514
16515 _ = editor.update(cx, |editor, window, cx| {
16516 struct Type1;
16517 struct Type2;
16518
16519 let buffer = editor.buffer.read(cx).snapshot(cx);
16520
16521 let anchor_range =
16522 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16523
16524 editor.highlight_background::<Type1>(
16525 &[
16526 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16527 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16528 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16529 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16530 ],
16531 |_| Hsla::red(),
16532 cx,
16533 );
16534 editor.highlight_background::<Type2>(
16535 &[
16536 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16537 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16538 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16539 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16540 ],
16541 |_| Hsla::green(),
16542 cx,
16543 );
16544
16545 let snapshot = editor.snapshot(window, cx);
16546 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16547 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16548 &snapshot,
16549 cx.theme(),
16550 );
16551 assert_eq!(
16552 highlighted_ranges,
16553 &[
16554 (
16555 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16556 Hsla::green(),
16557 ),
16558 (
16559 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16560 Hsla::red(),
16561 ),
16562 (
16563 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16564 Hsla::green(),
16565 ),
16566 (
16567 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16568 Hsla::red(),
16569 ),
16570 ]
16571 );
16572 assert_eq!(
16573 editor.sorted_background_highlights_in_range(
16574 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16575 &snapshot,
16576 cx.theme(),
16577 ),
16578 &[(
16579 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16580 Hsla::red(),
16581 )]
16582 );
16583 });
16584}
16585
16586#[gpui::test]
16587async fn test_following(cx: &mut TestAppContext) {
16588 init_test(cx, |_| {});
16589
16590 let fs = FakeFs::new(cx.executor());
16591 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16592
16593 let buffer = project.update(cx, |project, cx| {
16594 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16595 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16596 });
16597 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16598 let follower = cx.update(|cx| {
16599 cx.open_window(
16600 WindowOptions {
16601 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16602 gpui::Point::new(px(0.), px(0.)),
16603 gpui::Point::new(px(10.), px(80.)),
16604 ))),
16605 ..Default::default()
16606 },
16607 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16608 )
16609 .unwrap()
16610 });
16611
16612 let is_still_following = Rc::new(RefCell::new(true));
16613 let follower_edit_event_count = Rc::new(RefCell::new(0));
16614 let pending_update = Rc::new(RefCell::new(None));
16615 let leader_entity = leader.root(cx).unwrap();
16616 let follower_entity = follower.root(cx).unwrap();
16617 _ = follower.update(cx, {
16618 let update = pending_update.clone();
16619 let is_still_following = is_still_following.clone();
16620 let follower_edit_event_count = follower_edit_event_count.clone();
16621 |_, window, cx| {
16622 cx.subscribe_in(
16623 &leader_entity,
16624 window,
16625 move |_, leader, event, window, cx| {
16626 leader.read(cx).add_event_to_update_proto(
16627 event,
16628 &mut update.borrow_mut(),
16629 window,
16630 cx,
16631 );
16632 },
16633 )
16634 .detach();
16635
16636 cx.subscribe_in(
16637 &follower_entity,
16638 window,
16639 move |_, _, event: &EditorEvent, _window, _cx| {
16640 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16641 *is_still_following.borrow_mut() = false;
16642 }
16643
16644 if let EditorEvent::BufferEdited = event {
16645 *follower_edit_event_count.borrow_mut() += 1;
16646 }
16647 },
16648 )
16649 .detach();
16650 }
16651 });
16652
16653 // Update the selections only
16654 _ = leader.update(cx, |leader, window, cx| {
16655 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16656 s.select_ranges([1..1])
16657 });
16658 });
16659 follower
16660 .update(cx, |follower, window, cx| {
16661 follower.apply_update_proto(
16662 &project,
16663 pending_update.borrow_mut().take().unwrap(),
16664 window,
16665 cx,
16666 )
16667 })
16668 .unwrap()
16669 .await
16670 .unwrap();
16671 _ = follower.update(cx, |follower, _, cx| {
16672 assert_eq!(
16673 follower.selections.ranges(&follower.display_snapshot(cx)),
16674 vec![1..1]
16675 );
16676 });
16677 assert!(*is_still_following.borrow());
16678 assert_eq!(*follower_edit_event_count.borrow(), 0);
16679
16680 // Update the scroll position only
16681 _ = leader.update(cx, |leader, window, cx| {
16682 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16683 });
16684 follower
16685 .update(cx, |follower, window, cx| {
16686 follower.apply_update_proto(
16687 &project,
16688 pending_update.borrow_mut().take().unwrap(),
16689 window,
16690 cx,
16691 )
16692 })
16693 .unwrap()
16694 .await
16695 .unwrap();
16696 assert_eq!(
16697 follower
16698 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16699 .unwrap(),
16700 gpui::Point::new(1.5, 3.5)
16701 );
16702 assert!(*is_still_following.borrow());
16703 assert_eq!(*follower_edit_event_count.borrow(), 0);
16704
16705 // Update the selections and scroll position. The follower's scroll position is updated
16706 // via autoscroll, not via the leader's exact scroll position.
16707 _ = leader.update(cx, |leader, window, cx| {
16708 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16709 s.select_ranges([0..0])
16710 });
16711 leader.request_autoscroll(Autoscroll::newest(), cx);
16712 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16713 });
16714 follower
16715 .update(cx, |follower, window, cx| {
16716 follower.apply_update_proto(
16717 &project,
16718 pending_update.borrow_mut().take().unwrap(),
16719 window,
16720 cx,
16721 )
16722 })
16723 .unwrap()
16724 .await
16725 .unwrap();
16726 _ = follower.update(cx, |follower, _, cx| {
16727 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16728 assert_eq!(
16729 follower.selections.ranges(&follower.display_snapshot(cx)),
16730 vec![0..0]
16731 );
16732 });
16733 assert!(*is_still_following.borrow());
16734
16735 // Creating a pending selection that precedes another selection
16736 _ = leader.update(cx, |leader, window, cx| {
16737 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16738 s.select_ranges([1..1])
16739 });
16740 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16741 });
16742 follower
16743 .update(cx, |follower, window, cx| {
16744 follower.apply_update_proto(
16745 &project,
16746 pending_update.borrow_mut().take().unwrap(),
16747 window,
16748 cx,
16749 )
16750 })
16751 .unwrap()
16752 .await
16753 .unwrap();
16754 _ = follower.update(cx, |follower, _, cx| {
16755 assert_eq!(
16756 follower.selections.ranges(&follower.display_snapshot(cx)),
16757 vec![0..0, 1..1]
16758 );
16759 });
16760 assert!(*is_still_following.borrow());
16761
16762 // Extend the pending selection so that it surrounds another selection
16763 _ = leader.update(cx, |leader, window, cx| {
16764 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16765 });
16766 follower
16767 .update(cx, |follower, window, cx| {
16768 follower.apply_update_proto(
16769 &project,
16770 pending_update.borrow_mut().take().unwrap(),
16771 window,
16772 cx,
16773 )
16774 })
16775 .unwrap()
16776 .await
16777 .unwrap();
16778 _ = follower.update(cx, |follower, _, cx| {
16779 assert_eq!(
16780 follower.selections.ranges(&follower.display_snapshot(cx)),
16781 vec![0..2]
16782 );
16783 });
16784
16785 // Scrolling locally breaks the follow
16786 _ = follower.update(cx, |follower, window, cx| {
16787 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16788 follower.set_scroll_anchor(
16789 ScrollAnchor {
16790 anchor: top_anchor,
16791 offset: gpui::Point::new(0.0, 0.5),
16792 },
16793 window,
16794 cx,
16795 );
16796 });
16797 assert!(!(*is_still_following.borrow()));
16798}
16799
16800#[gpui::test]
16801async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16802 init_test(cx, |_| {});
16803
16804 let fs = FakeFs::new(cx.executor());
16805 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16806 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16807 let pane = workspace
16808 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16809 .unwrap();
16810
16811 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16812
16813 let leader = pane.update_in(cx, |_, window, cx| {
16814 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16815 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16816 });
16817
16818 // Start following the editor when it has no excerpts.
16819 let mut state_message =
16820 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16821 let workspace_entity = workspace.root(cx).unwrap();
16822 let follower_1 = cx
16823 .update_window(*workspace.deref(), |_, window, cx| {
16824 Editor::from_state_proto(
16825 workspace_entity,
16826 ViewId {
16827 creator: CollaboratorId::PeerId(PeerId::default()),
16828 id: 0,
16829 },
16830 &mut state_message,
16831 window,
16832 cx,
16833 )
16834 })
16835 .unwrap()
16836 .unwrap()
16837 .await
16838 .unwrap();
16839
16840 let update_message = Rc::new(RefCell::new(None));
16841 follower_1.update_in(cx, {
16842 let update = update_message.clone();
16843 |_, window, cx| {
16844 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16845 leader.read(cx).add_event_to_update_proto(
16846 event,
16847 &mut update.borrow_mut(),
16848 window,
16849 cx,
16850 );
16851 })
16852 .detach();
16853 }
16854 });
16855
16856 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16857 (
16858 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16859 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16860 )
16861 });
16862
16863 // Insert some excerpts.
16864 leader.update(cx, |leader, cx| {
16865 leader.buffer.update(cx, |multibuffer, cx| {
16866 multibuffer.set_excerpts_for_path(
16867 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16868 buffer_1.clone(),
16869 vec![
16870 Point::row_range(0..3),
16871 Point::row_range(1..6),
16872 Point::row_range(12..15),
16873 ],
16874 0,
16875 cx,
16876 );
16877 multibuffer.set_excerpts_for_path(
16878 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16879 buffer_2.clone(),
16880 vec![Point::row_range(0..6), Point::row_range(8..12)],
16881 0,
16882 cx,
16883 );
16884 });
16885 });
16886
16887 // Apply the update of adding the excerpts.
16888 follower_1
16889 .update_in(cx, |follower, window, cx| {
16890 follower.apply_update_proto(
16891 &project,
16892 update_message.borrow().clone().unwrap(),
16893 window,
16894 cx,
16895 )
16896 })
16897 .await
16898 .unwrap();
16899 assert_eq!(
16900 follower_1.update(cx, |editor, cx| editor.text(cx)),
16901 leader.update(cx, |editor, cx| editor.text(cx))
16902 );
16903 update_message.borrow_mut().take();
16904
16905 // Start following separately after it already has excerpts.
16906 let mut state_message =
16907 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16908 let workspace_entity = workspace.root(cx).unwrap();
16909 let follower_2 = cx
16910 .update_window(*workspace.deref(), |_, window, cx| {
16911 Editor::from_state_proto(
16912 workspace_entity,
16913 ViewId {
16914 creator: CollaboratorId::PeerId(PeerId::default()),
16915 id: 0,
16916 },
16917 &mut state_message,
16918 window,
16919 cx,
16920 )
16921 })
16922 .unwrap()
16923 .unwrap()
16924 .await
16925 .unwrap();
16926 assert_eq!(
16927 follower_2.update(cx, |editor, cx| editor.text(cx)),
16928 leader.update(cx, |editor, cx| editor.text(cx))
16929 );
16930
16931 // Remove some excerpts.
16932 leader.update(cx, |leader, cx| {
16933 leader.buffer.update(cx, |multibuffer, cx| {
16934 let excerpt_ids = multibuffer.excerpt_ids();
16935 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16936 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16937 });
16938 });
16939
16940 // Apply the update of removing the excerpts.
16941 follower_1
16942 .update_in(cx, |follower, window, cx| {
16943 follower.apply_update_proto(
16944 &project,
16945 update_message.borrow().clone().unwrap(),
16946 window,
16947 cx,
16948 )
16949 })
16950 .await
16951 .unwrap();
16952 follower_2
16953 .update_in(cx, |follower, window, cx| {
16954 follower.apply_update_proto(
16955 &project,
16956 update_message.borrow().clone().unwrap(),
16957 window,
16958 cx,
16959 )
16960 })
16961 .await
16962 .unwrap();
16963 update_message.borrow_mut().take();
16964 assert_eq!(
16965 follower_1.update(cx, |editor, cx| editor.text(cx)),
16966 leader.update(cx, |editor, cx| editor.text(cx))
16967 );
16968}
16969
16970#[gpui::test]
16971async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16972 init_test(cx, |_| {});
16973
16974 let mut cx = EditorTestContext::new(cx).await;
16975 let lsp_store =
16976 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16977
16978 cx.set_state(indoc! {"
16979 ˇfn func(abc def: i32) -> u32 {
16980 }
16981 "});
16982
16983 cx.update(|_, cx| {
16984 lsp_store.update(cx, |lsp_store, cx| {
16985 lsp_store
16986 .update_diagnostics(
16987 LanguageServerId(0),
16988 lsp::PublishDiagnosticsParams {
16989 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16990 version: None,
16991 diagnostics: vec![
16992 lsp::Diagnostic {
16993 range: lsp::Range::new(
16994 lsp::Position::new(0, 11),
16995 lsp::Position::new(0, 12),
16996 ),
16997 severity: Some(lsp::DiagnosticSeverity::ERROR),
16998 ..Default::default()
16999 },
17000 lsp::Diagnostic {
17001 range: lsp::Range::new(
17002 lsp::Position::new(0, 12),
17003 lsp::Position::new(0, 15),
17004 ),
17005 severity: Some(lsp::DiagnosticSeverity::ERROR),
17006 ..Default::default()
17007 },
17008 lsp::Diagnostic {
17009 range: lsp::Range::new(
17010 lsp::Position::new(0, 25),
17011 lsp::Position::new(0, 28),
17012 ),
17013 severity: Some(lsp::DiagnosticSeverity::ERROR),
17014 ..Default::default()
17015 },
17016 ],
17017 },
17018 None,
17019 DiagnosticSourceKind::Pushed,
17020 &[],
17021 cx,
17022 )
17023 .unwrap()
17024 });
17025 });
17026
17027 executor.run_until_parked();
17028
17029 cx.update_editor(|editor, window, cx| {
17030 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17031 });
17032
17033 cx.assert_editor_state(indoc! {"
17034 fn func(abc def: i32) -> ˇu32 {
17035 }
17036 "});
17037
17038 cx.update_editor(|editor, window, cx| {
17039 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17040 });
17041
17042 cx.assert_editor_state(indoc! {"
17043 fn func(abc ˇdef: i32) -> u32 {
17044 }
17045 "});
17046
17047 cx.update_editor(|editor, window, cx| {
17048 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17049 });
17050
17051 cx.assert_editor_state(indoc! {"
17052 fn func(abcˇ def: i32) -> u32 {
17053 }
17054 "});
17055
17056 cx.update_editor(|editor, window, cx| {
17057 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17058 });
17059
17060 cx.assert_editor_state(indoc! {"
17061 fn func(abc def: i32) -> ˇu32 {
17062 }
17063 "});
17064}
17065
17066#[gpui::test]
17067async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17068 init_test(cx, |_| {});
17069
17070 let mut cx = EditorTestContext::new(cx).await;
17071
17072 let diff_base = r#"
17073 use some::mod;
17074
17075 const A: u32 = 42;
17076
17077 fn main() {
17078 println!("hello");
17079
17080 println!("world");
17081 }
17082 "#
17083 .unindent();
17084
17085 // Edits are modified, removed, modified, added
17086 cx.set_state(
17087 &r#"
17088 use some::modified;
17089
17090 ˇ
17091 fn main() {
17092 println!("hello there");
17093
17094 println!("around the");
17095 println!("world");
17096 }
17097 "#
17098 .unindent(),
17099 );
17100
17101 cx.set_head_text(&diff_base);
17102 executor.run_until_parked();
17103
17104 cx.update_editor(|editor, window, cx| {
17105 //Wrap around the bottom of the buffer
17106 for _ in 0..3 {
17107 editor.go_to_next_hunk(&GoToHunk, window, cx);
17108 }
17109 });
17110
17111 cx.assert_editor_state(
17112 &r#"
17113 ˇuse some::modified;
17114
17115
17116 fn main() {
17117 println!("hello there");
17118
17119 println!("around the");
17120 println!("world");
17121 }
17122 "#
17123 .unindent(),
17124 );
17125
17126 cx.update_editor(|editor, window, cx| {
17127 //Wrap around the top of the buffer
17128 for _ in 0..2 {
17129 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17130 }
17131 });
17132
17133 cx.assert_editor_state(
17134 &r#"
17135 use some::modified;
17136
17137
17138 fn main() {
17139 ˇ println!("hello there");
17140
17141 println!("around the");
17142 println!("world");
17143 }
17144 "#
17145 .unindent(),
17146 );
17147
17148 cx.update_editor(|editor, window, cx| {
17149 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17150 });
17151
17152 cx.assert_editor_state(
17153 &r#"
17154 use some::modified;
17155
17156 ˇ
17157 fn main() {
17158 println!("hello there");
17159
17160 println!("around the");
17161 println!("world");
17162 }
17163 "#
17164 .unindent(),
17165 );
17166
17167 cx.update_editor(|editor, window, cx| {
17168 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17169 });
17170
17171 cx.assert_editor_state(
17172 &r#"
17173 ˇuse some::modified;
17174
17175
17176 fn main() {
17177 println!("hello there");
17178
17179 println!("around the");
17180 println!("world");
17181 }
17182 "#
17183 .unindent(),
17184 );
17185
17186 cx.update_editor(|editor, window, cx| {
17187 for _ in 0..2 {
17188 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17189 }
17190 });
17191
17192 cx.assert_editor_state(
17193 &r#"
17194 use some::modified;
17195
17196
17197 fn main() {
17198 ˇ println!("hello there");
17199
17200 println!("around the");
17201 println!("world");
17202 }
17203 "#
17204 .unindent(),
17205 );
17206
17207 cx.update_editor(|editor, window, cx| {
17208 editor.fold(&Fold, window, cx);
17209 });
17210
17211 cx.update_editor(|editor, window, cx| {
17212 editor.go_to_next_hunk(&GoToHunk, window, cx);
17213 });
17214
17215 cx.assert_editor_state(
17216 &r#"
17217 ˇuse some::modified;
17218
17219
17220 fn main() {
17221 println!("hello there");
17222
17223 println!("around the");
17224 println!("world");
17225 }
17226 "#
17227 .unindent(),
17228 );
17229}
17230
17231#[test]
17232fn test_split_words() {
17233 fn split(text: &str) -> Vec<&str> {
17234 split_words(text).collect()
17235 }
17236
17237 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17238 assert_eq!(split("hello_world"), &["hello_", "world"]);
17239 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17240 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17241 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17242 assert_eq!(split("helloworld"), &["helloworld"]);
17243
17244 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17245}
17246
17247#[gpui::test]
17248async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17249 init_test(cx, |_| {});
17250
17251 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17252 let mut assert = |before, after| {
17253 let _state_context = cx.set_state(before);
17254 cx.run_until_parked();
17255 cx.update_editor(|editor, window, cx| {
17256 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17257 });
17258 cx.run_until_parked();
17259 cx.assert_editor_state(after);
17260 };
17261
17262 // Outside bracket jumps to outside of matching bracket
17263 assert("console.logˇ(var);", "console.log(var)ˇ;");
17264 assert("console.log(var)ˇ;", "console.logˇ(var);");
17265
17266 // Inside bracket jumps to inside of matching bracket
17267 assert("console.log(ˇvar);", "console.log(varˇ);");
17268 assert("console.log(varˇ);", "console.log(ˇvar);");
17269
17270 // When outside a bracket and inside, favor jumping to the inside bracket
17271 assert(
17272 "console.log('foo', [1, 2, 3]ˇ);",
17273 "console.log(ˇ'foo', [1, 2, 3]);",
17274 );
17275 assert(
17276 "console.log(ˇ'foo', [1, 2, 3]);",
17277 "console.log('foo', [1, 2, 3]ˇ);",
17278 );
17279
17280 // Bias forward if two options are equally likely
17281 assert(
17282 "let result = curried_fun()ˇ();",
17283 "let result = curried_fun()()ˇ;",
17284 );
17285
17286 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17287 assert(
17288 indoc! {"
17289 function test() {
17290 console.log('test')ˇ
17291 }"},
17292 indoc! {"
17293 function test() {
17294 console.logˇ('test')
17295 }"},
17296 );
17297}
17298
17299#[gpui::test]
17300async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17301 init_test(cx, |_| {});
17302
17303 let fs = FakeFs::new(cx.executor());
17304 fs.insert_tree(
17305 path!("/a"),
17306 json!({
17307 "main.rs": "fn main() { let a = 5; }",
17308 "other.rs": "// Test file",
17309 }),
17310 )
17311 .await;
17312 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17313
17314 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17315 language_registry.add(Arc::new(Language::new(
17316 LanguageConfig {
17317 name: "Rust".into(),
17318 matcher: LanguageMatcher {
17319 path_suffixes: vec!["rs".to_string()],
17320 ..Default::default()
17321 },
17322 brackets: BracketPairConfig {
17323 pairs: vec![BracketPair {
17324 start: "{".to_string(),
17325 end: "}".to_string(),
17326 close: true,
17327 surround: true,
17328 newline: true,
17329 }],
17330 disabled_scopes_by_bracket_ix: Vec::new(),
17331 },
17332 ..Default::default()
17333 },
17334 Some(tree_sitter_rust::LANGUAGE.into()),
17335 )));
17336 let mut fake_servers = language_registry.register_fake_lsp(
17337 "Rust",
17338 FakeLspAdapter {
17339 capabilities: lsp::ServerCapabilities {
17340 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17341 first_trigger_character: "{".to_string(),
17342 more_trigger_character: None,
17343 }),
17344 ..Default::default()
17345 },
17346 ..Default::default()
17347 },
17348 );
17349
17350 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17351
17352 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17353
17354 let worktree_id = workspace
17355 .update(cx, |workspace, _, cx| {
17356 workspace.project().update(cx, |project, cx| {
17357 project.worktrees(cx).next().unwrap().read(cx).id()
17358 })
17359 })
17360 .unwrap();
17361
17362 let buffer = project
17363 .update(cx, |project, cx| {
17364 project.open_local_buffer(path!("/a/main.rs"), cx)
17365 })
17366 .await
17367 .unwrap();
17368 let editor_handle = workspace
17369 .update(cx, |workspace, window, cx| {
17370 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17371 })
17372 .unwrap()
17373 .await
17374 .unwrap()
17375 .downcast::<Editor>()
17376 .unwrap();
17377
17378 cx.executor().start_waiting();
17379 let fake_server = fake_servers.next().await.unwrap();
17380
17381 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17382 |params, _| async move {
17383 assert_eq!(
17384 params.text_document_position.text_document.uri,
17385 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17386 );
17387 assert_eq!(
17388 params.text_document_position.position,
17389 lsp::Position::new(0, 21),
17390 );
17391
17392 Ok(Some(vec![lsp::TextEdit {
17393 new_text: "]".to_string(),
17394 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17395 }]))
17396 },
17397 );
17398
17399 editor_handle.update_in(cx, |editor, window, cx| {
17400 window.focus(&editor.focus_handle(cx));
17401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17402 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17403 });
17404 editor.handle_input("{", window, cx);
17405 });
17406
17407 cx.executor().run_until_parked();
17408
17409 buffer.update(cx, |buffer, _| {
17410 assert_eq!(
17411 buffer.text(),
17412 "fn main() { let a = {5}; }",
17413 "No extra braces from on type formatting should appear in the buffer"
17414 )
17415 });
17416}
17417
17418#[gpui::test(iterations = 20, seeds(31))]
17419async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17420 init_test(cx, |_| {});
17421
17422 let mut cx = EditorLspTestContext::new_rust(
17423 lsp::ServerCapabilities {
17424 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17425 first_trigger_character: ".".to_string(),
17426 more_trigger_character: None,
17427 }),
17428 ..Default::default()
17429 },
17430 cx,
17431 )
17432 .await;
17433
17434 cx.update_buffer(|buffer, _| {
17435 // This causes autoindent to be async.
17436 buffer.set_sync_parse_timeout(Duration::ZERO)
17437 });
17438
17439 cx.set_state("fn c() {\n d()ˇ\n}\n");
17440 cx.simulate_keystroke("\n");
17441 cx.run_until_parked();
17442
17443 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17444 let mut request =
17445 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17446 let buffer_cloned = buffer_cloned.clone();
17447 async move {
17448 buffer_cloned.update(&mut cx, |buffer, _| {
17449 assert_eq!(
17450 buffer.text(),
17451 "fn c() {\n d()\n .\n}\n",
17452 "OnTypeFormatting should triggered after autoindent applied"
17453 )
17454 })?;
17455
17456 Ok(Some(vec![]))
17457 }
17458 });
17459
17460 cx.simulate_keystroke(".");
17461 cx.run_until_parked();
17462
17463 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17464 assert!(request.next().await.is_some());
17465 request.close();
17466 assert!(request.next().await.is_none());
17467}
17468
17469#[gpui::test]
17470async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17471 init_test(cx, |_| {});
17472
17473 let fs = FakeFs::new(cx.executor());
17474 fs.insert_tree(
17475 path!("/a"),
17476 json!({
17477 "main.rs": "fn main() { let a = 5; }",
17478 "other.rs": "// Test file",
17479 }),
17480 )
17481 .await;
17482
17483 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17484
17485 let server_restarts = Arc::new(AtomicUsize::new(0));
17486 let closure_restarts = Arc::clone(&server_restarts);
17487 let language_server_name = "test language server";
17488 let language_name: LanguageName = "Rust".into();
17489
17490 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17491 language_registry.add(Arc::new(Language::new(
17492 LanguageConfig {
17493 name: language_name.clone(),
17494 matcher: LanguageMatcher {
17495 path_suffixes: vec!["rs".to_string()],
17496 ..Default::default()
17497 },
17498 ..Default::default()
17499 },
17500 Some(tree_sitter_rust::LANGUAGE.into()),
17501 )));
17502 let mut fake_servers = language_registry.register_fake_lsp(
17503 "Rust",
17504 FakeLspAdapter {
17505 name: language_server_name,
17506 initialization_options: Some(json!({
17507 "testOptionValue": true
17508 })),
17509 initializer: Some(Box::new(move |fake_server| {
17510 let task_restarts = Arc::clone(&closure_restarts);
17511 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17512 task_restarts.fetch_add(1, atomic::Ordering::Release);
17513 futures::future::ready(Ok(()))
17514 });
17515 })),
17516 ..Default::default()
17517 },
17518 );
17519
17520 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17521 let _buffer = project
17522 .update(cx, |project, cx| {
17523 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17524 })
17525 .await
17526 .unwrap();
17527 let _fake_server = fake_servers.next().await.unwrap();
17528 update_test_language_settings(cx, |language_settings| {
17529 language_settings.languages.0.insert(
17530 language_name.clone().0,
17531 LanguageSettingsContent {
17532 tab_size: NonZeroU32::new(8),
17533 ..Default::default()
17534 },
17535 );
17536 });
17537 cx.executor().run_until_parked();
17538 assert_eq!(
17539 server_restarts.load(atomic::Ordering::Acquire),
17540 0,
17541 "Should not restart LSP server on an unrelated change"
17542 );
17543
17544 update_test_project_settings(cx, |project_settings| {
17545 project_settings.lsp.insert(
17546 "Some other server name".into(),
17547 LspSettings {
17548 binary: None,
17549 settings: None,
17550 initialization_options: Some(json!({
17551 "some other init value": false
17552 })),
17553 enable_lsp_tasks: false,
17554 fetch: None,
17555 },
17556 );
17557 });
17558 cx.executor().run_until_parked();
17559 assert_eq!(
17560 server_restarts.load(atomic::Ordering::Acquire),
17561 0,
17562 "Should not restart LSP server on an unrelated LSP settings change"
17563 );
17564
17565 update_test_project_settings(cx, |project_settings| {
17566 project_settings.lsp.insert(
17567 language_server_name.into(),
17568 LspSettings {
17569 binary: None,
17570 settings: None,
17571 initialization_options: Some(json!({
17572 "anotherInitValue": false
17573 })),
17574 enable_lsp_tasks: false,
17575 fetch: None,
17576 },
17577 );
17578 });
17579 cx.executor().run_until_parked();
17580 assert_eq!(
17581 server_restarts.load(atomic::Ordering::Acquire),
17582 1,
17583 "Should restart LSP server on a related LSP settings change"
17584 );
17585
17586 update_test_project_settings(cx, |project_settings| {
17587 project_settings.lsp.insert(
17588 language_server_name.into(),
17589 LspSettings {
17590 binary: None,
17591 settings: None,
17592 initialization_options: Some(json!({
17593 "anotherInitValue": false
17594 })),
17595 enable_lsp_tasks: false,
17596 fetch: None,
17597 },
17598 );
17599 });
17600 cx.executor().run_until_parked();
17601 assert_eq!(
17602 server_restarts.load(atomic::Ordering::Acquire),
17603 1,
17604 "Should not restart LSP server on a related LSP settings change that is the same"
17605 );
17606
17607 update_test_project_settings(cx, |project_settings| {
17608 project_settings.lsp.insert(
17609 language_server_name.into(),
17610 LspSettings {
17611 binary: None,
17612 settings: None,
17613 initialization_options: None,
17614 enable_lsp_tasks: false,
17615 fetch: None,
17616 },
17617 );
17618 });
17619 cx.executor().run_until_parked();
17620 assert_eq!(
17621 server_restarts.load(atomic::Ordering::Acquire),
17622 2,
17623 "Should restart LSP server on another related LSP settings change"
17624 );
17625}
17626
17627#[gpui::test]
17628async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17629 init_test(cx, |_| {});
17630
17631 let mut cx = EditorLspTestContext::new_rust(
17632 lsp::ServerCapabilities {
17633 completion_provider: Some(lsp::CompletionOptions {
17634 trigger_characters: Some(vec![".".to_string()]),
17635 resolve_provider: Some(true),
17636 ..Default::default()
17637 }),
17638 ..Default::default()
17639 },
17640 cx,
17641 )
17642 .await;
17643
17644 cx.set_state("fn main() { let a = 2ˇ; }");
17645 cx.simulate_keystroke(".");
17646 let completion_item = lsp::CompletionItem {
17647 label: "some".into(),
17648 kind: Some(lsp::CompletionItemKind::SNIPPET),
17649 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17650 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17651 kind: lsp::MarkupKind::Markdown,
17652 value: "```rust\nSome(2)\n```".to_string(),
17653 })),
17654 deprecated: Some(false),
17655 sort_text: Some("fffffff2".to_string()),
17656 filter_text: Some("some".to_string()),
17657 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17658 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17659 range: lsp::Range {
17660 start: lsp::Position {
17661 line: 0,
17662 character: 22,
17663 },
17664 end: lsp::Position {
17665 line: 0,
17666 character: 22,
17667 },
17668 },
17669 new_text: "Some(2)".to_string(),
17670 })),
17671 additional_text_edits: Some(vec![lsp::TextEdit {
17672 range: lsp::Range {
17673 start: lsp::Position {
17674 line: 0,
17675 character: 20,
17676 },
17677 end: lsp::Position {
17678 line: 0,
17679 character: 22,
17680 },
17681 },
17682 new_text: "".to_string(),
17683 }]),
17684 ..Default::default()
17685 };
17686
17687 let closure_completion_item = completion_item.clone();
17688 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17689 let task_completion_item = closure_completion_item.clone();
17690 async move {
17691 Ok(Some(lsp::CompletionResponse::Array(vec![
17692 task_completion_item,
17693 ])))
17694 }
17695 });
17696
17697 request.next().await;
17698
17699 cx.condition(|editor, _| editor.context_menu_visible())
17700 .await;
17701 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17702 editor
17703 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17704 .unwrap()
17705 });
17706 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17707
17708 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17709 let task_completion_item = completion_item.clone();
17710 async move { Ok(task_completion_item) }
17711 })
17712 .next()
17713 .await
17714 .unwrap();
17715 apply_additional_edits.await.unwrap();
17716 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17717}
17718
17719#[gpui::test]
17720async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17721 init_test(cx, |_| {});
17722
17723 let mut cx = EditorLspTestContext::new_rust(
17724 lsp::ServerCapabilities {
17725 completion_provider: Some(lsp::CompletionOptions {
17726 trigger_characters: Some(vec![".".to_string()]),
17727 resolve_provider: Some(true),
17728 ..Default::default()
17729 }),
17730 ..Default::default()
17731 },
17732 cx,
17733 )
17734 .await;
17735
17736 cx.set_state("fn main() { let a = 2ˇ; }");
17737 cx.simulate_keystroke(".");
17738
17739 let item1 = lsp::CompletionItem {
17740 label: "method id()".to_string(),
17741 filter_text: Some("id".to_string()),
17742 detail: None,
17743 documentation: None,
17744 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17745 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17746 new_text: ".id".to_string(),
17747 })),
17748 ..lsp::CompletionItem::default()
17749 };
17750
17751 let item2 = lsp::CompletionItem {
17752 label: "other".to_string(),
17753 filter_text: Some("other".to_string()),
17754 detail: None,
17755 documentation: None,
17756 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17757 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17758 new_text: ".other".to_string(),
17759 })),
17760 ..lsp::CompletionItem::default()
17761 };
17762
17763 let item1 = item1.clone();
17764 cx.set_request_handler::<lsp::request::Completion, _, _>({
17765 let item1 = item1.clone();
17766 move |_, _, _| {
17767 let item1 = item1.clone();
17768 let item2 = item2.clone();
17769 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17770 }
17771 })
17772 .next()
17773 .await;
17774
17775 cx.condition(|editor, _| editor.context_menu_visible())
17776 .await;
17777 cx.update_editor(|editor, _, _| {
17778 let context_menu = editor.context_menu.borrow_mut();
17779 let context_menu = context_menu
17780 .as_ref()
17781 .expect("Should have the context menu deployed");
17782 match context_menu {
17783 CodeContextMenu::Completions(completions_menu) => {
17784 let completions = completions_menu.completions.borrow_mut();
17785 assert_eq!(
17786 completions
17787 .iter()
17788 .map(|completion| &completion.label.text)
17789 .collect::<Vec<_>>(),
17790 vec!["method id()", "other"]
17791 )
17792 }
17793 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17794 }
17795 });
17796
17797 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17798 let item1 = item1.clone();
17799 move |_, item_to_resolve, _| {
17800 let item1 = item1.clone();
17801 async move {
17802 if item1 == item_to_resolve {
17803 Ok(lsp::CompletionItem {
17804 label: "method id()".to_string(),
17805 filter_text: Some("id".to_string()),
17806 detail: Some("Now resolved!".to_string()),
17807 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17808 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17809 range: lsp::Range::new(
17810 lsp::Position::new(0, 22),
17811 lsp::Position::new(0, 22),
17812 ),
17813 new_text: ".id".to_string(),
17814 })),
17815 ..lsp::CompletionItem::default()
17816 })
17817 } else {
17818 Ok(item_to_resolve)
17819 }
17820 }
17821 }
17822 })
17823 .next()
17824 .await
17825 .unwrap();
17826 cx.run_until_parked();
17827
17828 cx.update_editor(|editor, window, cx| {
17829 editor.context_menu_next(&Default::default(), window, cx);
17830 });
17831
17832 cx.update_editor(|editor, _, _| {
17833 let context_menu = editor.context_menu.borrow_mut();
17834 let context_menu = context_menu
17835 .as_ref()
17836 .expect("Should have the context menu deployed");
17837 match context_menu {
17838 CodeContextMenu::Completions(completions_menu) => {
17839 let completions = completions_menu.completions.borrow_mut();
17840 assert_eq!(
17841 completions
17842 .iter()
17843 .map(|completion| &completion.label.text)
17844 .collect::<Vec<_>>(),
17845 vec!["method id() Now resolved!", "other"],
17846 "Should update first completion label, but not second as the filter text did not match."
17847 );
17848 }
17849 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17850 }
17851 });
17852}
17853
17854#[gpui::test]
17855async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17856 init_test(cx, |_| {});
17857 let mut cx = EditorLspTestContext::new_rust(
17858 lsp::ServerCapabilities {
17859 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17860 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17861 completion_provider: Some(lsp::CompletionOptions {
17862 resolve_provider: Some(true),
17863 ..Default::default()
17864 }),
17865 ..Default::default()
17866 },
17867 cx,
17868 )
17869 .await;
17870 cx.set_state(indoc! {"
17871 struct TestStruct {
17872 field: i32
17873 }
17874
17875 fn mainˇ() {
17876 let unused_var = 42;
17877 let test_struct = TestStruct { field: 42 };
17878 }
17879 "});
17880 let symbol_range = cx.lsp_range(indoc! {"
17881 struct TestStruct {
17882 field: i32
17883 }
17884
17885 «fn main»() {
17886 let unused_var = 42;
17887 let test_struct = TestStruct { field: 42 };
17888 }
17889 "});
17890 let mut hover_requests =
17891 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17892 Ok(Some(lsp::Hover {
17893 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17894 kind: lsp::MarkupKind::Markdown,
17895 value: "Function documentation".to_string(),
17896 }),
17897 range: Some(symbol_range),
17898 }))
17899 });
17900
17901 // Case 1: Test that code action menu hide hover popover
17902 cx.dispatch_action(Hover);
17903 hover_requests.next().await;
17904 cx.condition(|editor, _| editor.hover_state.visible()).await;
17905 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17906 move |_, _, _| async move {
17907 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17908 lsp::CodeAction {
17909 title: "Remove unused variable".to_string(),
17910 kind: Some(CodeActionKind::QUICKFIX),
17911 edit: Some(lsp::WorkspaceEdit {
17912 changes: Some(
17913 [(
17914 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17915 vec![lsp::TextEdit {
17916 range: lsp::Range::new(
17917 lsp::Position::new(5, 4),
17918 lsp::Position::new(5, 27),
17919 ),
17920 new_text: "".to_string(),
17921 }],
17922 )]
17923 .into_iter()
17924 .collect(),
17925 ),
17926 ..Default::default()
17927 }),
17928 ..Default::default()
17929 },
17930 )]))
17931 },
17932 );
17933 cx.update_editor(|editor, window, cx| {
17934 editor.toggle_code_actions(
17935 &ToggleCodeActions {
17936 deployed_from: None,
17937 quick_launch: false,
17938 },
17939 window,
17940 cx,
17941 );
17942 });
17943 code_action_requests.next().await;
17944 cx.run_until_parked();
17945 cx.condition(|editor, _| editor.context_menu_visible())
17946 .await;
17947 cx.update_editor(|editor, _, _| {
17948 assert!(
17949 !editor.hover_state.visible(),
17950 "Hover popover should be hidden when code action menu is shown"
17951 );
17952 // Hide code actions
17953 editor.context_menu.take();
17954 });
17955
17956 // Case 2: Test that code completions hide hover popover
17957 cx.dispatch_action(Hover);
17958 hover_requests.next().await;
17959 cx.condition(|editor, _| editor.hover_state.visible()).await;
17960 let counter = Arc::new(AtomicUsize::new(0));
17961 let mut completion_requests =
17962 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17963 let counter = counter.clone();
17964 async move {
17965 counter.fetch_add(1, atomic::Ordering::Release);
17966 Ok(Some(lsp::CompletionResponse::Array(vec![
17967 lsp::CompletionItem {
17968 label: "main".into(),
17969 kind: Some(lsp::CompletionItemKind::FUNCTION),
17970 detail: Some("() -> ()".to_string()),
17971 ..Default::default()
17972 },
17973 lsp::CompletionItem {
17974 label: "TestStruct".into(),
17975 kind: Some(lsp::CompletionItemKind::STRUCT),
17976 detail: Some("struct TestStruct".to_string()),
17977 ..Default::default()
17978 },
17979 ])))
17980 }
17981 });
17982 cx.update_editor(|editor, window, cx| {
17983 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17984 });
17985 completion_requests.next().await;
17986 cx.condition(|editor, _| editor.context_menu_visible())
17987 .await;
17988 cx.update_editor(|editor, _, _| {
17989 assert!(
17990 !editor.hover_state.visible(),
17991 "Hover popover should be hidden when completion menu is shown"
17992 );
17993 });
17994}
17995
17996#[gpui::test]
17997async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17998 init_test(cx, |_| {});
17999
18000 let mut cx = EditorLspTestContext::new_rust(
18001 lsp::ServerCapabilities {
18002 completion_provider: Some(lsp::CompletionOptions {
18003 trigger_characters: Some(vec![".".to_string()]),
18004 resolve_provider: Some(true),
18005 ..Default::default()
18006 }),
18007 ..Default::default()
18008 },
18009 cx,
18010 )
18011 .await;
18012
18013 cx.set_state("fn main() { let a = 2ˇ; }");
18014 cx.simulate_keystroke(".");
18015
18016 let unresolved_item_1 = lsp::CompletionItem {
18017 label: "id".to_string(),
18018 filter_text: Some("id".to_string()),
18019 detail: None,
18020 documentation: None,
18021 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18022 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18023 new_text: ".id".to_string(),
18024 })),
18025 ..lsp::CompletionItem::default()
18026 };
18027 let resolved_item_1 = lsp::CompletionItem {
18028 additional_text_edits: Some(vec![lsp::TextEdit {
18029 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18030 new_text: "!!".to_string(),
18031 }]),
18032 ..unresolved_item_1.clone()
18033 };
18034 let unresolved_item_2 = lsp::CompletionItem {
18035 label: "other".to_string(),
18036 filter_text: Some("other".to_string()),
18037 detail: None,
18038 documentation: None,
18039 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18040 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18041 new_text: ".other".to_string(),
18042 })),
18043 ..lsp::CompletionItem::default()
18044 };
18045 let resolved_item_2 = lsp::CompletionItem {
18046 additional_text_edits: Some(vec![lsp::TextEdit {
18047 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18048 new_text: "??".to_string(),
18049 }]),
18050 ..unresolved_item_2.clone()
18051 };
18052
18053 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18054 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18055 cx.lsp
18056 .server
18057 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18058 let unresolved_item_1 = unresolved_item_1.clone();
18059 let resolved_item_1 = resolved_item_1.clone();
18060 let unresolved_item_2 = unresolved_item_2.clone();
18061 let resolved_item_2 = resolved_item_2.clone();
18062 let resolve_requests_1 = resolve_requests_1.clone();
18063 let resolve_requests_2 = resolve_requests_2.clone();
18064 move |unresolved_request, _| {
18065 let unresolved_item_1 = unresolved_item_1.clone();
18066 let resolved_item_1 = resolved_item_1.clone();
18067 let unresolved_item_2 = unresolved_item_2.clone();
18068 let resolved_item_2 = resolved_item_2.clone();
18069 let resolve_requests_1 = resolve_requests_1.clone();
18070 let resolve_requests_2 = resolve_requests_2.clone();
18071 async move {
18072 if unresolved_request == unresolved_item_1 {
18073 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18074 Ok(resolved_item_1.clone())
18075 } else if unresolved_request == unresolved_item_2 {
18076 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18077 Ok(resolved_item_2.clone())
18078 } else {
18079 panic!("Unexpected completion item {unresolved_request:?}")
18080 }
18081 }
18082 }
18083 })
18084 .detach();
18085
18086 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18087 let unresolved_item_1 = unresolved_item_1.clone();
18088 let unresolved_item_2 = unresolved_item_2.clone();
18089 async move {
18090 Ok(Some(lsp::CompletionResponse::Array(vec![
18091 unresolved_item_1,
18092 unresolved_item_2,
18093 ])))
18094 }
18095 })
18096 .next()
18097 .await;
18098
18099 cx.condition(|editor, _| editor.context_menu_visible())
18100 .await;
18101 cx.update_editor(|editor, _, _| {
18102 let context_menu = editor.context_menu.borrow_mut();
18103 let context_menu = context_menu
18104 .as_ref()
18105 .expect("Should have the context menu deployed");
18106 match context_menu {
18107 CodeContextMenu::Completions(completions_menu) => {
18108 let completions = completions_menu.completions.borrow_mut();
18109 assert_eq!(
18110 completions
18111 .iter()
18112 .map(|completion| &completion.label.text)
18113 .collect::<Vec<_>>(),
18114 vec!["id", "other"]
18115 )
18116 }
18117 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18118 }
18119 });
18120 cx.run_until_parked();
18121
18122 cx.update_editor(|editor, window, cx| {
18123 editor.context_menu_next(&ContextMenuNext, window, cx);
18124 });
18125 cx.run_until_parked();
18126 cx.update_editor(|editor, window, cx| {
18127 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18128 });
18129 cx.run_until_parked();
18130 cx.update_editor(|editor, window, cx| {
18131 editor.context_menu_next(&ContextMenuNext, window, cx);
18132 });
18133 cx.run_until_parked();
18134 cx.update_editor(|editor, window, cx| {
18135 editor
18136 .compose_completion(&ComposeCompletion::default(), window, cx)
18137 .expect("No task returned")
18138 })
18139 .await
18140 .expect("Completion failed");
18141 cx.run_until_parked();
18142
18143 cx.update_editor(|editor, _, cx| {
18144 assert_eq!(
18145 resolve_requests_1.load(atomic::Ordering::Acquire),
18146 1,
18147 "Should always resolve once despite multiple selections"
18148 );
18149 assert_eq!(
18150 resolve_requests_2.load(atomic::Ordering::Acquire),
18151 1,
18152 "Should always resolve once after multiple selections and applying the completion"
18153 );
18154 assert_eq!(
18155 editor.text(cx),
18156 "fn main() { let a = ??.other; }",
18157 "Should use resolved data when applying the completion"
18158 );
18159 });
18160}
18161
18162#[gpui::test]
18163async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18164 init_test(cx, |_| {});
18165
18166 let item_0 = lsp::CompletionItem {
18167 label: "abs".into(),
18168 insert_text: Some("abs".into()),
18169 data: Some(json!({ "very": "special"})),
18170 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18171 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18172 lsp::InsertReplaceEdit {
18173 new_text: "abs".to_string(),
18174 insert: lsp::Range::default(),
18175 replace: lsp::Range::default(),
18176 },
18177 )),
18178 ..lsp::CompletionItem::default()
18179 };
18180 let items = iter::once(item_0.clone())
18181 .chain((11..51).map(|i| lsp::CompletionItem {
18182 label: format!("item_{}", i),
18183 insert_text: Some(format!("item_{}", i)),
18184 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18185 ..lsp::CompletionItem::default()
18186 }))
18187 .collect::<Vec<_>>();
18188
18189 let default_commit_characters = vec!["?".to_string()];
18190 let default_data = json!({ "default": "data"});
18191 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18192 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18193 let default_edit_range = lsp::Range {
18194 start: lsp::Position {
18195 line: 0,
18196 character: 5,
18197 },
18198 end: lsp::Position {
18199 line: 0,
18200 character: 5,
18201 },
18202 };
18203
18204 let mut cx = EditorLspTestContext::new_rust(
18205 lsp::ServerCapabilities {
18206 completion_provider: Some(lsp::CompletionOptions {
18207 trigger_characters: Some(vec![".".to_string()]),
18208 resolve_provider: Some(true),
18209 ..Default::default()
18210 }),
18211 ..Default::default()
18212 },
18213 cx,
18214 )
18215 .await;
18216
18217 cx.set_state("fn main() { let a = 2ˇ; }");
18218 cx.simulate_keystroke(".");
18219
18220 let completion_data = default_data.clone();
18221 let completion_characters = default_commit_characters.clone();
18222 let completion_items = items.clone();
18223 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18224 let default_data = completion_data.clone();
18225 let default_commit_characters = completion_characters.clone();
18226 let items = completion_items.clone();
18227 async move {
18228 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18229 items,
18230 item_defaults: Some(lsp::CompletionListItemDefaults {
18231 data: Some(default_data.clone()),
18232 commit_characters: Some(default_commit_characters.clone()),
18233 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18234 default_edit_range,
18235 )),
18236 insert_text_format: Some(default_insert_text_format),
18237 insert_text_mode: Some(default_insert_text_mode),
18238 }),
18239 ..lsp::CompletionList::default()
18240 })))
18241 }
18242 })
18243 .next()
18244 .await;
18245
18246 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18247 cx.lsp
18248 .server
18249 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18250 let closure_resolved_items = resolved_items.clone();
18251 move |item_to_resolve, _| {
18252 let closure_resolved_items = closure_resolved_items.clone();
18253 async move {
18254 closure_resolved_items.lock().push(item_to_resolve.clone());
18255 Ok(item_to_resolve)
18256 }
18257 }
18258 })
18259 .detach();
18260
18261 cx.condition(|editor, _| editor.context_menu_visible())
18262 .await;
18263 cx.run_until_parked();
18264 cx.update_editor(|editor, _, _| {
18265 let menu = editor.context_menu.borrow_mut();
18266 match menu.as_ref().expect("should have the completions menu") {
18267 CodeContextMenu::Completions(completions_menu) => {
18268 assert_eq!(
18269 completions_menu
18270 .entries
18271 .borrow()
18272 .iter()
18273 .map(|mat| mat.string.clone())
18274 .collect::<Vec<String>>(),
18275 items
18276 .iter()
18277 .map(|completion| completion.label.clone())
18278 .collect::<Vec<String>>()
18279 );
18280 }
18281 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18282 }
18283 });
18284 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18285 // with 4 from the end.
18286 assert_eq!(
18287 *resolved_items.lock(),
18288 [&items[0..16], &items[items.len() - 4..items.len()]]
18289 .concat()
18290 .iter()
18291 .cloned()
18292 .map(|mut item| {
18293 if item.data.is_none() {
18294 item.data = Some(default_data.clone());
18295 }
18296 item
18297 })
18298 .collect::<Vec<lsp::CompletionItem>>(),
18299 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18300 );
18301 resolved_items.lock().clear();
18302
18303 cx.update_editor(|editor, window, cx| {
18304 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18305 });
18306 cx.run_until_parked();
18307 // Completions that have already been resolved are skipped.
18308 assert_eq!(
18309 *resolved_items.lock(),
18310 items[items.len() - 17..items.len() - 4]
18311 .iter()
18312 .cloned()
18313 .map(|mut item| {
18314 if item.data.is_none() {
18315 item.data = Some(default_data.clone());
18316 }
18317 item
18318 })
18319 .collect::<Vec<lsp::CompletionItem>>()
18320 );
18321 resolved_items.lock().clear();
18322}
18323
18324#[gpui::test]
18325async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18326 init_test(cx, |_| {});
18327
18328 let mut cx = EditorLspTestContext::new(
18329 Language::new(
18330 LanguageConfig {
18331 matcher: LanguageMatcher {
18332 path_suffixes: vec!["jsx".into()],
18333 ..Default::default()
18334 },
18335 overrides: [(
18336 "element".into(),
18337 LanguageConfigOverride {
18338 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18339 ..Default::default()
18340 },
18341 )]
18342 .into_iter()
18343 .collect(),
18344 ..Default::default()
18345 },
18346 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18347 )
18348 .with_override_query("(jsx_self_closing_element) @element")
18349 .unwrap(),
18350 lsp::ServerCapabilities {
18351 completion_provider: Some(lsp::CompletionOptions {
18352 trigger_characters: Some(vec![":".to_string()]),
18353 ..Default::default()
18354 }),
18355 ..Default::default()
18356 },
18357 cx,
18358 )
18359 .await;
18360
18361 cx.lsp
18362 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18363 Ok(Some(lsp::CompletionResponse::Array(vec![
18364 lsp::CompletionItem {
18365 label: "bg-blue".into(),
18366 ..Default::default()
18367 },
18368 lsp::CompletionItem {
18369 label: "bg-red".into(),
18370 ..Default::default()
18371 },
18372 lsp::CompletionItem {
18373 label: "bg-yellow".into(),
18374 ..Default::default()
18375 },
18376 ])))
18377 });
18378
18379 cx.set_state(r#"<p class="bgˇ" />"#);
18380
18381 // Trigger completion when typing a dash, because the dash is an extra
18382 // word character in the 'element' scope, which contains the cursor.
18383 cx.simulate_keystroke("-");
18384 cx.executor().run_until_parked();
18385 cx.update_editor(|editor, _, _| {
18386 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18387 {
18388 assert_eq!(
18389 completion_menu_entries(menu),
18390 &["bg-blue", "bg-red", "bg-yellow"]
18391 );
18392 } else {
18393 panic!("expected completion menu to be open");
18394 }
18395 });
18396
18397 cx.simulate_keystroke("l");
18398 cx.executor().run_until_parked();
18399 cx.update_editor(|editor, _, _| {
18400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18401 {
18402 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18403 } else {
18404 panic!("expected completion menu to be open");
18405 }
18406 });
18407
18408 // When filtering completions, consider the character after the '-' to
18409 // be the start of a subword.
18410 cx.set_state(r#"<p class="yelˇ" />"#);
18411 cx.simulate_keystroke("l");
18412 cx.executor().run_until_parked();
18413 cx.update_editor(|editor, _, _| {
18414 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18415 {
18416 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18417 } else {
18418 panic!("expected completion menu to be open");
18419 }
18420 });
18421}
18422
18423fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18424 let entries = menu.entries.borrow();
18425 entries.iter().map(|mat| mat.string.clone()).collect()
18426}
18427
18428#[gpui::test]
18429async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18430 init_test(cx, |settings| {
18431 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18432 });
18433
18434 let fs = FakeFs::new(cx.executor());
18435 fs.insert_file(path!("/file.ts"), Default::default()).await;
18436
18437 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18438 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18439
18440 language_registry.add(Arc::new(Language::new(
18441 LanguageConfig {
18442 name: "TypeScript".into(),
18443 matcher: LanguageMatcher {
18444 path_suffixes: vec!["ts".to_string()],
18445 ..Default::default()
18446 },
18447 ..Default::default()
18448 },
18449 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18450 )));
18451 update_test_language_settings(cx, |settings| {
18452 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18453 });
18454
18455 let test_plugin = "test_plugin";
18456 let _ = language_registry.register_fake_lsp(
18457 "TypeScript",
18458 FakeLspAdapter {
18459 prettier_plugins: vec![test_plugin],
18460 ..Default::default()
18461 },
18462 );
18463
18464 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18465 let buffer = project
18466 .update(cx, |project, cx| {
18467 project.open_local_buffer(path!("/file.ts"), cx)
18468 })
18469 .await
18470 .unwrap();
18471
18472 let buffer_text = "one\ntwo\nthree\n";
18473 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18474 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18475 editor.update_in(cx, |editor, window, cx| {
18476 editor.set_text(buffer_text, window, cx)
18477 });
18478
18479 editor
18480 .update_in(cx, |editor, window, cx| {
18481 editor.perform_format(
18482 project.clone(),
18483 FormatTrigger::Manual,
18484 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18485 window,
18486 cx,
18487 )
18488 })
18489 .unwrap()
18490 .await;
18491 assert_eq!(
18492 editor.update(cx, |editor, cx| editor.text(cx)),
18493 buffer_text.to_string() + prettier_format_suffix,
18494 "Test prettier formatting was not applied to the original buffer text",
18495 );
18496
18497 update_test_language_settings(cx, |settings| {
18498 settings.defaults.formatter = Some(FormatterList::default())
18499 });
18500 let format = editor.update_in(cx, |editor, window, cx| {
18501 editor.perform_format(
18502 project.clone(),
18503 FormatTrigger::Manual,
18504 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18505 window,
18506 cx,
18507 )
18508 });
18509 format.await.unwrap();
18510 assert_eq!(
18511 editor.update(cx, |editor, cx| editor.text(cx)),
18512 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18513 "Autoformatting (via test prettier) was not applied to the original buffer text",
18514 );
18515}
18516
18517#[gpui::test]
18518async fn test_addition_reverts(cx: &mut TestAppContext) {
18519 init_test(cx, |_| {});
18520 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18521 let base_text = indoc! {r#"
18522 struct Row;
18523 struct Row1;
18524 struct Row2;
18525
18526 struct Row4;
18527 struct Row5;
18528 struct Row6;
18529
18530 struct Row8;
18531 struct Row9;
18532 struct Row10;"#};
18533
18534 // When addition hunks are not adjacent to carets, no hunk revert is performed
18535 assert_hunk_revert(
18536 indoc! {r#"struct Row;
18537 struct Row1;
18538 struct Row1.1;
18539 struct Row1.2;
18540 struct Row2;ˇ
18541
18542 struct Row4;
18543 struct Row5;
18544 struct Row6;
18545
18546 struct Row8;
18547 ˇstruct Row9;
18548 struct Row9.1;
18549 struct Row9.2;
18550 struct Row9.3;
18551 struct Row10;"#},
18552 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18553 indoc! {r#"struct Row;
18554 struct Row1;
18555 struct Row1.1;
18556 struct Row1.2;
18557 struct Row2;ˇ
18558
18559 struct Row4;
18560 struct Row5;
18561 struct Row6;
18562
18563 struct Row8;
18564 ˇstruct Row9;
18565 struct Row9.1;
18566 struct Row9.2;
18567 struct Row9.3;
18568 struct Row10;"#},
18569 base_text,
18570 &mut cx,
18571 );
18572 // Same for selections
18573 assert_hunk_revert(
18574 indoc! {r#"struct Row;
18575 struct Row1;
18576 struct Row2;
18577 struct Row2.1;
18578 struct Row2.2;
18579 «ˇ
18580 struct Row4;
18581 struct» Row5;
18582 «struct Row6;
18583 ˇ»
18584 struct Row9.1;
18585 struct Row9.2;
18586 struct Row9.3;
18587 struct Row8;
18588 struct Row9;
18589 struct Row10;"#},
18590 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18591 indoc! {r#"struct Row;
18592 struct Row1;
18593 struct Row2;
18594 struct Row2.1;
18595 struct Row2.2;
18596 «ˇ
18597 struct Row4;
18598 struct» Row5;
18599 «struct Row6;
18600 ˇ»
18601 struct Row9.1;
18602 struct Row9.2;
18603 struct Row9.3;
18604 struct Row8;
18605 struct Row9;
18606 struct Row10;"#},
18607 base_text,
18608 &mut cx,
18609 );
18610
18611 // When carets and selections intersect the addition hunks, those are reverted.
18612 // Adjacent carets got merged.
18613 assert_hunk_revert(
18614 indoc! {r#"struct Row;
18615 ˇ// something on the top
18616 struct Row1;
18617 struct Row2;
18618 struct Roˇw3.1;
18619 struct Row2.2;
18620 struct Row2.3;ˇ
18621
18622 struct Row4;
18623 struct ˇRow5.1;
18624 struct Row5.2;
18625 struct «Rowˇ»5.3;
18626 struct Row5;
18627 struct Row6;
18628 ˇ
18629 struct Row9.1;
18630 struct «Rowˇ»9.2;
18631 struct «ˇRow»9.3;
18632 struct Row8;
18633 struct Row9;
18634 «ˇ// something on bottom»
18635 struct Row10;"#},
18636 vec![
18637 DiffHunkStatusKind::Added,
18638 DiffHunkStatusKind::Added,
18639 DiffHunkStatusKind::Added,
18640 DiffHunkStatusKind::Added,
18641 DiffHunkStatusKind::Added,
18642 ],
18643 indoc! {r#"struct Row;
18644 ˇstruct Row1;
18645 struct Row2;
18646 ˇ
18647 struct Row4;
18648 ˇstruct Row5;
18649 struct Row6;
18650 ˇ
18651 ˇstruct Row8;
18652 struct Row9;
18653 ˇstruct Row10;"#},
18654 base_text,
18655 &mut cx,
18656 );
18657}
18658
18659#[gpui::test]
18660async fn test_modification_reverts(cx: &mut TestAppContext) {
18661 init_test(cx, |_| {});
18662 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18663 let base_text = indoc! {r#"
18664 struct Row;
18665 struct Row1;
18666 struct Row2;
18667
18668 struct Row4;
18669 struct Row5;
18670 struct Row6;
18671
18672 struct Row8;
18673 struct Row9;
18674 struct Row10;"#};
18675
18676 // Modification hunks behave the same as the addition ones.
18677 assert_hunk_revert(
18678 indoc! {r#"struct Row;
18679 struct Row1;
18680 struct Row33;
18681 ˇ
18682 struct Row4;
18683 struct Row5;
18684 struct Row6;
18685 ˇ
18686 struct Row99;
18687 struct Row9;
18688 struct Row10;"#},
18689 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18690 indoc! {r#"struct Row;
18691 struct Row1;
18692 struct Row33;
18693 ˇ
18694 struct Row4;
18695 struct Row5;
18696 struct Row6;
18697 ˇ
18698 struct Row99;
18699 struct Row9;
18700 struct Row10;"#},
18701 base_text,
18702 &mut cx,
18703 );
18704 assert_hunk_revert(
18705 indoc! {r#"struct Row;
18706 struct Row1;
18707 struct Row33;
18708 «ˇ
18709 struct Row4;
18710 struct» Row5;
18711 «struct Row6;
18712 ˇ»
18713 struct Row99;
18714 struct Row9;
18715 struct Row10;"#},
18716 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18717 indoc! {r#"struct Row;
18718 struct Row1;
18719 struct Row33;
18720 «ˇ
18721 struct Row4;
18722 struct» Row5;
18723 «struct Row6;
18724 ˇ»
18725 struct Row99;
18726 struct Row9;
18727 struct Row10;"#},
18728 base_text,
18729 &mut cx,
18730 );
18731
18732 assert_hunk_revert(
18733 indoc! {r#"ˇstruct Row1.1;
18734 struct Row1;
18735 «ˇstr»uct Row22;
18736
18737 struct ˇRow44;
18738 struct Row5;
18739 struct «Rˇ»ow66;ˇ
18740
18741 «struˇ»ct Row88;
18742 struct Row9;
18743 struct Row1011;ˇ"#},
18744 vec![
18745 DiffHunkStatusKind::Modified,
18746 DiffHunkStatusKind::Modified,
18747 DiffHunkStatusKind::Modified,
18748 DiffHunkStatusKind::Modified,
18749 DiffHunkStatusKind::Modified,
18750 DiffHunkStatusKind::Modified,
18751 ],
18752 indoc! {r#"struct Row;
18753 ˇstruct Row1;
18754 struct Row2;
18755 ˇ
18756 struct Row4;
18757 ˇstruct Row5;
18758 struct Row6;
18759 ˇ
18760 struct Row8;
18761 ˇstruct Row9;
18762 struct Row10;ˇ"#},
18763 base_text,
18764 &mut cx,
18765 );
18766}
18767
18768#[gpui::test]
18769async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18770 init_test(cx, |_| {});
18771 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18772 let base_text = indoc! {r#"
18773 one
18774
18775 two
18776 three
18777 "#};
18778
18779 cx.set_head_text(base_text);
18780 cx.set_state("\nˇ\n");
18781 cx.executor().run_until_parked();
18782 cx.update_editor(|editor, _window, cx| {
18783 editor.expand_selected_diff_hunks(cx);
18784 });
18785 cx.executor().run_until_parked();
18786 cx.update_editor(|editor, window, cx| {
18787 editor.backspace(&Default::default(), window, cx);
18788 });
18789 cx.run_until_parked();
18790 cx.assert_state_with_diff(
18791 indoc! {r#"
18792
18793 - two
18794 - threeˇ
18795 +
18796 "#}
18797 .to_string(),
18798 );
18799}
18800
18801#[gpui::test]
18802async fn test_deletion_reverts(cx: &mut TestAppContext) {
18803 init_test(cx, |_| {});
18804 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18805 let base_text = indoc! {r#"struct Row;
18806struct Row1;
18807struct Row2;
18808
18809struct Row4;
18810struct Row5;
18811struct Row6;
18812
18813struct Row8;
18814struct Row9;
18815struct Row10;"#};
18816
18817 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18818 assert_hunk_revert(
18819 indoc! {r#"struct Row;
18820 struct Row2;
18821
18822 ˇstruct Row4;
18823 struct Row5;
18824 struct Row6;
18825 ˇ
18826 struct Row8;
18827 struct Row10;"#},
18828 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18829 indoc! {r#"struct Row;
18830 struct Row2;
18831
18832 ˇstruct Row4;
18833 struct Row5;
18834 struct Row6;
18835 ˇ
18836 struct Row8;
18837 struct Row10;"#},
18838 base_text,
18839 &mut cx,
18840 );
18841 assert_hunk_revert(
18842 indoc! {r#"struct Row;
18843 struct Row2;
18844
18845 «ˇstruct Row4;
18846 struct» Row5;
18847 «struct Row6;
18848 ˇ»
18849 struct Row8;
18850 struct Row10;"#},
18851 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18852 indoc! {r#"struct Row;
18853 struct Row2;
18854
18855 «ˇstruct Row4;
18856 struct» Row5;
18857 «struct Row6;
18858 ˇ»
18859 struct Row8;
18860 struct Row10;"#},
18861 base_text,
18862 &mut cx,
18863 );
18864
18865 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18866 assert_hunk_revert(
18867 indoc! {r#"struct Row;
18868 ˇstruct Row2;
18869
18870 struct Row4;
18871 struct Row5;
18872 struct Row6;
18873
18874 struct Row8;ˇ
18875 struct Row10;"#},
18876 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18877 indoc! {r#"struct Row;
18878 struct Row1;
18879 ˇstruct Row2;
18880
18881 struct Row4;
18882 struct Row5;
18883 struct Row6;
18884
18885 struct Row8;ˇ
18886 struct Row9;
18887 struct Row10;"#},
18888 base_text,
18889 &mut cx,
18890 );
18891 assert_hunk_revert(
18892 indoc! {r#"struct Row;
18893 struct Row2«ˇ;
18894 struct Row4;
18895 struct» Row5;
18896 «struct Row6;
18897
18898 struct Row8;ˇ»
18899 struct Row10;"#},
18900 vec![
18901 DiffHunkStatusKind::Deleted,
18902 DiffHunkStatusKind::Deleted,
18903 DiffHunkStatusKind::Deleted,
18904 ],
18905 indoc! {r#"struct Row;
18906 struct Row1;
18907 struct Row2«ˇ;
18908
18909 struct Row4;
18910 struct» Row5;
18911 «struct Row6;
18912
18913 struct Row8;ˇ»
18914 struct Row9;
18915 struct Row10;"#},
18916 base_text,
18917 &mut cx,
18918 );
18919}
18920
18921#[gpui::test]
18922async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18923 init_test(cx, |_| {});
18924
18925 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18926 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18927 let base_text_3 =
18928 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18929
18930 let text_1 = edit_first_char_of_every_line(base_text_1);
18931 let text_2 = edit_first_char_of_every_line(base_text_2);
18932 let text_3 = edit_first_char_of_every_line(base_text_3);
18933
18934 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18935 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18936 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18937
18938 let multibuffer = cx.new(|cx| {
18939 let mut multibuffer = MultiBuffer::new(ReadWrite);
18940 multibuffer.push_excerpts(
18941 buffer_1.clone(),
18942 [
18943 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18944 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18945 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18946 ],
18947 cx,
18948 );
18949 multibuffer.push_excerpts(
18950 buffer_2.clone(),
18951 [
18952 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18953 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18954 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18955 ],
18956 cx,
18957 );
18958 multibuffer.push_excerpts(
18959 buffer_3.clone(),
18960 [
18961 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18962 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18963 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18964 ],
18965 cx,
18966 );
18967 multibuffer
18968 });
18969
18970 let fs = FakeFs::new(cx.executor());
18971 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18972 let (editor, cx) = cx
18973 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18974 editor.update_in(cx, |editor, _window, cx| {
18975 for (buffer, diff_base) in [
18976 (buffer_1.clone(), base_text_1),
18977 (buffer_2.clone(), base_text_2),
18978 (buffer_3.clone(), base_text_3),
18979 ] {
18980 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18981 editor
18982 .buffer
18983 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18984 }
18985 });
18986 cx.executor().run_until_parked();
18987
18988 editor.update_in(cx, |editor, window, cx| {
18989 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}");
18990 editor.select_all(&SelectAll, window, cx);
18991 editor.git_restore(&Default::default(), window, cx);
18992 });
18993 cx.executor().run_until_parked();
18994
18995 // When all ranges are selected, all buffer hunks are reverted.
18996 editor.update(cx, |editor, cx| {
18997 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");
18998 });
18999 buffer_1.update(cx, |buffer, _| {
19000 assert_eq!(buffer.text(), base_text_1);
19001 });
19002 buffer_2.update(cx, |buffer, _| {
19003 assert_eq!(buffer.text(), base_text_2);
19004 });
19005 buffer_3.update(cx, |buffer, _| {
19006 assert_eq!(buffer.text(), base_text_3);
19007 });
19008
19009 editor.update_in(cx, |editor, window, cx| {
19010 editor.undo(&Default::default(), window, cx);
19011 });
19012
19013 editor.update_in(cx, |editor, window, cx| {
19014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19015 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19016 });
19017 editor.git_restore(&Default::default(), window, cx);
19018 });
19019
19020 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19021 // but not affect buffer_2 and its related excerpts.
19022 editor.update(cx, |editor, cx| {
19023 assert_eq!(
19024 editor.text(cx),
19025 "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}"
19026 );
19027 });
19028 buffer_1.update(cx, |buffer, _| {
19029 assert_eq!(buffer.text(), base_text_1);
19030 });
19031 buffer_2.update(cx, |buffer, _| {
19032 assert_eq!(
19033 buffer.text(),
19034 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19035 );
19036 });
19037 buffer_3.update(cx, |buffer, _| {
19038 assert_eq!(
19039 buffer.text(),
19040 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19041 );
19042 });
19043
19044 fn edit_first_char_of_every_line(text: &str) -> String {
19045 text.split('\n')
19046 .map(|line| format!("X{}", &line[1..]))
19047 .collect::<Vec<_>>()
19048 .join("\n")
19049 }
19050}
19051
19052#[gpui::test]
19053async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19054 init_test(cx, |_| {});
19055
19056 let cols = 4;
19057 let rows = 10;
19058 let sample_text_1 = sample_text(rows, cols, 'a');
19059 assert_eq!(
19060 sample_text_1,
19061 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19062 );
19063 let sample_text_2 = sample_text(rows, cols, 'l');
19064 assert_eq!(
19065 sample_text_2,
19066 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19067 );
19068 let sample_text_3 = sample_text(rows, cols, 'v');
19069 assert_eq!(
19070 sample_text_3,
19071 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19072 );
19073
19074 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19075 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19076 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19077
19078 let multi_buffer = cx.new(|cx| {
19079 let mut multibuffer = MultiBuffer::new(ReadWrite);
19080 multibuffer.push_excerpts(
19081 buffer_1.clone(),
19082 [
19083 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19084 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19085 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19086 ],
19087 cx,
19088 );
19089 multibuffer.push_excerpts(
19090 buffer_2.clone(),
19091 [
19092 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19093 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19094 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19095 ],
19096 cx,
19097 );
19098 multibuffer.push_excerpts(
19099 buffer_3.clone(),
19100 [
19101 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19102 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19103 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19104 ],
19105 cx,
19106 );
19107 multibuffer
19108 });
19109
19110 let fs = FakeFs::new(cx.executor());
19111 fs.insert_tree(
19112 "/a",
19113 json!({
19114 "main.rs": sample_text_1,
19115 "other.rs": sample_text_2,
19116 "lib.rs": sample_text_3,
19117 }),
19118 )
19119 .await;
19120 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19121 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19122 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19123 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19124 Editor::new(
19125 EditorMode::full(),
19126 multi_buffer,
19127 Some(project.clone()),
19128 window,
19129 cx,
19130 )
19131 });
19132 let multibuffer_item_id = workspace
19133 .update(cx, |workspace, window, cx| {
19134 assert!(
19135 workspace.active_item(cx).is_none(),
19136 "active item should be None before the first item is added"
19137 );
19138 workspace.add_item_to_active_pane(
19139 Box::new(multi_buffer_editor.clone()),
19140 None,
19141 true,
19142 window,
19143 cx,
19144 );
19145 let active_item = workspace
19146 .active_item(cx)
19147 .expect("should have an active item after adding the multi buffer");
19148 assert_eq!(
19149 active_item.buffer_kind(cx),
19150 ItemBufferKind::Multibuffer,
19151 "A multi buffer was expected to active after adding"
19152 );
19153 active_item.item_id()
19154 })
19155 .unwrap();
19156 cx.executor().run_until_parked();
19157
19158 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19159 editor.change_selections(
19160 SelectionEffects::scroll(Autoscroll::Next),
19161 window,
19162 cx,
19163 |s| s.select_ranges(Some(1..2)),
19164 );
19165 editor.open_excerpts(&OpenExcerpts, window, cx);
19166 });
19167 cx.executor().run_until_parked();
19168 let first_item_id = workspace
19169 .update(cx, |workspace, window, cx| {
19170 let active_item = workspace
19171 .active_item(cx)
19172 .expect("should have an active item after navigating into the 1st buffer");
19173 let first_item_id = active_item.item_id();
19174 assert_ne!(
19175 first_item_id, multibuffer_item_id,
19176 "Should navigate into the 1st buffer and activate it"
19177 );
19178 assert_eq!(
19179 active_item.buffer_kind(cx),
19180 ItemBufferKind::Singleton,
19181 "New active item should be a singleton buffer"
19182 );
19183 assert_eq!(
19184 active_item
19185 .act_as::<Editor>(cx)
19186 .expect("should have navigated into an editor for the 1st buffer")
19187 .read(cx)
19188 .text(cx),
19189 sample_text_1
19190 );
19191
19192 workspace
19193 .go_back(workspace.active_pane().downgrade(), window, cx)
19194 .detach_and_log_err(cx);
19195
19196 first_item_id
19197 })
19198 .unwrap();
19199 cx.executor().run_until_parked();
19200 workspace
19201 .update(cx, |workspace, _, cx| {
19202 let active_item = workspace
19203 .active_item(cx)
19204 .expect("should have an active item after navigating back");
19205 assert_eq!(
19206 active_item.item_id(),
19207 multibuffer_item_id,
19208 "Should navigate back to the multi buffer"
19209 );
19210 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19211 })
19212 .unwrap();
19213
19214 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19215 editor.change_selections(
19216 SelectionEffects::scroll(Autoscroll::Next),
19217 window,
19218 cx,
19219 |s| s.select_ranges(Some(39..40)),
19220 );
19221 editor.open_excerpts(&OpenExcerpts, window, cx);
19222 });
19223 cx.executor().run_until_parked();
19224 let second_item_id = workspace
19225 .update(cx, |workspace, window, cx| {
19226 let active_item = workspace
19227 .active_item(cx)
19228 .expect("should have an active item after navigating into the 2nd buffer");
19229 let second_item_id = active_item.item_id();
19230 assert_ne!(
19231 second_item_id, multibuffer_item_id,
19232 "Should navigate away from the multibuffer"
19233 );
19234 assert_ne!(
19235 second_item_id, first_item_id,
19236 "Should navigate into the 2nd buffer and activate it"
19237 );
19238 assert_eq!(
19239 active_item.buffer_kind(cx),
19240 ItemBufferKind::Singleton,
19241 "New active item should be a singleton buffer"
19242 );
19243 assert_eq!(
19244 active_item
19245 .act_as::<Editor>(cx)
19246 .expect("should have navigated into an editor")
19247 .read(cx)
19248 .text(cx),
19249 sample_text_2
19250 );
19251
19252 workspace
19253 .go_back(workspace.active_pane().downgrade(), window, cx)
19254 .detach_and_log_err(cx);
19255
19256 second_item_id
19257 })
19258 .unwrap();
19259 cx.executor().run_until_parked();
19260 workspace
19261 .update(cx, |workspace, _, cx| {
19262 let active_item = workspace
19263 .active_item(cx)
19264 .expect("should have an active item after navigating back from the 2nd buffer");
19265 assert_eq!(
19266 active_item.item_id(),
19267 multibuffer_item_id,
19268 "Should navigate back from the 2nd buffer to the multi buffer"
19269 );
19270 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19271 })
19272 .unwrap();
19273
19274 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19275 editor.change_selections(
19276 SelectionEffects::scroll(Autoscroll::Next),
19277 window,
19278 cx,
19279 |s| s.select_ranges(Some(70..70)),
19280 );
19281 editor.open_excerpts(&OpenExcerpts, window, cx);
19282 });
19283 cx.executor().run_until_parked();
19284 workspace
19285 .update(cx, |workspace, window, cx| {
19286 let active_item = workspace
19287 .active_item(cx)
19288 .expect("should have an active item after navigating into the 3rd buffer");
19289 let third_item_id = active_item.item_id();
19290 assert_ne!(
19291 third_item_id, multibuffer_item_id,
19292 "Should navigate into the 3rd buffer and activate it"
19293 );
19294 assert_ne!(third_item_id, first_item_id);
19295 assert_ne!(third_item_id, second_item_id);
19296 assert_eq!(
19297 active_item.buffer_kind(cx),
19298 ItemBufferKind::Singleton,
19299 "New active item should be a singleton buffer"
19300 );
19301 assert_eq!(
19302 active_item
19303 .act_as::<Editor>(cx)
19304 .expect("should have navigated into an editor")
19305 .read(cx)
19306 .text(cx),
19307 sample_text_3
19308 );
19309
19310 workspace
19311 .go_back(workspace.active_pane().downgrade(), window, cx)
19312 .detach_and_log_err(cx);
19313 })
19314 .unwrap();
19315 cx.executor().run_until_parked();
19316 workspace
19317 .update(cx, |workspace, _, cx| {
19318 let active_item = workspace
19319 .active_item(cx)
19320 .expect("should have an active item after navigating back from the 3rd buffer");
19321 assert_eq!(
19322 active_item.item_id(),
19323 multibuffer_item_id,
19324 "Should navigate back from the 3rd buffer to the multi buffer"
19325 );
19326 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19327 })
19328 .unwrap();
19329}
19330
19331#[gpui::test]
19332async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19333 init_test(cx, |_| {});
19334
19335 let mut cx = EditorTestContext::new(cx).await;
19336
19337 let diff_base = r#"
19338 use some::mod;
19339
19340 const A: u32 = 42;
19341
19342 fn main() {
19343 println!("hello");
19344
19345 println!("world");
19346 }
19347 "#
19348 .unindent();
19349
19350 cx.set_state(
19351 &r#"
19352 use some::modified;
19353
19354 ˇ
19355 fn main() {
19356 println!("hello there");
19357
19358 println!("around the");
19359 println!("world");
19360 }
19361 "#
19362 .unindent(),
19363 );
19364
19365 cx.set_head_text(&diff_base);
19366 executor.run_until_parked();
19367
19368 cx.update_editor(|editor, window, cx| {
19369 editor.go_to_next_hunk(&GoToHunk, window, cx);
19370 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19371 });
19372 executor.run_until_parked();
19373 cx.assert_state_with_diff(
19374 r#"
19375 use some::modified;
19376
19377
19378 fn main() {
19379 - println!("hello");
19380 + ˇ println!("hello there");
19381
19382 println!("around the");
19383 println!("world");
19384 }
19385 "#
19386 .unindent(),
19387 );
19388
19389 cx.update_editor(|editor, window, cx| {
19390 for _ in 0..2 {
19391 editor.go_to_next_hunk(&GoToHunk, window, cx);
19392 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19393 }
19394 });
19395 executor.run_until_parked();
19396 cx.assert_state_with_diff(
19397 r#"
19398 - use some::mod;
19399 + ˇuse some::modified;
19400
19401
19402 fn main() {
19403 - println!("hello");
19404 + println!("hello there");
19405
19406 + println!("around the");
19407 println!("world");
19408 }
19409 "#
19410 .unindent(),
19411 );
19412
19413 cx.update_editor(|editor, window, cx| {
19414 editor.go_to_next_hunk(&GoToHunk, window, cx);
19415 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19416 });
19417 executor.run_until_parked();
19418 cx.assert_state_with_diff(
19419 r#"
19420 - use some::mod;
19421 + use some::modified;
19422
19423 - const A: u32 = 42;
19424 ˇ
19425 fn main() {
19426 - println!("hello");
19427 + println!("hello there");
19428
19429 + println!("around the");
19430 println!("world");
19431 }
19432 "#
19433 .unindent(),
19434 );
19435
19436 cx.update_editor(|editor, window, cx| {
19437 editor.cancel(&Cancel, window, cx);
19438 });
19439
19440 cx.assert_state_with_diff(
19441 r#"
19442 use some::modified;
19443
19444 ˇ
19445 fn main() {
19446 println!("hello there");
19447
19448 println!("around the");
19449 println!("world");
19450 }
19451 "#
19452 .unindent(),
19453 );
19454}
19455
19456#[gpui::test]
19457async fn test_diff_base_change_with_expanded_diff_hunks(
19458 executor: BackgroundExecutor,
19459 cx: &mut TestAppContext,
19460) {
19461 init_test(cx, |_| {});
19462
19463 let mut cx = EditorTestContext::new(cx).await;
19464
19465 let diff_base = r#"
19466 use some::mod1;
19467 use some::mod2;
19468
19469 const A: u32 = 42;
19470 const B: u32 = 42;
19471 const C: u32 = 42;
19472
19473 fn main() {
19474 println!("hello");
19475
19476 println!("world");
19477 }
19478 "#
19479 .unindent();
19480
19481 cx.set_state(
19482 &r#"
19483 use some::mod2;
19484
19485 const A: u32 = 42;
19486 const C: u32 = 42;
19487
19488 fn main(ˇ) {
19489 //println!("hello");
19490
19491 println!("world");
19492 //
19493 //
19494 }
19495 "#
19496 .unindent(),
19497 );
19498
19499 cx.set_head_text(&diff_base);
19500 executor.run_until_parked();
19501
19502 cx.update_editor(|editor, window, cx| {
19503 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19504 });
19505 executor.run_until_parked();
19506 cx.assert_state_with_diff(
19507 r#"
19508 - use some::mod1;
19509 use some::mod2;
19510
19511 const A: u32 = 42;
19512 - const B: u32 = 42;
19513 const C: u32 = 42;
19514
19515 fn main(ˇ) {
19516 - println!("hello");
19517 + //println!("hello");
19518
19519 println!("world");
19520 + //
19521 + //
19522 }
19523 "#
19524 .unindent(),
19525 );
19526
19527 cx.set_head_text("new diff base!");
19528 executor.run_until_parked();
19529 cx.assert_state_with_diff(
19530 r#"
19531 - new diff base!
19532 + use some::mod2;
19533 +
19534 + const A: u32 = 42;
19535 + const C: u32 = 42;
19536 +
19537 + fn main(ˇ) {
19538 + //println!("hello");
19539 +
19540 + println!("world");
19541 + //
19542 + //
19543 + }
19544 "#
19545 .unindent(),
19546 );
19547}
19548
19549#[gpui::test]
19550async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19551 init_test(cx, |_| {});
19552
19553 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19554 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19555 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19556 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19557 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19558 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19559
19560 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19561 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19562 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19563
19564 let multi_buffer = cx.new(|cx| {
19565 let mut multibuffer = MultiBuffer::new(ReadWrite);
19566 multibuffer.push_excerpts(
19567 buffer_1.clone(),
19568 [
19569 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19570 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19571 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19572 ],
19573 cx,
19574 );
19575 multibuffer.push_excerpts(
19576 buffer_2.clone(),
19577 [
19578 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19579 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19580 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19581 ],
19582 cx,
19583 );
19584 multibuffer.push_excerpts(
19585 buffer_3.clone(),
19586 [
19587 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19588 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19589 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19590 ],
19591 cx,
19592 );
19593 multibuffer
19594 });
19595
19596 let editor =
19597 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19598 editor
19599 .update(cx, |editor, _window, cx| {
19600 for (buffer, diff_base) in [
19601 (buffer_1.clone(), file_1_old),
19602 (buffer_2.clone(), file_2_old),
19603 (buffer_3.clone(), file_3_old),
19604 ] {
19605 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19606 editor
19607 .buffer
19608 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19609 }
19610 })
19611 .unwrap();
19612
19613 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19614 cx.run_until_parked();
19615
19616 cx.assert_editor_state(
19617 &"
19618 ˇaaa
19619 ccc
19620 ddd
19621
19622 ggg
19623 hhh
19624
19625
19626 lll
19627 mmm
19628 NNN
19629
19630 qqq
19631 rrr
19632
19633 uuu
19634 111
19635 222
19636 333
19637
19638 666
19639 777
19640
19641 000
19642 !!!"
19643 .unindent(),
19644 );
19645
19646 cx.update_editor(|editor, window, cx| {
19647 editor.select_all(&SelectAll, window, cx);
19648 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19649 });
19650 cx.executor().run_until_parked();
19651
19652 cx.assert_state_with_diff(
19653 "
19654 «aaa
19655 - bbb
19656 ccc
19657 ddd
19658
19659 ggg
19660 hhh
19661
19662
19663 lll
19664 mmm
19665 - nnn
19666 + NNN
19667
19668 qqq
19669 rrr
19670
19671 uuu
19672 111
19673 222
19674 333
19675
19676 + 666
19677 777
19678
19679 000
19680 !!!ˇ»"
19681 .unindent(),
19682 );
19683}
19684
19685#[gpui::test]
19686async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19687 init_test(cx, |_| {});
19688
19689 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19690 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19691
19692 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19693 let multi_buffer = cx.new(|cx| {
19694 let mut multibuffer = MultiBuffer::new(ReadWrite);
19695 multibuffer.push_excerpts(
19696 buffer.clone(),
19697 [
19698 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19699 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19700 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19701 ],
19702 cx,
19703 );
19704 multibuffer
19705 });
19706
19707 let editor =
19708 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19709 editor
19710 .update(cx, |editor, _window, cx| {
19711 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19712 editor
19713 .buffer
19714 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19715 })
19716 .unwrap();
19717
19718 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19719 cx.run_until_parked();
19720
19721 cx.update_editor(|editor, window, cx| {
19722 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19723 });
19724 cx.executor().run_until_parked();
19725
19726 // When the start of a hunk coincides with the start of its excerpt,
19727 // the hunk is expanded. When the start of a hunk is earlier than
19728 // the start of its excerpt, the hunk is not expanded.
19729 cx.assert_state_with_diff(
19730 "
19731 ˇaaa
19732 - bbb
19733 + BBB
19734
19735 - ddd
19736 - eee
19737 + DDD
19738 + EEE
19739 fff
19740
19741 iii
19742 "
19743 .unindent(),
19744 );
19745}
19746
19747#[gpui::test]
19748async fn test_edits_around_expanded_insertion_hunks(
19749 executor: BackgroundExecutor,
19750 cx: &mut TestAppContext,
19751) {
19752 init_test(cx, |_| {});
19753
19754 let mut cx = EditorTestContext::new(cx).await;
19755
19756 let diff_base = r#"
19757 use some::mod1;
19758 use some::mod2;
19759
19760 const A: u32 = 42;
19761
19762 fn main() {
19763 println!("hello");
19764
19765 println!("world");
19766 }
19767 "#
19768 .unindent();
19769 executor.run_until_parked();
19770 cx.set_state(
19771 &r#"
19772 use some::mod1;
19773 use some::mod2;
19774
19775 const A: u32 = 42;
19776 const B: u32 = 42;
19777 const C: u32 = 42;
19778 ˇ
19779
19780 fn main() {
19781 println!("hello");
19782
19783 println!("world");
19784 }
19785 "#
19786 .unindent(),
19787 );
19788
19789 cx.set_head_text(&diff_base);
19790 executor.run_until_parked();
19791
19792 cx.update_editor(|editor, window, cx| {
19793 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19794 });
19795 executor.run_until_parked();
19796
19797 cx.assert_state_with_diff(
19798 r#"
19799 use some::mod1;
19800 use some::mod2;
19801
19802 const A: u32 = 42;
19803 + const B: u32 = 42;
19804 + const C: u32 = 42;
19805 + ˇ
19806
19807 fn main() {
19808 println!("hello");
19809
19810 println!("world");
19811 }
19812 "#
19813 .unindent(),
19814 );
19815
19816 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19817 executor.run_until_parked();
19818
19819 cx.assert_state_with_diff(
19820 r#"
19821 use some::mod1;
19822 use some::mod2;
19823
19824 const A: u32 = 42;
19825 + const B: u32 = 42;
19826 + const C: u32 = 42;
19827 + const D: u32 = 42;
19828 + ˇ
19829
19830 fn main() {
19831 println!("hello");
19832
19833 println!("world");
19834 }
19835 "#
19836 .unindent(),
19837 );
19838
19839 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19840 executor.run_until_parked();
19841
19842 cx.assert_state_with_diff(
19843 r#"
19844 use some::mod1;
19845 use some::mod2;
19846
19847 const A: u32 = 42;
19848 + const B: u32 = 42;
19849 + const C: u32 = 42;
19850 + const D: u32 = 42;
19851 + const E: u32 = 42;
19852 + ˇ
19853
19854 fn main() {
19855 println!("hello");
19856
19857 println!("world");
19858 }
19859 "#
19860 .unindent(),
19861 );
19862
19863 cx.update_editor(|editor, window, cx| {
19864 editor.delete_line(&DeleteLine, window, cx);
19865 });
19866 executor.run_until_parked();
19867
19868 cx.assert_state_with_diff(
19869 r#"
19870 use some::mod1;
19871 use some::mod2;
19872
19873 const A: u32 = 42;
19874 + const B: u32 = 42;
19875 + const C: u32 = 42;
19876 + const D: u32 = 42;
19877 + const E: u32 = 42;
19878 ˇ
19879 fn main() {
19880 println!("hello");
19881
19882 println!("world");
19883 }
19884 "#
19885 .unindent(),
19886 );
19887
19888 cx.update_editor(|editor, window, cx| {
19889 editor.move_up(&MoveUp, window, cx);
19890 editor.delete_line(&DeleteLine, window, cx);
19891 editor.move_up(&MoveUp, window, cx);
19892 editor.delete_line(&DeleteLine, window, cx);
19893 editor.move_up(&MoveUp, window, cx);
19894 editor.delete_line(&DeleteLine, window, cx);
19895 });
19896 executor.run_until_parked();
19897 cx.assert_state_with_diff(
19898 r#"
19899 use some::mod1;
19900 use some::mod2;
19901
19902 const A: u32 = 42;
19903 + const B: u32 = 42;
19904 ˇ
19905 fn main() {
19906 println!("hello");
19907
19908 println!("world");
19909 }
19910 "#
19911 .unindent(),
19912 );
19913
19914 cx.update_editor(|editor, window, cx| {
19915 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19916 editor.delete_line(&DeleteLine, window, cx);
19917 });
19918 executor.run_until_parked();
19919 cx.assert_state_with_diff(
19920 r#"
19921 ˇ
19922 fn main() {
19923 println!("hello");
19924
19925 println!("world");
19926 }
19927 "#
19928 .unindent(),
19929 );
19930}
19931
19932#[gpui::test]
19933async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19934 init_test(cx, |_| {});
19935
19936 let mut cx = EditorTestContext::new(cx).await;
19937 cx.set_head_text(indoc! { "
19938 one
19939 two
19940 three
19941 four
19942 five
19943 "
19944 });
19945 cx.set_state(indoc! { "
19946 one
19947 ˇthree
19948 five
19949 "});
19950 cx.run_until_parked();
19951 cx.update_editor(|editor, window, cx| {
19952 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19953 });
19954 cx.assert_state_with_diff(
19955 indoc! { "
19956 one
19957 - two
19958 ˇthree
19959 - four
19960 five
19961 "}
19962 .to_string(),
19963 );
19964 cx.update_editor(|editor, window, cx| {
19965 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19966 });
19967
19968 cx.assert_state_with_diff(
19969 indoc! { "
19970 one
19971 ˇthree
19972 five
19973 "}
19974 .to_string(),
19975 );
19976
19977 cx.set_state(indoc! { "
19978 one
19979 ˇTWO
19980 three
19981 four
19982 five
19983 "});
19984 cx.run_until_parked();
19985 cx.update_editor(|editor, window, cx| {
19986 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19987 });
19988
19989 cx.assert_state_with_diff(
19990 indoc! { "
19991 one
19992 - two
19993 + ˇTWO
19994 three
19995 four
19996 five
19997 "}
19998 .to_string(),
19999 );
20000 cx.update_editor(|editor, window, cx| {
20001 editor.move_up(&Default::default(), window, cx);
20002 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20003 });
20004 cx.assert_state_with_diff(
20005 indoc! { "
20006 one
20007 ˇTWO
20008 three
20009 four
20010 five
20011 "}
20012 .to_string(),
20013 );
20014}
20015
20016#[gpui::test]
20017async fn test_edits_around_expanded_deletion_hunks(
20018 executor: BackgroundExecutor,
20019 cx: &mut TestAppContext,
20020) {
20021 init_test(cx, |_| {});
20022
20023 let mut cx = EditorTestContext::new(cx).await;
20024
20025 let diff_base = r#"
20026 use some::mod1;
20027 use some::mod2;
20028
20029 const A: u32 = 42;
20030 const B: u32 = 42;
20031 const C: u32 = 42;
20032
20033
20034 fn main() {
20035 println!("hello");
20036
20037 println!("world");
20038 }
20039 "#
20040 .unindent();
20041 executor.run_until_parked();
20042 cx.set_state(
20043 &r#"
20044 use some::mod1;
20045 use some::mod2;
20046
20047 ˇconst B: u32 = 42;
20048 const C: u32 = 42;
20049
20050
20051 fn main() {
20052 println!("hello");
20053
20054 println!("world");
20055 }
20056 "#
20057 .unindent(),
20058 );
20059
20060 cx.set_head_text(&diff_base);
20061 executor.run_until_parked();
20062
20063 cx.update_editor(|editor, window, cx| {
20064 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20065 });
20066 executor.run_until_parked();
20067
20068 cx.assert_state_with_diff(
20069 r#"
20070 use some::mod1;
20071 use some::mod2;
20072
20073 - const A: u32 = 42;
20074 ˇconst B: u32 = 42;
20075 const C: u32 = 42;
20076
20077
20078 fn main() {
20079 println!("hello");
20080
20081 println!("world");
20082 }
20083 "#
20084 .unindent(),
20085 );
20086
20087 cx.update_editor(|editor, window, cx| {
20088 editor.delete_line(&DeleteLine, window, cx);
20089 });
20090 executor.run_until_parked();
20091 cx.assert_state_with_diff(
20092 r#"
20093 use some::mod1;
20094 use some::mod2;
20095
20096 - const A: u32 = 42;
20097 - const B: u32 = 42;
20098 ˇconst C: u32 = 42;
20099
20100
20101 fn main() {
20102 println!("hello");
20103
20104 println!("world");
20105 }
20106 "#
20107 .unindent(),
20108 );
20109
20110 cx.update_editor(|editor, window, cx| {
20111 editor.delete_line(&DeleteLine, window, cx);
20112 });
20113 executor.run_until_parked();
20114 cx.assert_state_with_diff(
20115 r#"
20116 use some::mod1;
20117 use some::mod2;
20118
20119 - const A: u32 = 42;
20120 - const B: u32 = 42;
20121 - const C: u32 = 42;
20122 ˇ
20123
20124 fn main() {
20125 println!("hello");
20126
20127 println!("world");
20128 }
20129 "#
20130 .unindent(),
20131 );
20132
20133 cx.update_editor(|editor, window, cx| {
20134 editor.handle_input("replacement", window, cx);
20135 });
20136 executor.run_until_parked();
20137 cx.assert_state_with_diff(
20138 r#"
20139 use some::mod1;
20140 use some::mod2;
20141
20142 - const A: u32 = 42;
20143 - const B: u32 = 42;
20144 - const C: u32 = 42;
20145 -
20146 + replacementˇ
20147
20148 fn main() {
20149 println!("hello");
20150
20151 println!("world");
20152 }
20153 "#
20154 .unindent(),
20155 );
20156}
20157
20158#[gpui::test]
20159async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20160 init_test(cx, |_| {});
20161
20162 let mut cx = EditorTestContext::new(cx).await;
20163
20164 let base_text = r#"
20165 one
20166 two
20167 three
20168 four
20169 five
20170 "#
20171 .unindent();
20172 executor.run_until_parked();
20173 cx.set_state(
20174 &r#"
20175 one
20176 two
20177 fˇour
20178 five
20179 "#
20180 .unindent(),
20181 );
20182
20183 cx.set_head_text(&base_text);
20184 executor.run_until_parked();
20185
20186 cx.update_editor(|editor, window, cx| {
20187 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20188 });
20189 executor.run_until_parked();
20190
20191 cx.assert_state_with_diff(
20192 r#"
20193 one
20194 two
20195 - three
20196 fˇour
20197 five
20198 "#
20199 .unindent(),
20200 );
20201
20202 cx.update_editor(|editor, window, cx| {
20203 editor.backspace(&Backspace, window, cx);
20204 editor.backspace(&Backspace, window, cx);
20205 });
20206 executor.run_until_parked();
20207 cx.assert_state_with_diff(
20208 r#"
20209 one
20210 two
20211 - threeˇ
20212 - four
20213 + our
20214 five
20215 "#
20216 .unindent(),
20217 );
20218}
20219
20220#[gpui::test]
20221async fn test_edit_after_expanded_modification_hunk(
20222 executor: BackgroundExecutor,
20223 cx: &mut TestAppContext,
20224) {
20225 init_test(cx, |_| {});
20226
20227 let mut cx = EditorTestContext::new(cx).await;
20228
20229 let diff_base = r#"
20230 use some::mod1;
20231 use some::mod2;
20232
20233 const A: u32 = 42;
20234 const B: u32 = 42;
20235 const C: u32 = 42;
20236 const D: u32 = 42;
20237
20238
20239 fn main() {
20240 println!("hello");
20241
20242 println!("world");
20243 }"#
20244 .unindent();
20245
20246 cx.set_state(
20247 &r#"
20248 use some::mod1;
20249 use some::mod2;
20250
20251 const A: u32 = 42;
20252 const B: u32 = 42;
20253 const C: u32 = 43ˇ
20254 const D: u32 = 42;
20255
20256
20257 fn main() {
20258 println!("hello");
20259
20260 println!("world");
20261 }"#
20262 .unindent(),
20263 );
20264
20265 cx.set_head_text(&diff_base);
20266 executor.run_until_parked();
20267 cx.update_editor(|editor, window, cx| {
20268 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20269 });
20270 executor.run_until_parked();
20271
20272 cx.assert_state_with_diff(
20273 r#"
20274 use some::mod1;
20275 use some::mod2;
20276
20277 const A: u32 = 42;
20278 const B: u32 = 42;
20279 - const C: u32 = 42;
20280 + const C: u32 = 43ˇ
20281 const D: u32 = 42;
20282
20283
20284 fn main() {
20285 println!("hello");
20286
20287 println!("world");
20288 }"#
20289 .unindent(),
20290 );
20291
20292 cx.update_editor(|editor, window, cx| {
20293 editor.handle_input("\nnew_line\n", window, cx);
20294 });
20295 executor.run_until_parked();
20296
20297 cx.assert_state_with_diff(
20298 r#"
20299 use some::mod1;
20300 use some::mod2;
20301
20302 const A: u32 = 42;
20303 const B: u32 = 42;
20304 - const C: u32 = 42;
20305 + const C: u32 = 43
20306 + new_line
20307 + ˇ
20308 const D: u32 = 42;
20309
20310
20311 fn main() {
20312 println!("hello");
20313
20314 println!("world");
20315 }"#
20316 .unindent(),
20317 );
20318}
20319
20320#[gpui::test]
20321async fn test_stage_and_unstage_added_file_hunk(
20322 executor: BackgroundExecutor,
20323 cx: &mut TestAppContext,
20324) {
20325 init_test(cx, |_| {});
20326
20327 let mut cx = EditorTestContext::new(cx).await;
20328 cx.update_editor(|editor, _, cx| {
20329 editor.set_expand_all_diff_hunks(cx);
20330 });
20331
20332 let working_copy = r#"
20333 ˇfn main() {
20334 println!("hello, world!");
20335 }
20336 "#
20337 .unindent();
20338
20339 cx.set_state(&working_copy);
20340 executor.run_until_parked();
20341
20342 cx.assert_state_with_diff(
20343 r#"
20344 + ˇfn main() {
20345 + println!("hello, world!");
20346 + }
20347 "#
20348 .unindent(),
20349 );
20350 cx.assert_index_text(None);
20351
20352 cx.update_editor(|editor, window, cx| {
20353 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20354 });
20355 executor.run_until_parked();
20356 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20357 cx.assert_state_with_diff(
20358 r#"
20359 + ˇfn main() {
20360 + println!("hello, world!");
20361 + }
20362 "#
20363 .unindent(),
20364 );
20365
20366 cx.update_editor(|editor, window, cx| {
20367 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20368 });
20369 executor.run_until_parked();
20370 cx.assert_index_text(None);
20371}
20372
20373async fn setup_indent_guides_editor(
20374 text: &str,
20375 cx: &mut TestAppContext,
20376) -> (BufferId, EditorTestContext) {
20377 init_test(cx, |_| {});
20378
20379 let mut cx = EditorTestContext::new(cx).await;
20380
20381 let buffer_id = cx.update_editor(|editor, window, cx| {
20382 editor.set_text(text, window, cx);
20383 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20384
20385 buffer_ids[0]
20386 });
20387
20388 (buffer_id, cx)
20389}
20390
20391fn assert_indent_guides(
20392 range: Range<u32>,
20393 expected: Vec<IndentGuide>,
20394 active_indices: Option<Vec<usize>>,
20395 cx: &mut EditorTestContext,
20396) {
20397 let indent_guides = cx.update_editor(|editor, window, cx| {
20398 let snapshot = editor.snapshot(window, cx).display_snapshot;
20399 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20400 editor,
20401 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20402 true,
20403 &snapshot,
20404 cx,
20405 );
20406
20407 indent_guides.sort_by(|a, b| {
20408 a.depth.cmp(&b.depth).then(
20409 a.start_row
20410 .cmp(&b.start_row)
20411 .then(a.end_row.cmp(&b.end_row)),
20412 )
20413 });
20414 indent_guides
20415 });
20416
20417 if let Some(expected) = active_indices {
20418 let active_indices = cx.update_editor(|editor, window, cx| {
20419 let snapshot = editor.snapshot(window, cx).display_snapshot;
20420 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20421 });
20422
20423 assert_eq!(
20424 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20425 expected,
20426 "Active indent guide indices do not match"
20427 );
20428 }
20429
20430 assert_eq!(indent_guides, expected, "Indent guides do not match");
20431}
20432
20433fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20434 IndentGuide {
20435 buffer_id,
20436 start_row: MultiBufferRow(start_row),
20437 end_row: MultiBufferRow(end_row),
20438 depth,
20439 tab_size: 4,
20440 settings: IndentGuideSettings {
20441 enabled: true,
20442 line_width: 1,
20443 active_line_width: 1,
20444 coloring: IndentGuideColoring::default(),
20445 background_coloring: IndentGuideBackgroundColoring::default(),
20446 },
20447 }
20448}
20449
20450#[gpui::test]
20451async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20452 let (buffer_id, mut cx) = setup_indent_guides_editor(
20453 &"
20454 fn main() {
20455 let a = 1;
20456 }"
20457 .unindent(),
20458 cx,
20459 )
20460 .await;
20461
20462 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20463}
20464
20465#[gpui::test]
20466async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20467 let (buffer_id, mut cx) = setup_indent_guides_editor(
20468 &"
20469 fn main() {
20470 let a = 1;
20471 let b = 2;
20472 }"
20473 .unindent(),
20474 cx,
20475 )
20476 .await;
20477
20478 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20479}
20480
20481#[gpui::test]
20482async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20483 let (buffer_id, mut cx) = setup_indent_guides_editor(
20484 &"
20485 fn main() {
20486 let a = 1;
20487 if a == 3 {
20488 let b = 2;
20489 } else {
20490 let c = 3;
20491 }
20492 }"
20493 .unindent(),
20494 cx,
20495 )
20496 .await;
20497
20498 assert_indent_guides(
20499 0..8,
20500 vec![
20501 indent_guide(buffer_id, 1, 6, 0),
20502 indent_guide(buffer_id, 3, 3, 1),
20503 indent_guide(buffer_id, 5, 5, 1),
20504 ],
20505 None,
20506 &mut cx,
20507 );
20508}
20509
20510#[gpui::test]
20511async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20512 let (buffer_id, mut cx) = setup_indent_guides_editor(
20513 &"
20514 fn main() {
20515 let a = 1;
20516 let b = 2;
20517 let c = 3;
20518 }"
20519 .unindent(),
20520 cx,
20521 )
20522 .await;
20523
20524 assert_indent_guides(
20525 0..5,
20526 vec![
20527 indent_guide(buffer_id, 1, 3, 0),
20528 indent_guide(buffer_id, 2, 2, 1),
20529 ],
20530 None,
20531 &mut cx,
20532 );
20533}
20534
20535#[gpui::test]
20536async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20537 let (buffer_id, mut cx) = setup_indent_guides_editor(
20538 &"
20539 fn main() {
20540 let a = 1;
20541
20542 let c = 3;
20543 }"
20544 .unindent(),
20545 cx,
20546 )
20547 .await;
20548
20549 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20550}
20551
20552#[gpui::test]
20553async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20554 let (buffer_id, mut cx) = setup_indent_guides_editor(
20555 &"
20556 fn main() {
20557 let a = 1;
20558
20559 let c = 3;
20560
20561 if a == 3 {
20562 let b = 2;
20563 } else {
20564 let c = 3;
20565 }
20566 }"
20567 .unindent(),
20568 cx,
20569 )
20570 .await;
20571
20572 assert_indent_guides(
20573 0..11,
20574 vec![
20575 indent_guide(buffer_id, 1, 9, 0),
20576 indent_guide(buffer_id, 6, 6, 1),
20577 indent_guide(buffer_id, 8, 8, 1),
20578 ],
20579 None,
20580 &mut cx,
20581 );
20582}
20583
20584#[gpui::test]
20585async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20586 let (buffer_id, mut cx) = setup_indent_guides_editor(
20587 &"
20588 fn main() {
20589 let a = 1;
20590
20591 let c = 3;
20592
20593 if a == 3 {
20594 let b = 2;
20595 } else {
20596 let c = 3;
20597 }
20598 }"
20599 .unindent(),
20600 cx,
20601 )
20602 .await;
20603
20604 assert_indent_guides(
20605 1..11,
20606 vec![
20607 indent_guide(buffer_id, 1, 9, 0),
20608 indent_guide(buffer_id, 6, 6, 1),
20609 indent_guide(buffer_id, 8, 8, 1),
20610 ],
20611 None,
20612 &mut cx,
20613 );
20614}
20615
20616#[gpui::test]
20617async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20618 let (buffer_id, mut cx) = setup_indent_guides_editor(
20619 &"
20620 fn main() {
20621 let a = 1;
20622
20623 let c = 3;
20624
20625 if a == 3 {
20626 let b = 2;
20627 } else {
20628 let c = 3;
20629 }
20630 }"
20631 .unindent(),
20632 cx,
20633 )
20634 .await;
20635
20636 assert_indent_guides(
20637 1..10,
20638 vec![
20639 indent_guide(buffer_id, 1, 9, 0),
20640 indent_guide(buffer_id, 6, 6, 1),
20641 indent_guide(buffer_id, 8, 8, 1),
20642 ],
20643 None,
20644 &mut cx,
20645 );
20646}
20647
20648#[gpui::test]
20649async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20650 let (buffer_id, mut cx) = setup_indent_guides_editor(
20651 &"
20652 fn main() {
20653 if a {
20654 b(
20655 c,
20656 d,
20657 )
20658 } else {
20659 e(
20660 f
20661 )
20662 }
20663 }"
20664 .unindent(),
20665 cx,
20666 )
20667 .await;
20668
20669 assert_indent_guides(
20670 0..11,
20671 vec![
20672 indent_guide(buffer_id, 1, 10, 0),
20673 indent_guide(buffer_id, 2, 5, 1),
20674 indent_guide(buffer_id, 7, 9, 1),
20675 indent_guide(buffer_id, 3, 4, 2),
20676 indent_guide(buffer_id, 8, 8, 2),
20677 ],
20678 None,
20679 &mut cx,
20680 );
20681
20682 cx.update_editor(|editor, window, cx| {
20683 editor.fold_at(MultiBufferRow(2), window, cx);
20684 assert_eq!(
20685 editor.display_text(cx),
20686 "
20687 fn main() {
20688 if a {
20689 b(⋯
20690 )
20691 } else {
20692 e(
20693 f
20694 )
20695 }
20696 }"
20697 .unindent()
20698 );
20699 });
20700
20701 assert_indent_guides(
20702 0..11,
20703 vec![
20704 indent_guide(buffer_id, 1, 10, 0),
20705 indent_guide(buffer_id, 2, 5, 1),
20706 indent_guide(buffer_id, 7, 9, 1),
20707 indent_guide(buffer_id, 8, 8, 2),
20708 ],
20709 None,
20710 &mut cx,
20711 );
20712}
20713
20714#[gpui::test]
20715async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20716 let (buffer_id, mut cx) = setup_indent_guides_editor(
20717 &"
20718 block1
20719 block2
20720 block3
20721 block4
20722 block2
20723 block1
20724 block1"
20725 .unindent(),
20726 cx,
20727 )
20728 .await;
20729
20730 assert_indent_guides(
20731 1..10,
20732 vec![
20733 indent_guide(buffer_id, 1, 4, 0),
20734 indent_guide(buffer_id, 2, 3, 1),
20735 indent_guide(buffer_id, 3, 3, 2),
20736 ],
20737 None,
20738 &mut cx,
20739 );
20740}
20741
20742#[gpui::test]
20743async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20744 let (buffer_id, mut cx) = setup_indent_guides_editor(
20745 &"
20746 block1
20747 block2
20748 block3
20749
20750 block1
20751 block1"
20752 .unindent(),
20753 cx,
20754 )
20755 .await;
20756
20757 assert_indent_guides(
20758 0..6,
20759 vec![
20760 indent_guide(buffer_id, 1, 2, 0),
20761 indent_guide(buffer_id, 2, 2, 1),
20762 ],
20763 None,
20764 &mut cx,
20765 );
20766}
20767
20768#[gpui::test]
20769async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20770 let (buffer_id, mut cx) = setup_indent_guides_editor(
20771 &"
20772 function component() {
20773 \treturn (
20774 \t\t\t
20775 \t\t<div>
20776 \t\t\t<abc></abc>
20777 \t\t</div>
20778 \t)
20779 }"
20780 .unindent(),
20781 cx,
20782 )
20783 .await;
20784
20785 assert_indent_guides(
20786 0..8,
20787 vec![
20788 indent_guide(buffer_id, 1, 6, 0),
20789 indent_guide(buffer_id, 2, 5, 1),
20790 indent_guide(buffer_id, 4, 4, 2),
20791 ],
20792 None,
20793 &mut cx,
20794 );
20795}
20796
20797#[gpui::test]
20798async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20799 let (buffer_id, mut cx) = setup_indent_guides_editor(
20800 &"
20801 function component() {
20802 \treturn (
20803 \t
20804 \t\t<div>
20805 \t\t\t<abc></abc>
20806 \t\t</div>
20807 \t)
20808 }"
20809 .unindent(),
20810 cx,
20811 )
20812 .await;
20813
20814 assert_indent_guides(
20815 0..8,
20816 vec![
20817 indent_guide(buffer_id, 1, 6, 0),
20818 indent_guide(buffer_id, 2, 5, 1),
20819 indent_guide(buffer_id, 4, 4, 2),
20820 ],
20821 None,
20822 &mut cx,
20823 );
20824}
20825
20826#[gpui::test]
20827async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20828 let (buffer_id, mut cx) = setup_indent_guides_editor(
20829 &"
20830 block1
20831
20832
20833
20834 block2
20835 "
20836 .unindent(),
20837 cx,
20838 )
20839 .await;
20840
20841 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20842}
20843
20844#[gpui::test]
20845async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20846 let (buffer_id, mut cx) = setup_indent_guides_editor(
20847 &"
20848 def a:
20849 \tb = 3
20850 \tif True:
20851 \t\tc = 4
20852 \t\td = 5
20853 \tprint(b)
20854 "
20855 .unindent(),
20856 cx,
20857 )
20858 .await;
20859
20860 assert_indent_guides(
20861 0..6,
20862 vec![
20863 indent_guide(buffer_id, 1, 5, 0),
20864 indent_guide(buffer_id, 3, 4, 1),
20865 ],
20866 None,
20867 &mut cx,
20868 );
20869}
20870
20871#[gpui::test]
20872async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20873 let (buffer_id, mut cx) = setup_indent_guides_editor(
20874 &"
20875 fn main() {
20876 let a = 1;
20877 }"
20878 .unindent(),
20879 cx,
20880 )
20881 .await;
20882
20883 cx.update_editor(|editor, window, cx| {
20884 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20885 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20886 });
20887 });
20888
20889 assert_indent_guides(
20890 0..3,
20891 vec![indent_guide(buffer_id, 1, 1, 0)],
20892 Some(vec![0]),
20893 &mut cx,
20894 );
20895}
20896
20897#[gpui::test]
20898async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20899 let (buffer_id, mut cx) = setup_indent_guides_editor(
20900 &"
20901 fn main() {
20902 if 1 == 2 {
20903 let a = 1;
20904 }
20905 }"
20906 .unindent(),
20907 cx,
20908 )
20909 .await;
20910
20911 cx.update_editor(|editor, window, cx| {
20912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20913 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20914 });
20915 });
20916
20917 assert_indent_guides(
20918 0..4,
20919 vec![
20920 indent_guide(buffer_id, 1, 3, 0),
20921 indent_guide(buffer_id, 2, 2, 1),
20922 ],
20923 Some(vec![1]),
20924 &mut cx,
20925 );
20926
20927 cx.update_editor(|editor, window, cx| {
20928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20929 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20930 });
20931 });
20932
20933 assert_indent_guides(
20934 0..4,
20935 vec![
20936 indent_guide(buffer_id, 1, 3, 0),
20937 indent_guide(buffer_id, 2, 2, 1),
20938 ],
20939 Some(vec![1]),
20940 &mut cx,
20941 );
20942
20943 cx.update_editor(|editor, window, cx| {
20944 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20945 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20946 });
20947 });
20948
20949 assert_indent_guides(
20950 0..4,
20951 vec![
20952 indent_guide(buffer_id, 1, 3, 0),
20953 indent_guide(buffer_id, 2, 2, 1),
20954 ],
20955 Some(vec![0]),
20956 &mut cx,
20957 );
20958}
20959
20960#[gpui::test]
20961async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20962 let (buffer_id, mut cx) = setup_indent_guides_editor(
20963 &"
20964 fn main() {
20965 let a = 1;
20966
20967 let b = 2;
20968 }"
20969 .unindent(),
20970 cx,
20971 )
20972 .await;
20973
20974 cx.update_editor(|editor, window, cx| {
20975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20976 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20977 });
20978 });
20979
20980 assert_indent_guides(
20981 0..5,
20982 vec![indent_guide(buffer_id, 1, 3, 0)],
20983 Some(vec![0]),
20984 &mut cx,
20985 );
20986}
20987
20988#[gpui::test]
20989async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20990 let (buffer_id, mut cx) = setup_indent_guides_editor(
20991 &"
20992 def m:
20993 a = 1
20994 pass"
20995 .unindent(),
20996 cx,
20997 )
20998 .await;
20999
21000 cx.update_editor(|editor, window, cx| {
21001 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21002 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21003 });
21004 });
21005
21006 assert_indent_guides(
21007 0..3,
21008 vec![indent_guide(buffer_id, 1, 2, 0)],
21009 Some(vec![0]),
21010 &mut cx,
21011 );
21012}
21013
21014#[gpui::test]
21015async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21016 init_test(cx, |_| {});
21017 let mut cx = EditorTestContext::new(cx).await;
21018 let text = indoc! {
21019 "
21020 impl A {
21021 fn b() {
21022 0;
21023 3;
21024 5;
21025 6;
21026 7;
21027 }
21028 }
21029 "
21030 };
21031 let base_text = indoc! {
21032 "
21033 impl A {
21034 fn b() {
21035 0;
21036 1;
21037 2;
21038 3;
21039 4;
21040 }
21041 fn c() {
21042 5;
21043 6;
21044 7;
21045 }
21046 }
21047 "
21048 };
21049
21050 cx.update_editor(|editor, window, cx| {
21051 editor.set_text(text, window, cx);
21052
21053 editor.buffer().update(cx, |multibuffer, cx| {
21054 let buffer = multibuffer.as_singleton().unwrap();
21055 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21056
21057 multibuffer.set_all_diff_hunks_expanded(cx);
21058 multibuffer.add_diff(diff, cx);
21059
21060 buffer.read(cx).remote_id()
21061 })
21062 });
21063 cx.run_until_parked();
21064
21065 cx.assert_state_with_diff(
21066 indoc! { "
21067 impl A {
21068 fn b() {
21069 0;
21070 - 1;
21071 - 2;
21072 3;
21073 - 4;
21074 - }
21075 - fn c() {
21076 5;
21077 6;
21078 7;
21079 }
21080 }
21081 ˇ"
21082 }
21083 .to_string(),
21084 );
21085
21086 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21087 editor
21088 .snapshot(window, cx)
21089 .buffer_snapshot()
21090 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21091 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21092 .collect::<Vec<_>>()
21093 });
21094 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21095 assert_eq!(
21096 actual_guides,
21097 vec![
21098 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21099 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21100 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21101 ]
21102 );
21103}
21104
21105#[gpui::test]
21106async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21107 init_test(cx, |_| {});
21108 let mut cx = EditorTestContext::new(cx).await;
21109
21110 let diff_base = r#"
21111 a
21112 b
21113 c
21114 "#
21115 .unindent();
21116
21117 cx.set_state(
21118 &r#"
21119 ˇA
21120 b
21121 C
21122 "#
21123 .unindent(),
21124 );
21125 cx.set_head_text(&diff_base);
21126 cx.update_editor(|editor, window, cx| {
21127 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21128 });
21129 executor.run_until_parked();
21130
21131 let both_hunks_expanded = r#"
21132 - a
21133 + ˇA
21134 b
21135 - c
21136 + C
21137 "#
21138 .unindent();
21139
21140 cx.assert_state_with_diff(both_hunks_expanded.clone());
21141
21142 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21143 let snapshot = editor.snapshot(window, cx);
21144 let hunks = editor
21145 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21146 .collect::<Vec<_>>();
21147 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21148 let buffer_id = hunks[0].buffer_id;
21149 hunks
21150 .into_iter()
21151 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21152 .collect::<Vec<_>>()
21153 });
21154 assert_eq!(hunk_ranges.len(), 2);
21155
21156 cx.update_editor(|editor, _, cx| {
21157 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21158 });
21159 executor.run_until_parked();
21160
21161 let second_hunk_expanded = r#"
21162 ˇA
21163 b
21164 - c
21165 + C
21166 "#
21167 .unindent();
21168
21169 cx.assert_state_with_diff(second_hunk_expanded);
21170
21171 cx.update_editor(|editor, _, cx| {
21172 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21173 });
21174 executor.run_until_parked();
21175
21176 cx.assert_state_with_diff(both_hunks_expanded.clone());
21177
21178 cx.update_editor(|editor, _, cx| {
21179 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21180 });
21181 executor.run_until_parked();
21182
21183 let first_hunk_expanded = r#"
21184 - a
21185 + ˇA
21186 b
21187 C
21188 "#
21189 .unindent();
21190
21191 cx.assert_state_with_diff(first_hunk_expanded);
21192
21193 cx.update_editor(|editor, _, cx| {
21194 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21195 });
21196 executor.run_until_parked();
21197
21198 cx.assert_state_with_diff(both_hunks_expanded);
21199
21200 cx.set_state(
21201 &r#"
21202 ˇA
21203 b
21204 "#
21205 .unindent(),
21206 );
21207 cx.run_until_parked();
21208
21209 // TODO this cursor position seems bad
21210 cx.assert_state_with_diff(
21211 r#"
21212 - ˇa
21213 + A
21214 b
21215 "#
21216 .unindent(),
21217 );
21218
21219 cx.update_editor(|editor, window, cx| {
21220 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21221 });
21222
21223 cx.assert_state_with_diff(
21224 r#"
21225 - ˇa
21226 + A
21227 b
21228 - c
21229 "#
21230 .unindent(),
21231 );
21232
21233 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21234 let snapshot = editor.snapshot(window, cx);
21235 let hunks = editor
21236 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21237 .collect::<Vec<_>>();
21238 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21239 let buffer_id = hunks[0].buffer_id;
21240 hunks
21241 .into_iter()
21242 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21243 .collect::<Vec<_>>()
21244 });
21245 assert_eq!(hunk_ranges.len(), 2);
21246
21247 cx.update_editor(|editor, _, cx| {
21248 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21249 });
21250 executor.run_until_parked();
21251
21252 cx.assert_state_with_diff(
21253 r#"
21254 - ˇa
21255 + A
21256 b
21257 "#
21258 .unindent(),
21259 );
21260}
21261
21262#[gpui::test]
21263async fn test_toggle_deletion_hunk_at_start_of_file(
21264 executor: BackgroundExecutor,
21265 cx: &mut TestAppContext,
21266) {
21267 init_test(cx, |_| {});
21268 let mut cx = EditorTestContext::new(cx).await;
21269
21270 let diff_base = r#"
21271 a
21272 b
21273 c
21274 "#
21275 .unindent();
21276
21277 cx.set_state(
21278 &r#"
21279 ˇb
21280 c
21281 "#
21282 .unindent(),
21283 );
21284 cx.set_head_text(&diff_base);
21285 cx.update_editor(|editor, window, cx| {
21286 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21287 });
21288 executor.run_until_parked();
21289
21290 let hunk_expanded = r#"
21291 - a
21292 ˇb
21293 c
21294 "#
21295 .unindent();
21296
21297 cx.assert_state_with_diff(hunk_expanded.clone());
21298
21299 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21300 let snapshot = editor.snapshot(window, cx);
21301 let hunks = editor
21302 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21303 .collect::<Vec<_>>();
21304 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21305 let buffer_id = hunks[0].buffer_id;
21306 hunks
21307 .into_iter()
21308 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21309 .collect::<Vec<_>>()
21310 });
21311 assert_eq!(hunk_ranges.len(), 1);
21312
21313 cx.update_editor(|editor, _, cx| {
21314 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21315 });
21316 executor.run_until_parked();
21317
21318 let hunk_collapsed = r#"
21319 ˇb
21320 c
21321 "#
21322 .unindent();
21323
21324 cx.assert_state_with_diff(hunk_collapsed);
21325
21326 cx.update_editor(|editor, _, cx| {
21327 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21328 });
21329 executor.run_until_parked();
21330
21331 cx.assert_state_with_diff(hunk_expanded);
21332}
21333
21334#[gpui::test]
21335async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21336 init_test(cx, |_| {});
21337
21338 let fs = FakeFs::new(cx.executor());
21339 fs.insert_tree(
21340 path!("/test"),
21341 json!({
21342 ".git": {},
21343 "file-1": "ONE\n",
21344 "file-2": "TWO\n",
21345 "file-3": "THREE\n",
21346 }),
21347 )
21348 .await;
21349
21350 fs.set_head_for_repo(
21351 path!("/test/.git").as_ref(),
21352 &[
21353 ("file-1", "one\n".into()),
21354 ("file-2", "two\n".into()),
21355 ("file-3", "three\n".into()),
21356 ],
21357 "deadbeef",
21358 );
21359
21360 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21361 let mut buffers = vec![];
21362 for i in 1..=3 {
21363 let buffer = project
21364 .update(cx, |project, cx| {
21365 let path = format!(path!("/test/file-{}"), i);
21366 project.open_local_buffer(path, cx)
21367 })
21368 .await
21369 .unwrap();
21370 buffers.push(buffer);
21371 }
21372
21373 let multibuffer = cx.new(|cx| {
21374 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21375 multibuffer.set_all_diff_hunks_expanded(cx);
21376 for buffer in &buffers {
21377 let snapshot = buffer.read(cx).snapshot();
21378 multibuffer.set_excerpts_for_path(
21379 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21380 buffer.clone(),
21381 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21382 2,
21383 cx,
21384 );
21385 }
21386 multibuffer
21387 });
21388
21389 let editor = cx.add_window(|window, cx| {
21390 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21391 });
21392 cx.run_until_parked();
21393
21394 let snapshot = editor
21395 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21396 .unwrap();
21397 let hunks = snapshot
21398 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21399 .map(|hunk| match hunk {
21400 DisplayDiffHunk::Unfolded {
21401 display_row_range, ..
21402 } => display_row_range,
21403 DisplayDiffHunk::Folded { .. } => unreachable!(),
21404 })
21405 .collect::<Vec<_>>();
21406 assert_eq!(
21407 hunks,
21408 [
21409 DisplayRow(2)..DisplayRow(4),
21410 DisplayRow(7)..DisplayRow(9),
21411 DisplayRow(12)..DisplayRow(14),
21412 ]
21413 );
21414}
21415
21416#[gpui::test]
21417async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21418 init_test(cx, |_| {});
21419
21420 let mut cx = EditorTestContext::new(cx).await;
21421 cx.set_head_text(indoc! { "
21422 one
21423 two
21424 three
21425 four
21426 five
21427 "
21428 });
21429 cx.set_index_text(indoc! { "
21430 one
21431 two
21432 three
21433 four
21434 five
21435 "
21436 });
21437 cx.set_state(indoc! {"
21438 one
21439 TWO
21440 ˇTHREE
21441 FOUR
21442 five
21443 "});
21444 cx.run_until_parked();
21445 cx.update_editor(|editor, window, cx| {
21446 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21447 });
21448 cx.run_until_parked();
21449 cx.assert_index_text(Some(indoc! {"
21450 one
21451 TWO
21452 THREE
21453 FOUR
21454 five
21455 "}));
21456 cx.set_state(indoc! { "
21457 one
21458 TWO
21459 ˇTHREE-HUNDRED
21460 FOUR
21461 five
21462 "});
21463 cx.run_until_parked();
21464 cx.update_editor(|editor, window, cx| {
21465 let snapshot = editor.snapshot(window, cx);
21466 let hunks = editor
21467 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21468 .collect::<Vec<_>>();
21469 assert_eq!(hunks.len(), 1);
21470 assert_eq!(
21471 hunks[0].status(),
21472 DiffHunkStatus {
21473 kind: DiffHunkStatusKind::Modified,
21474 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21475 }
21476 );
21477
21478 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21479 });
21480 cx.run_until_parked();
21481 cx.assert_index_text(Some(indoc! {"
21482 one
21483 TWO
21484 THREE-HUNDRED
21485 FOUR
21486 five
21487 "}));
21488}
21489
21490#[gpui::test]
21491fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21492 init_test(cx, |_| {});
21493
21494 let editor = cx.add_window(|window, cx| {
21495 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21496 build_editor(buffer, window, cx)
21497 });
21498
21499 let render_args = Arc::new(Mutex::new(None));
21500 let snapshot = editor
21501 .update(cx, |editor, window, cx| {
21502 let snapshot = editor.buffer().read(cx).snapshot(cx);
21503 let range =
21504 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21505
21506 struct RenderArgs {
21507 row: MultiBufferRow,
21508 folded: bool,
21509 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21510 }
21511
21512 let crease = Crease::inline(
21513 range,
21514 FoldPlaceholder::test(),
21515 {
21516 let toggle_callback = render_args.clone();
21517 move |row, folded, callback, _window, _cx| {
21518 *toggle_callback.lock() = Some(RenderArgs {
21519 row,
21520 folded,
21521 callback,
21522 });
21523 div()
21524 }
21525 },
21526 |_row, _folded, _window, _cx| div(),
21527 );
21528
21529 editor.insert_creases(Some(crease), cx);
21530 let snapshot = editor.snapshot(window, cx);
21531 let _div =
21532 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21533 snapshot
21534 })
21535 .unwrap();
21536
21537 let render_args = render_args.lock().take().unwrap();
21538 assert_eq!(render_args.row, MultiBufferRow(1));
21539 assert!(!render_args.folded);
21540 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21541
21542 cx.update_window(*editor, |_, window, cx| {
21543 (render_args.callback)(true, window, cx)
21544 })
21545 .unwrap();
21546 let snapshot = editor
21547 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21548 .unwrap();
21549 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21550
21551 cx.update_window(*editor, |_, window, cx| {
21552 (render_args.callback)(false, window, cx)
21553 })
21554 .unwrap();
21555 let snapshot = editor
21556 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21557 .unwrap();
21558 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21559}
21560
21561#[gpui::test]
21562async fn test_input_text(cx: &mut TestAppContext) {
21563 init_test(cx, |_| {});
21564 let mut cx = EditorTestContext::new(cx).await;
21565
21566 cx.set_state(
21567 &r#"ˇone
21568 two
21569
21570 three
21571 fourˇ
21572 five
21573
21574 siˇx"#
21575 .unindent(),
21576 );
21577
21578 cx.dispatch_action(HandleInput(String::new()));
21579 cx.assert_editor_state(
21580 &r#"ˇone
21581 two
21582
21583 three
21584 fourˇ
21585 five
21586
21587 siˇx"#
21588 .unindent(),
21589 );
21590
21591 cx.dispatch_action(HandleInput("AAAA".to_string()));
21592 cx.assert_editor_state(
21593 &r#"AAAAˇone
21594 two
21595
21596 three
21597 fourAAAAˇ
21598 five
21599
21600 siAAAAˇx"#
21601 .unindent(),
21602 );
21603}
21604
21605#[gpui::test]
21606async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21607 init_test(cx, |_| {});
21608
21609 let mut cx = EditorTestContext::new(cx).await;
21610 cx.set_state(
21611 r#"let foo = 1;
21612let foo = 2;
21613let foo = 3;
21614let fooˇ = 4;
21615let foo = 5;
21616let foo = 6;
21617let foo = 7;
21618let foo = 8;
21619let foo = 9;
21620let foo = 10;
21621let foo = 11;
21622let foo = 12;
21623let foo = 13;
21624let foo = 14;
21625let foo = 15;"#,
21626 );
21627
21628 cx.update_editor(|e, window, cx| {
21629 assert_eq!(
21630 e.next_scroll_position,
21631 NextScrollCursorCenterTopBottom::Center,
21632 "Default next scroll direction is center",
21633 );
21634
21635 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21636 assert_eq!(
21637 e.next_scroll_position,
21638 NextScrollCursorCenterTopBottom::Top,
21639 "After center, next scroll direction should be top",
21640 );
21641
21642 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21643 assert_eq!(
21644 e.next_scroll_position,
21645 NextScrollCursorCenterTopBottom::Bottom,
21646 "After top, next scroll direction should be bottom",
21647 );
21648
21649 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21650 assert_eq!(
21651 e.next_scroll_position,
21652 NextScrollCursorCenterTopBottom::Center,
21653 "After bottom, scrolling should start over",
21654 );
21655
21656 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21657 assert_eq!(
21658 e.next_scroll_position,
21659 NextScrollCursorCenterTopBottom::Top,
21660 "Scrolling continues if retriggered fast enough"
21661 );
21662 });
21663
21664 cx.executor()
21665 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21666 cx.executor().run_until_parked();
21667 cx.update_editor(|e, _, _| {
21668 assert_eq!(
21669 e.next_scroll_position,
21670 NextScrollCursorCenterTopBottom::Center,
21671 "If scrolling is not triggered fast enough, it should reset"
21672 );
21673 });
21674}
21675
21676#[gpui::test]
21677async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21678 init_test(cx, |_| {});
21679 let mut cx = EditorLspTestContext::new_rust(
21680 lsp::ServerCapabilities {
21681 definition_provider: Some(lsp::OneOf::Left(true)),
21682 references_provider: Some(lsp::OneOf::Left(true)),
21683 ..lsp::ServerCapabilities::default()
21684 },
21685 cx,
21686 )
21687 .await;
21688
21689 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21690 let go_to_definition = cx
21691 .lsp
21692 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21693 move |params, _| async move {
21694 if empty_go_to_definition {
21695 Ok(None)
21696 } else {
21697 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21698 uri: params.text_document_position_params.text_document.uri,
21699 range: lsp::Range::new(
21700 lsp::Position::new(4, 3),
21701 lsp::Position::new(4, 6),
21702 ),
21703 })))
21704 }
21705 },
21706 );
21707 let references = cx
21708 .lsp
21709 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21710 Ok(Some(vec![lsp::Location {
21711 uri: params.text_document_position.text_document.uri,
21712 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21713 }]))
21714 });
21715 (go_to_definition, references)
21716 };
21717
21718 cx.set_state(
21719 &r#"fn one() {
21720 let mut a = ˇtwo();
21721 }
21722
21723 fn two() {}"#
21724 .unindent(),
21725 );
21726 set_up_lsp_handlers(false, &mut cx);
21727 let navigated = cx
21728 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21729 .await
21730 .expect("Failed to navigate to definition");
21731 assert_eq!(
21732 navigated,
21733 Navigated::Yes,
21734 "Should have navigated to definition from the GetDefinition response"
21735 );
21736 cx.assert_editor_state(
21737 &r#"fn one() {
21738 let mut a = two();
21739 }
21740
21741 fn «twoˇ»() {}"#
21742 .unindent(),
21743 );
21744
21745 let editors = cx.update_workspace(|workspace, _, cx| {
21746 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21747 });
21748 cx.update_editor(|_, _, test_editor_cx| {
21749 assert_eq!(
21750 editors.len(),
21751 1,
21752 "Initially, only one, test, editor should be open in the workspace"
21753 );
21754 assert_eq!(
21755 test_editor_cx.entity(),
21756 editors.last().expect("Asserted len is 1").clone()
21757 );
21758 });
21759
21760 set_up_lsp_handlers(true, &mut cx);
21761 let navigated = cx
21762 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21763 .await
21764 .expect("Failed to navigate to lookup references");
21765 assert_eq!(
21766 navigated,
21767 Navigated::Yes,
21768 "Should have navigated to references as a fallback after empty GoToDefinition response"
21769 );
21770 // We should not change the selections in the existing file,
21771 // if opening another milti buffer with the references
21772 cx.assert_editor_state(
21773 &r#"fn one() {
21774 let mut a = two();
21775 }
21776
21777 fn «twoˇ»() {}"#
21778 .unindent(),
21779 );
21780 let editors = cx.update_workspace(|workspace, _, cx| {
21781 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21782 });
21783 cx.update_editor(|_, _, test_editor_cx| {
21784 assert_eq!(
21785 editors.len(),
21786 2,
21787 "After falling back to references search, we open a new editor with the results"
21788 );
21789 let references_fallback_text = editors
21790 .into_iter()
21791 .find(|new_editor| *new_editor != test_editor_cx.entity())
21792 .expect("Should have one non-test editor now")
21793 .read(test_editor_cx)
21794 .text(test_editor_cx);
21795 assert_eq!(
21796 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21797 "Should use the range from the references response and not the GoToDefinition one"
21798 );
21799 });
21800}
21801
21802#[gpui::test]
21803async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21804 init_test(cx, |_| {});
21805 cx.update(|cx| {
21806 let mut editor_settings = EditorSettings::get_global(cx).clone();
21807 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21808 EditorSettings::override_global(editor_settings, cx);
21809 });
21810 let mut cx = EditorLspTestContext::new_rust(
21811 lsp::ServerCapabilities {
21812 definition_provider: Some(lsp::OneOf::Left(true)),
21813 references_provider: Some(lsp::OneOf::Left(true)),
21814 ..lsp::ServerCapabilities::default()
21815 },
21816 cx,
21817 )
21818 .await;
21819 let original_state = r#"fn one() {
21820 let mut a = ˇtwo();
21821 }
21822
21823 fn two() {}"#
21824 .unindent();
21825 cx.set_state(&original_state);
21826
21827 let mut go_to_definition = cx
21828 .lsp
21829 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21830 move |_, _| async move { Ok(None) },
21831 );
21832 let _references = cx
21833 .lsp
21834 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21835 panic!("Should not call for references with no go to definition fallback")
21836 });
21837
21838 let navigated = cx
21839 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21840 .await
21841 .expect("Failed to navigate to lookup references");
21842 go_to_definition
21843 .next()
21844 .await
21845 .expect("Should have called the go_to_definition handler");
21846
21847 assert_eq!(
21848 navigated,
21849 Navigated::No,
21850 "Should have navigated to references as a fallback after empty GoToDefinition response"
21851 );
21852 cx.assert_editor_state(&original_state);
21853 let editors = cx.update_workspace(|workspace, _, cx| {
21854 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21855 });
21856 cx.update_editor(|_, _, _| {
21857 assert_eq!(
21858 editors.len(),
21859 1,
21860 "After unsuccessful fallback, no other editor should have been opened"
21861 );
21862 });
21863}
21864
21865#[gpui::test]
21866async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21867 init_test(cx, |_| {});
21868 let mut cx = EditorLspTestContext::new_rust(
21869 lsp::ServerCapabilities {
21870 references_provider: Some(lsp::OneOf::Left(true)),
21871 ..lsp::ServerCapabilities::default()
21872 },
21873 cx,
21874 )
21875 .await;
21876
21877 cx.set_state(
21878 &r#"
21879 fn one() {
21880 let mut a = two();
21881 }
21882
21883 fn ˇtwo() {}"#
21884 .unindent(),
21885 );
21886 cx.lsp
21887 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21888 Ok(Some(vec![
21889 lsp::Location {
21890 uri: params.text_document_position.text_document.uri.clone(),
21891 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21892 },
21893 lsp::Location {
21894 uri: params.text_document_position.text_document.uri,
21895 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21896 },
21897 ]))
21898 });
21899 let navigated = cx
21900 .update_editor(|editor, window, cx| {
21901 editor.find_all_references(&FindAllReferences, window, cx)
21902 })
21903 .unwrap()
21904 .await
21905 .expect("Failed to navigate to references");
21906 assert_eq!(
21907 navigated,
21908 Navigated::Yes,
21909 "Should have navigated to references from the FindAllReferences response"
21910 );
21911 cx.assert_editor_state(
21912 &r#"fn one() {
21913 let mut a = two();
21914 }
21915
21916 fn ˇtwo() {}"#
21917 .unindent(),
21918 );
21919
21920 let editors = cx.update_workspace(|workspace, _, cx| {
21921 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21922 });
21923 cx.update_editor(|_, _, _| {
21924 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21925 });
21926
21927 cx.set_state(
21928 &r#"fn one() {
21929 let mut a = ˇtwo();
21930 }
21931
21932 fn two() {}"#
21933 .unindent(),
21934 );
21935 let navigated = cx
21936 .update_editor(|editor, window, cx| {
21937 editor.find_all_references(&FindAllReferences, window, cx)
21938 })
21939 .unwrap()
21940 .await
21941 .expect("Failed to navigate to references");
21942 assert_eq!(
21943 navigated,
21944 Navigated::Yes,
21945 "Should have navigated to references from the FindAllReferences response"
21946 );
21947 cx.assert_editor_state(
21948 &r#"fn one() {
21949 let mut a = ˇtwo();
21950 }
21951
21952 fn two() {}"#
21953 .unindent(),
21954 );
21955 let editors = cx.update_workspace(|workspace, _, cx| {
21956 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21957 });
21958 cx.update_editor(|_, _, _| {
21959 assert_eq!(
21960 editors.len(),
21961 2,
21962 "should have re-used the previous multibuffer"
21963 );
21964 });
21965
21966 cx.set_state(
21967 &r#"fn one() {
21968 let mut a = ˇtwo();
21969 }
21970 fn three() {}
21971 fn two() {}"#
21972 .unindent(),
21973 );
21974 cx.lsp
21975 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21976 Ok(Some(vec![
21977 lsp::Location {
21978 uri: params.text_document_position.text_document.uri.clone(),
21979 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21980 },
21981 lsp::Location {
21982 uri: params.text_document_position.text_document.uri,
21983 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21984 },
21985 ]))
21986 });
21987 let navigated = cx
21988 .update_editor(|editor, window, cx| {
21989 editor.find_all_references(&FindAllReferences, window, cx)
21990 })
21991 .unwrap()
21992 .await
21993 .expect("Failed to navigate to references");
21994 assert_eq!(
21995 navigated,
21996 Navigated::Yes,
21997 "Should have navigated to references from the FindAllReferences response"
21998 );
21999 cx.assert_editor_state(
22000 &r#"fn one() {
22001 let mut a = ˇtwo();
22002 }
22003 fn three() {}
22004 fn two() {}"#
22005 .unindent(),
22006 );
22007 let editors = cx.update_workspace(|workspace, _, cx| {
22008 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22009 });
22010 cx.update_editor(|_, _, _| {
22011 assert_eq!(
22012 editors.len(),
22013 3,
22014 "should have used a new multibuffer as offsets changed"
22015 );
22016 });
22017}
22018#[gpui::test]
22019async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22020 init_test(cx, |_| {});
22021
22022 let language = Arc::new(Language::new(
22023 LanguageConfig::default(),
22024 Some(tree_sitter_rust::LANGUAGE.into()),
22025 ));
22026
22027 let text = r#"
22028 #[cfg(test)]
22029 mod tests() {
22030 #[test]
22031 fn runnable_1() {
22032 let a = 1;
22033 }
22034
22035 #[test]
22036 fn runnable_2() {
22037 let a = 1;
22038 let b = 2;
22039 }
22040 }
22041 "#
22042 .unindent();
22043
22044 let fs = FakeFs::new(cx.executor());
22045 fs.insert_file("/file.rs", Default::default()).await;
22046
22047 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22048 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22049 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22050 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22051 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22052
22053 let editor = cx.new_window_entity(|window, cx| {
22054 Editor::new(
22055 EditorMode::full(),
22056 multi_buffer,
22057 Some(project.clone()),
22058 window,
22059 cx,
22060 )
22061 });
22062
22063 editor.update_in(cx, |editor, window, cx| {
22064 let snapshot = editor.buffer().read(cx).snapshot(cx);
22065 editor.tasks.insert(
22066 (buffer.read(cx).remote_id(), 3),
22067 RunnableTasks {
22068 templates: vec![],
22069 offset: snapshot.anchor_before(43),
22070 column: 0,
22071 extra_variables: HashMap::default(),
22072 context_range: BufferOffset(43)..BufferOffset(85),
22073 },
22074 );
22075 editor.tasks.insert(
22076 (buffer.read(cx).remote_id(), 8),
22077 RunnableTasks {
22078 templates: vec![],
22079 offset: snapshot.anchor_before(86),
22080 column: 0,
22081 extra_variables: HashMap::default(),
22082 context_range: BufferOffset(86)..BufferOffset(191),
22083 },
22084 );
22085
22086 // Test finding task when cursor is inside function body
22087 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22088 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22089 });
22090 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22091 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22092
22093 // Test finding task when cursor is on function name
22094 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22095 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22096 });
22097 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22098 assert_eq!(row, 8, "Should find task when cursor is on function name");
22099 });
22100}
22101
22102#[gpui::test]
22103async fn test_folding_buffers(cx: &mut TestAppContext) {
22104 init_test(cx, |_| {});
22105
22106 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22107 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22108 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22109
22110 let fs = FakeFs::new(cx.executor());
22111 fs.insert_tree(
22112 path!("/a"),
22113 json!({
22114 "first.rs": sample_text_1,
22115 "second.rs": sample_text_2,
22116 "third.rs": sample_text_3,
22117 }),
22118 )
22119 .await;
22120 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22121 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22122 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22123 let worktree = project.update(cx, |project, cx| {
22124 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22125 assert_eq!(worktrees.len(), 1);
22126 worktrees.pop().unwrap()
22127 });
22128 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22129
22130 let buffer_1 = project
22131 .update(cx, |project, cx| {
22132 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22133 })
22134 .await
22135 .unwrap();
22136 let buffer_2 = project
22137 .update(cx, |project, cx| {
22138 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22139 })
22140 .await
22141 .unwrap();
22142 let buffer_3 = project
22143 .update(cx, |project, cx| {
22144 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22145 })
22146 .await
22147 .unwrap();
22148
22149 let multi_buffer = cx.new(|cx| {
22150 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22151 multi_buffer.push_excerpts(
22152 buffer_1.clone(),
22153 [
22154 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22155 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22156 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22157 ],
22158 cx,
22159 );
22160 multi_buffer.push_excerpts(
22161 buffer_2.clone(),
22162 [
22163 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22164 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22165 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22166 ],
22167 cx,
22168 );
22169 multi_buffer.push_excerpts(
22170 buffer_3.clone(),
22171 [
22172 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22173 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22174 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22175 ],
22176 cx,
22177 );
22178 multi_buffer
22179 });
22180 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22181 Editor::new(
22182 EditorMode::full(),
22183 multi_buffer.clone(),
22184 Some(project.clone()),
22185 window,
22186 cx,
22187 )
22188 });
22189
22190 assert_eq!(
22191 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22192 "\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",
22193 );
22194
22195 multi_buffer_editor.update(cx, |editor, cx| {
22196 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22197 });
22198 assert_eq!(
22199 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22200 "\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",
22201 "After folding the first buffer, its text should not be displayed"
22202 );
22203
22204 multi_buffer_editor.update(cx, |editor, cx| {
22205 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22206 });
22207 assert_eq!(
22208 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22209 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22210 "After folding the second buffer, its text should not be displayed"
22211 );
22212
22213 multi_buffer_editor.update(cx, |editor, cx| {
22214 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22215 });
22216 assert_eq!(
22217 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22218 "\n\n\n\n\n",
22219 "After folding the third buffer, its text should not be displayed"
22220 );
22221
22222 // Emulate selection inside the fold logic, that should work
22223 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22224 editor
22225 .snapshot(window, cx)
22226 .next_line_boundary(Point::new(0, 4));
22227 });
22228
22229 multi_buffer_editor.update(cx, |editor, cx| {
22230 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22231 });
22232 assert_eq!(
22233 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22234 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22235 "After unfolding the second buffer, its text should be displayed"
22236 );
22237
22238 // Typing inside of buffer 1 causes that buffer to be unfolded.
22239 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22240 assert_eq!(
22241 multi_buffer
22242 .read(cx)
22243 .snapshot(cx)
22244 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22245 .collect::<String>(),
22246 "bbbb"
22247 );
22248 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22249 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22250 });
22251 editor.handle_input("B", window, cx);
22252 });
22253
22254 assert_eq!(
22255 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22256 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22257 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22258 );
22259
22260 multi_buffer_editor.update(cx, |editor, cx| {
22261 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22262 });
22263 assert_eq!(
22264 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22265 "\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",
22266 "After unfolding the all buffers, all original text should be displayed"
22267 );
22268}
22269
22270#[gpui::test]
22271async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22272 init_test(cx, |_| {});
22273
22274 let sample_text_1 = "1111\n2222\n3333".to_string();
22275 let sample_text_2 = "4444\n5555\n6666".to_string();
22276 let sample_text_3 = "7777\n8888\n9999".to_string();
22277
22278 let fs = FakeFs::new(cx.executor());
22279 fs.insert_tree(
22280 path!("/a"),
22281 json!({
22282 "first.rs": sample_text_1,
22283 "second.rs": sample_text_2,
22284 "third.rs": sample_text_3,
22285 }),
22286 )
22287 .await;
22288 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22289 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22290 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22291 let worktree = project.update(cx, |project, cx| {
22292 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22293 assert_eq!(worktrees.len(), 1);
22294 worktrees.pop().unwrap()
22295 });
22296 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22297
22298 let buffer_1 = project
22299 .update(cx, |project, cx| {
22300 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22301 })
22302 .await
22303 .unwrap();
22304 let buffer_2 = project
22305 .update(cx, |project, cx| {
22306 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22307 })
22308 .await
22309 .unwrap();
22310 let buffer_3 = project
22311 .update(cx, |project, cx| {
22312 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22313 })
22314 .await
22315 .unwrap();
22316
22317 let multi_buffer = cx.new(|cx| {
22318 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22319 multi_buffer.push_excerpts(
22320 buffer_1.clone(),
22321 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22322 cx,
22323 );
22324 multi_buffer.push_excerpts(
22325 buffer_2.clone(),
22326 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22327 cx,
22328 );
22329 multi_buffer.push_excerpts(
22330 buffer_3.clone(),
22331 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22332 cx,
22333 );
22334 multi_buffer
22335 });
22336
22337 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22338 Editor::new(
22339 EditorMode::full(),
22340 multi_buffer,
22341 Some(project.clone()),
22342 window,
22343 cx,
22344 )
22345 });
22346
22347 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22348 assert_eq!(
22349 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22350 full_text,
22351 );
22352
22353 multi_buffer_editor.update(cx, |editor, cx| {
22354 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22355 });
22356 assert_eq!(
22357 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22358 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22359 "After folding the first buffer, its text should not be displayed"
22360 );
22361
22362 multi_buffer_editor.update(cx, |editor, cx| {
22363 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22364 });
22365
22366 assert_eq!(
22367 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22368 "\n\n\n\n\n\n7777\n8888\n9999",
22369 "After folding the second buffer, its text should not be displayed"
22370 );
22371
22372 multi_buffer_editor.update(cx, |editor, cx| {
22373 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22374 });
22375 assert_eq!(
22376 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22377 "\n\n\n\n\n",
22378 "After folding the third buffer, its text should not be displayed"
22379 );
22380
22381 multi_buffer_editor.update(cx, |editor, cx| {
22382 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22383 });
22384 assert_eq!(
22385 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22386 "\n\n\n\n4444\n5555\n6666\n\n",
22387 "After unfolding the second buffer, its text should be displayed"
22388 );
22389
22390 multi_buffer_editor.update(cx, |editor, cx| {
22391 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22392 });
22393 assert_eq!(
22394 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22395 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22396 "After unfolding the first buffer, its text should be displayed"
22397 );
22398
22399 multi_buffer_editor.update(cx, |editor, cx| {
22400 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22401 });
22402 assert_eq!(
22403 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22404 full_text,
22405 "After unfolding all buffers, all original text should be displayed"
22406 );
22407}
22408
22409#[gpui::test]
22410async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22411 init_test(cx, |_| {});
22412
22413 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22414
22415 let fs = FakeFs::new(cx.executor());
22416 fs.insert_tree(
22417 path!("/a"),
22418 json!({
22419 "main.rs": sample_text,
22420 }),
22421 )
22422 .await;
22423 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22424 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22425 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22426 let worktree = project.update(cx, |project, cx| {
22427 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22428 assert_eq!(worktrees.len(), 1);
22429 worktrees.pop().unwrap()
22430 });
22431 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22432
22433 let buffer_1 = project
22434 .update(cx, |project, cx| {
22435 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22436 })
22437 .await
22438 .unwrap();
22439
22440 let multi_buffer = cx.new(|cx| {
22441 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22442 multi_buffer.push_excerpts(
22443 buffer_1.clone(),
22444 [ExcerptRange::new(
22445 Point::new(0, 0)
22446 ..Point::new(
22447 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22448 0,
22449 ),
22450 )],
22451 cx,
22452 );
22453 multi_buffer
22454 });
22455 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22456 Editor::new(
22457 EditorMode::full(),
22458 multi_buffer,
22459 Some(project.clone()),
22460 window,
22461 cx,
22462 )
22463 });
22464
22465 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22466 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22467 enum TestHighlight {}
22468 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22469 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22470 editor.highlight_text::<TestHighlight>(
22471 vec![highlight_range.clone()],
22472 HighlightStyle::color(Hsla::green()),
22473 cx,
22474 );
22475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22476 s.select_ranges(Some(highlight_range))
22477 });
22478 });
22479
22480 let full_text = format!("\n\n{sample_text}");
22481 assert_eq!(
22482 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22483 full_text,
22484 );
22485}
22486
22487#[gpui::test]
22488async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22489 init_test(cx, |_| {});
22490 cx.update(|cx| {
22491 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22492 "keymaps/default-linux.json",
22493 cx,
22494 )
22495 .unwrap();
22496 cx.bind_keys(default_key_bindings);
22497 });
22498
22499 let (editor, cx) = cx.add_window_view(|window, cx| {
22500 let multi_buffer = MultiBuffer::build_multi(
22501 [
22502 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22503 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22504 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22505 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22506 ],
22507 cx,
22508 );
22509 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22510
22511 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22512 // fold all but the second buffer, so that we test navigating between two
22513 // adjacent folded buffers, as well as folded buffers at the start and
22514 // end the multibuffer
22515 editor.fold_buffer(buffer_ids[0], cx);
22516 editor.fold_buffer(buffer_ids[2], cx);
22517 editor.fold_buffer(buffer_ids[3], cx);
22518
22519 editor
22520 });
22521 cx.simulate_resize(size(px(1000.), px(1000.)));
22522
22523 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22524 cx.assert_excerpts_with_selections(indoc! {"
22525 [EXCERPT]
22526 ˇ[FOLDED]
22527 [EXCERPT]
22528 a1
22529 b1
22530 [EXCERPT]
22531 [FOLDED]
22532 [EXCERPT]
22533 [FOLDED]
22534 "
22535 });
22536 cx.simulate_keystroke("down");
22537 cx.assert_excerpts_with_selections(indoc! {"
22538 [EXCERPT]
22539 [FOLDED]
22540 [EXCERPT]
22541 ˇa1
22542 b1
22543 [EXCERPT]
22544 [FOLDED]
22545 [EXCERPT]
22546 [FOLDED]
22547 "
22548 });
22549 cx.simulate_keystroke("down");
22550 cx.assert_excerpts_with_selections(indoc! {"
22551 [EXCERPT]
22552 [FOLDED]
22553 [EXCERPT]
22554 a1
22555 ˇb1
22556 [EXCERPT]
22557 [FOLDED]
22558 [EXCERPT]
22559 [FOLDED]
22560 "
22561 });
22562 cx.simulate_keystroke("down");
22563 cx.assert_excerpts_with_selections(indoc! {"
22564 [EXCERPT]
22565 [FOLDED]
22566 [EXCERPT]
22567 a1
22568 b1
22569 ˇ[EXCERPT]
22570 [FOLDED]
22571 [EXCERPT]
22572 [FOLDED]
22573 "
22574 });
22575 cx.simulate_keystroke("down");
22576 cx.assert_excerpts_with_selections(indoc! {"
22577 [EXCERPT]
22578 [FOLDED]
22579 [EXCERPT]
22580 a1
22581 b1
22582 [EXCERPT]
22583 ˇ[FOLDED]
22584 [EXCERPT]
22585 [FOLDED]
22586 "
22587 });
22588 for _ in 0..5 {
22589 cx.simulate_keystroke("down");
22590 cx.assert_excerpts_with_selections(indoc! {"
22591 [EXCERPT]
22592 [FOLDED]
22593 [EXCERPT]
22594 a1
22595 b1
22596 [EXCERPT]
22597 [FOLDED]
22598 [EXCERPT]
22599 ˇ[FOLDED]
22600 "
22601 });
22602 }
22603
22604 cx.simulate_keystroke("up");
22605 cx.assert_excerpts_with_selections(indoc! {"
22606 [EXCERPT]
22607 [FOLDED]
22608 [EXCERPT]
22609 a1
22610 b1
22611 [EXCERPT]
22612 ˇ[FOLDED]
22613 [EXCERPT]
22614 [FOLDED]
22615 "
22616 });
22617 cx.simulate_keystroke("up");
22618 cx.assert_excerpts_with_selections(indoc! {"
22619 [EXCERPT]
22620 [FOLDED]
22621 [EXCERPT]
22622 a1
22623 b1
22624 ˇ[EXCERPT]
22625 [FOLDED]
22626 [EXCERPT]
22627 [FOLDED]
22628 "
22629 });
22630 cx.simulate_keystroke("up");
22631 cx.assert_excerpts_with_selections(indoc! {"
22632 [EXCERPT]
22633 [FOLDED]
22634 [EXCERPT]
22635 a1
22636 ˇb1
22637 [EXCERPT]
22638 [FOLDED]
22639 [EXCERPT]
22640 [FOLDED]
22641 "
22642 });
22643 cx.simulate_keystroke("up");
22644 cx.assert_excerpts_with_selections(indoc! {"
22645 [EXCERPT]
22646 [FOLDED]
22647 [EXCERPT]
22648 ˇa1
22649 b1
22650 [EXCERPT]
22651 [FOLDED]
22652 [EXCERPT]
22653 [FOLDED]
22654 "
22655 });
22656 for _ in 0..5 {
22657 cx.simulate_keystroke("up");
22658 cx.assert_excerpts_with_selections(indoc! {"
22659 [EXCERPT]
22660 ˇ[FOLDED]
22661 [EXCERPT]
22662 a1
22663 b1
22664 [EXCERPT]
22665 [FOLDED]
22666 [EXCERPT]
22667 [FOLDED]
22668 "
22669 });
22670 }
22671}
22672
22673#[gpui::test]
22674async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22675 init_test(cx, |_| {});
22676
22677 // Simple insertion
22678 assert_highlighted_edits(
22679 "Hello, world!",
22680 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22681 true,
22682 cx,
22683 |highlighted_edits, cx| {
22684 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22685 assert_eq!(highlighted_edits.highlights.len(), 1);
22686 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22687 assert_eq!(
22688 highlighted_edits.highlights[0].1.background_color,
22689 Some(cx.theme().status().created_background)
22690 );
22691 },
22692 )
22693 .await;
22694
22695 // Replacement
22696 assert_highlighted_edits(
22697 "This is a test.",
22698 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22699 false,
22700 cx,
22701 |highlighted_edits, cx| {
22702 assert_eq!(highlighted_edits.text, "That is a test.");
22703 assert_eq!(highlighted_edits.highlights.len(), 1);
22704 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22705 assert_eq!(
22706 highlighted_edits.highlights[0].1.background_color,
22707 Some(cx.theme().status().created_background)
22708 );
22709 },
22710 )
22711 .await;
22712
22713 // Multiple edits
22714 assert_highlighted_edits(
22715 "Hello, world!",
22716 vec![
22717 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22718 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22719 ],
22720 false,
22721 cx,
22722 |highlighted_edits, cx| {
22723 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22724 assert_eq!(highlighted_edits.highlights.len(), 2);
22725 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22726 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22727 assert_eq!(
22728 highlighted_edits.highlights[0].1.background_color,
22729 Some(cx.theme().status().created_background)
22730 );
22731 assert_eq!(
22732 highlighted_edits.highlights[1].1.background_color,
22733 Some(cx.theme().status().created_background)
22734 );
22735 },
22736 )
22737 .await;
22738
22739 // Multiple lines with edits
22740 assert_highlighted_edits(
22741 "First line\nSecond line\nThird line\nFourth line",
22742 vec![
22743 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22744 (
22745 Point::new(2, 0)..Point::new(2, 10),
22746 "New third line".to_string(),
22747 ),
22748 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22749 ],
22750 false,
22751 cx,
22752 |highlighted_edits, cx| {
22753 assert_eq!(
22754 highlighted_edits.text,
22755 "Second modified\nNew third line\nFourth updated line"
22756 );
22757 assert_eq!(highlighted_edits.highlights.len(), 3);
22758 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22759 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22760 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22761 for highlight in &highlighted_edits.highlights {
22762 assert_eq!(
22763 highlight.1.background_color,
22764 Some(cx.theme().status().created_background)
22765 );
22766 }
22767 },
22768 )
22769 .await;
22770}
22771
22772#[gpui::test]
22773async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22774 init_test(cx, |_| {});
22775
22776 // Deletion
22777 assert_highlighted_edits(
22778 "Hello, world!",
22779 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22780 true,
22781 cx,
22782 |highlighted_edits, cx| {
22783 assert_eq!(highlighted_edits.text, "Hello, world!");
22784 assert_eq!(highlighted_edits.highlights.len(), 1);
22785 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22786 assert_eq!(
22787 highlighted_edits.highlights[0].1.background_color,
22788 Some(cx.theme().status().deleted_background)
22789 );
22790 },
22791 )
22792 .await;
22793
22794 // Insertion
22795 assert_highlighted_edits(
22796 "Hello, world!",
22797 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22798 true,
22799 cx,
22800 |highlighted_edits, cx| {
22801 assert_eq!(highlighted_edits.highlights.len(), 1);
22802 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22803 assert_eq!(
22804 highlighted_edits.highlights[0].1.background_color,
22805 Some(cx.theme().status().created_background)
22806 );
22807 },
22808 )
22809 .await;
22810}
22811
22812async fn assert_highlighted_edits(
22813 text: &str,
22814 edits: Vec<(Range<Point>, String)>,
22815 include_deletions: bool,
22816 cx: &mut TestAppContext,
22817 assertion_fn: impl Fn(HighlightedText, &App),
22818) {
22819 let window = cx.add_window(|window, cx| {
22820 let buffer = MultiBuffer::build_simple(text, cx);
22821 Editor::new(EditorMode::full(), buffer, None, window, cx)
22822 });
22823 let cx = &mut VisualTestContext::from_window(*window, cx);
22824
22825 let (buffer, snapshot) = window
22826 .update(cx, |editor, _window, cx| {
22827 (
22828 editor.buffer().clone(),
22829 editor.buffer().read(cx).snapshot(cx),
22830 )
22831 })
22832 .unwrap();
22833
22834 let edits = edits
22835 .into_iter()
22836 .map(|(range, edit)| {
22837 (
22838 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22839 edit,
22840 )
22841 })
22842 .collect::<Vec<_>>();
22843
22844 let text_anchor_edits = edits
22845 .clone()
22846 .into_iter()
22847 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22848 .collect::<Vec<_>>();
22849
22850 let edit_preview = window
22851 .update(cx, |_, _window, cx| {
22852 buffer
22853 .read(cx)
22854 .as_singleton()
22855 .unwrap()
22856 .read(cx)
22857 .preview_edits(text_anchor_edits.into(), cx)
22858 })
22859 .unwrap()
22860 .await;
22861
22862 cx.update(|_window, cx| {
22863 let highlighted_edits = edit_prediction_edit_text(
22864 snapshot.as_singleton().unwrap().2,
22865 &edits,
22866 &edit_preview,
22867 include_deletions,
22868 cx,
22869 );
22870 assertion_fn(highlighted_edits, cx)
22871 });
22872}
22873
22874#[track_caller]
22875fn assert_breakpoint(
22876 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22877 path: &Arc<Path>,
22878 expected: Vec<(u32, Breakpoint)>,
22879) {
22880 if expected.is_empty() {
22881 assert!(!breakpoints.contains_key(path), "{}", path.display());
22882 } else {
22883 let mut breakpoint = breakpoints
22884 .get(path)
22885 .unwrap()
22886 .iter()
22887 .map(|breakpoint| {
22888 (
22889 breakpoint.row,
22890 Breakpoint {
22891 message: breakpoint.message.clone(),
22892 state: breakpoint.state,
22893 condition: breakpoint.condition.clone(),
22894 hit_condition: breakpoint.hit_condition.clone(),
22895 },
22896 )
22897 })
22898 .collect::<Vec<_>>();
22899
22900 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22901
22902 assert_eq!(expected, breakpoint);
22903 }
22904}
22905
22906fn add_log_breakpoint_at_cursor(
22907 editor: &mut Editor,
22908 log_message: &str,
22909 window: &mut Window,
22910 cx: &mut Context<Editor>,
22911) {
22912 let (anchor, bp) = editor
22913 .breakpoints_at_cursors(window, cx)
22914 .first()
22915 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22916 .unwrap_or_else(|| {
22917 let snapshot = editor.snapshot(window, cx);
22918 let cursor_position: Point =
22919 editor.selections.newest(&snapshot.display_snapshot).head();
22920
22921 let breakpoint_position = snapshot
22922 .buffer_snapshot()
22923 .anchor_before(Point::new(cursor_position.row, 0));
22924
22925 (breakpoint_position, Breakpoint::new_log(log_message))
22926 });
22927
22928 editor.edit_breakpoint_at_anchor(
22929 anchor,
22930 bp,
22931 BreakpointEditAction::EditLogMessage(log_message.into()),
22932 cx,
22933 );
22934}
22935
22936#[gpui::test]
22937async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22938 init_test(cx, |_| {});
22939
22940 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22941 let fs = FakeFs::new(cx.executor());
22942 fs.insert_tree(
22943 path!("/a"),
22944 json!({
22945 "main.rs": sample_text,
22946 }),
22947 )
22948 .await;
22949 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22950 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22951 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22952
22953 let fs = FakeFs::new(cx.executor());
22954 fs.insert_tree(
22955 path!("/a"),
22956 json!({
22957 "main.rs": sample_text,
22958 }),
22959 )
22960 .await;
22961 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22962 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22963 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22964 let worktree_id = workspace
22965 .update(cx, |workspace, _window, cx| {
22966 workspace.project().update(cx, |project, cx| {
22967 project.worktrees(cx).next().unwrap().read(cx).id()
22968 })
22969 })
22970 .unwrap();
22971
22972 let buffer = project
22973 .update(cx, |project, cx| {
22974 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22975 })
22976 .await
22977 .unwrap();
22978
22979 let (editor, cx) = cx.add_window_view(|window, cx| {
22980 Editor::new(
22981 EditorMode::full(),
22982 MultiBuffer::build_from_buffer(buffer, cx),
22983 Some(project.clone()),
22984 window,
22985 cx,
22986 )
22987 });
22988
22989 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22990 let abs_path = project.read_with(cx, |project, cx| {
22991 project
22992 .absolute_path(&project_path, cx)
22993 .map(Arc::from)
22994 .unwrap()
22995 });
22996
22997 // assert we can add breakpoint on the first line
22998 editor.update_in(cx, |editor, window, cx| {
22999 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23000 editor.move_to_end(&MoveToEnd, window, cx);
23001 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23002 });
23003
23004 let breakpoints = editor.update(cx, |editor, cx| {
23005 editor
23006 .breakpoint_store()
23007 .as_ref()
23008 .unwrap()
23009 .read(cx)
23010 .all_source_breakpoints(cx)
23011 });
23012
23013 assert_eq!(1, breakpoints.len());
23014 assert_breakpoint(
23015 &breakpoints,
23016 &abs_path,
23017 vec![
23018 (0, Breakpoint::new_standard()),
23019 (3, Breakpoint::new_standard()),
23020 ],
23021 );
23022
23023 editor.update_in(cx, |editor, window, cx| {
23024 editor.move_to_beginning(&MoveToBeginning, window, cx);
23025 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23026 });
23027
23028 let breakpoints = editor.update(cx, |editor, cx| {
23029 editor
23030 .breakpoint_store()
23031 .as_ref()
23032 .unwrap()
23033 .read(cx)
23034 .all_source_breakpoints(cx)
23035 });
23036
23037 assert_eq!(1, breakpoints.len());
23038 assert_breakpoint(
23039 &breakpoints,
23040 &abs_path,
23041 vec![(3, Breakpoint::new_standard())],
23042 );
23043
23044 editor.update_in(cx, |editor, window, cx| {
23045 editor.move_to_end(&MoveToEnd, window, cx);
23046 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23047 });
23048
23049 let breakpoints = editor.update(cx, |editor, cx| {
23050 editor
23051 .breakpoint_store()
23052 .as_ref()
23053 .unwrap()
23054 .read(cx)
23055 .all_source_breakpoints(cx)
23056 });
23057
23058 assert_eq!(0, breakpoints.len());
23059 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23060}
23061
23062#[gpui::test]
23063async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23064 init_test(cx, |_| {});
23065
23066 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23067
23068 let fs = FakeFs::new(cx.executor());
23069 fs.insert_tree(
23070 path!("/a"),
23071 json!({
23072 "main.rs": sample_text,
23073 }),
23074 )
23075 .await;
23076 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23077 let (workspace, cx) =
23078 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23079
23080 let worktree_id = workspace.update(cx, |workspace, cx| {
23081 workspace.project().update(cx, |project, cx| {
23082 project.worktrees(cx).next().unwrap().read(cx).id()
23083 })
23084 });
23085
23086 let buffer = project
23087 .update(cx, |project, cx| {
23088 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23089 })
23090 .await
23091 .unwrap();
23092
23093 let (editor, cx) = cx.add_window_view(|window, cx| {
23094 Editor::new(
23095 EditorMode::full(),
23096 MultiBuffer::build_from_buffer(buffer, cx),
23097 Some(project.clone()),
23098 window,
23099 cx,
23100 )
23101 });
23102
23103 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23104 let abs_path = project.read_with(cx, |project, cx| {
23105 project
23106 .absolute_path(&project_path, cx)
23107 .map(Arc::from)
23108 .unwrap()
23109 });
23110
23111 editor.update_in(cx, |editor, window, cx| {
23112 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23113 });
23114
23115 let breakpoints = editor.update(cx, |editor, cx| {
23116 editor
23117 .breakpoint_store()
23118 .as_ref()
23119 .unwrap()
23120 .read(cx)
23121 .all_source_breakpoints(cx)
23122 });
23123
23124 assert_breakpoint(
23125 &breakpoints,
23126 &abs_path,
23127 vec![(0, Breakpoint::new_log("hello world"))],
23128 );
23129
23130 // Removing a log message from a log breakpoint should remove it
23131 editor.update_in(cx, |editor, window, cx| {
23132 add_log_breakpoint_at_cursor(editor, "", window, cx);
23133 });
23134
23135 let breakpoints = editor.update(cx, |editor, cx| {
23136 editor
23137 .breakpoint_store()
23138 .as_ref()
23139 .unwrap()
23140 .read(cx)
23141 .all_source_breakpoints(cx)
23142 });
23143
23144 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23145
23146 editor.update_in(cx, |editor, window, cx| {
23147 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23148 editor.move_to_end(&MoveToEnd, window, cx);
23149 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23150 // Not adding a log message to a standard breakpoint shouldn't remove it
23151 add_log_breakpoint_at_cursor(editor, "", window, cx);
23152 });
23153
23154 let breakpoints = editor.update(cx, |editor, cx| {
23155 editor
23156 .breakpoint_store()
23157 .as_ref()
23158 .unwrap()
23159 .read(cx)
23160 .all_source_breakpoints(cx)
23161 });
23162
23163 assert_breakpoint(
23164 &breakpoints,
23165 &abs_path,
23166 vec![
23167 (0, Breakpoint::new_standard()),
23168 (3, Breakpoint::new_standard()),
23169 ],
23170 );
23171
23172 editor.update_in(cx, |editor, window, cx| {
23173 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23174 });
23175
23176 let breakpoints = editor.update(cx, |editor, cx| {
23177 editor
23178 .breakpoint_store()
23179 .as_ref()
23180 .unwrap()
23181 .read(cx)
23182 .all_source_breakpoints(cx)
23183 });
23184
23185 assert_breakpoint(
23186 &breakpoints,
23187 &abs_path,
23188 vec![
23189 (0, Breakpoint::new_standard()),
23190 (3, Breakpoint::new_log("hello world")),
23191 ],
23192 );
23193
23194 editor.update_in(cx, |editor, window, cx| {
23195 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23196 });
23197
23198 let breakpoints = editor.update(cx, |editor, cx| {
23199 editor
23200 .breakpoint_store()
23201 .as_ref()
23202 .unwrap()
23203 .read(cx)
23204 .all_source_breakpoints(cx)
23205 });
23206
23207 assert_breakpoint(
23208 &breakpoints,
23209 &abs_path,
23210 vec![
23211 (0, Breakpoint::new_standard()),
23212 (3, Breakpoint::new_log("hello Earth!!")),
23213 ],
23214 );
23215}
23216
23217/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23218/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23219/// or when breakpoints were placed out of order. This tests for a regression too
23220#[gpui::test]
23221async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23222 init_test(cx, |_| {});
23223
23224 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23225 let fs = FakeFs::new(cx.executor());
23226 fs.insert_tree(
23227 path!("/a"),
23228 json!({
23229 "main.rs": sample_text,
23230 }),
23231 )
23232 .await;
23233 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23234 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23235 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23236
23237 let fs = FakeFs::new(cx.executor());
23238 fs.insert_tree(
23239 path!("/a"),
23240 json!({
23241 "main.rs": sample_text,
23242 }),
23243 )
23244 .await;
23245 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23246 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23247 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23248 let worktree_id = workspace
23249 .update(cx, |workspace, _window, cx| {
23250 workspace.project().update(cx, |project, cx| {
23251 project.worktrees(cx).next().unwrap().read(cx).id()
23252 })
23253 })
23254 .unwrap();
23255
23256 let buffer = project
23257 .update(cx, |project, cx| {
23258 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23259 })
23260 .await
23261 .unwrap();
23262
23263 let (editor, cx) = cx.add_window_view(|window, cx| {
23264 Editor::new(
23265 EditorMode::full(),
23266 MultiBuffer::build_from_buffer(buffer, cx),
23267 Some(project.clone()),
23268 window,
23269 cx,
23270 )
23271 });
23272
23273 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23274 let abs_path = project.read_with(cx, |project, cx| {
23275 project
23276 .absolute_path(&project_path, cx)
23277 .map(Arc::from)
23278 .unwrap()
23279 });
23280
23281 // assert we can add breakpoint on the first line
23282 editor.update_in(cx, |editor, window, cx| {
23283 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23284 editor.move_to_end(&MoveToEnd, window, cx);
23285 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23286 editor.move_up(&MoveUp, window, cx);
23287 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23288 });
23289
23290 let breakpoints = editor.update(cx, |editor, cx| {
23291 editor
23292 .breakpoint_store()
23293 .as_ref()
23294 .unwrap()
23295 .read(cx)
23296 .all_source_breakpoints(cx)
23297 });
23298
23299 assert_eq!(1, breakpoints.len());
23300 assert_breakpoint(
23301 &breakpoints,
23302 &abs_path,
23303 vec![
23304 (0, Breakpoint::new_standard()),
23305 (2, Breakpoint::new_standard()),
23306 (3, Breakpoint::new_standard()),
23307 ],
23308 );
23309
23310 editor.update_in(cx, |editor, window, cx| {
23311 editor.move_to_beginning(&MoveToBeginning, window, cx);
23312 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23313 editor.move_to_end(&MoveToEnd, window, cx);
23314 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23315 // Disabling a breakpoint that doesn't exist should do nothing
23316 editor.move_up(&MoveUp, window, cx);
23317 editor.move_up(&MoveUp, window, cx);
23318 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23319 });
23320
23321 let breakpoints = editor.update(cx, |editor, cx| {
23322 editor
23323 .breakpoint_store()
23324 .as_ref()
23325 .unwrap()
23326 .read(cx)
23327 .all_source_breakpoints(cx)
23328 });
23329
23330 let disable_breakpoint = {
23331 let mut bp = Breakpoint::new_standard();
23332 bp.state = BreakpointState::Disabled;
23333 bp
23334 };
23335
23336 assert_eq!(1, breakpoints.len());
23337 assert_breakpoint(
23338 &breakpoints,
23339 &abs_path,
23340 vec![
23341 (0, disable_breakpoint.clone()),
23342 (2, Breakpoint::new_standard()),
23343 (3, disable_breakpoint.clone()),
23344 ],
23345 );
23346
23347 editor.update_in(cx, |editor, window, cx| {
23348 editor.move_to_beginning(&MoveToBeginning, window, cx);
23349 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23350 editor.move_to_end(&MoveToEnd, window, cx);
23351 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23352 editor.move_up(&MoveUp, window, cx);
23353 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23354 });
23355
23356 let breakpoints = editor.update(cx, |editor, cx| {
23357 editor
23358 .breakpoint_store()
23359 .as_ref()
23360 .unwrap()
23361 .read(cx)
23362 .all_source_breakpoints(cx)
23363 });
23364
23365 assert_eq!(1, breakpoints.len());
23366 assert_breakpoint(
23367 &breakpoints,
23368 &abs_path,
23369 vec![
23370 (0, Breakpoint::new_standard()),
23371 (2, disable_breakpoint),
23372 (3, Breakpoint::new_standard()),
23373 ],
23374 );
23375}
23376
23377#[gpui::test]
23378async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23379 init_test(cx, |_| {});
23380 let capabilities = lsp::ServerCapabilities {
23381 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23382 prepare_provider: Some(true),
23383 work_done_progress_options: Default::default(),
23384 })),
23385 ..Default::default()
23386 };
23387 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23388
23389 cx.set_state(indoc! {"
23390 struct Fˇoo {}
23391 "});
23392
23393 cx.update_editor(|editor, _, cx| {
23394 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23395 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23396 editor.highlight_background::<DocumentHighlightRead>(
23397 &[highlight_range],
23398 |theme| theme.colors().editor_document_highlight_read_background,
23399 cx,
23400 );
23401 });
23402
23403 let mut prepare_rename_handler = cx
23404 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23405 move |_, _, _| async move {
23406 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23407 start: lsp::Position {
23408 line: 0,
23409 character: 7,
23410 },
23411 end: lsp::Position {
23412 line: 0,
23413 character: 10,
23414 },
23415 })))
23416 },
23417 );
23418 let prepare_rename_task = cx
23419 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23420 .expect("Prepare rename was not started");
23421 prepare_rename_handler.next().await.unwrap();
23422 prepare_rename_task.await.expect("Prepare rename failed");
23423
23424 let mut rename_handler =
23425 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23426 let edit = lsp::TextEdit {
23427 range: lsp::Range {
23428 start: lsp::Position {
23429 line: 0,
23430 character: 7,
23431 },
23432 end: lsp::Position {
23433 line: 0,
23434 character: 10,
23435 },
23436 },
23437 new_text: "FooRenamed".to_string(),
23438 };
23439 Ok(Some(lsp::WorkspaceEdit::new(
23440 // Specify the same edit twice
23441 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23442 )))
23443 });
23444 let rename_task = cx
23445 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23446 .expect("Confirm rename was not started");
23447 rename_handler.next().await.unwrap();
23448 rename_task.await.expect("Confirm rename failed");
23449 cx.run_until_parked();
23450
23451 // Despite two edits, only one is actually applied as those are identical
23452 cx.assert_editor_state(indoc! {"
23453 struct FooRenamedˇ {}
23454 "});
23455}
23456
23457#[gpui::test]
23458async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23459 init_test(cx, |_| {});
23460 // These capabilities indicate that the server does not support prepare rename.
23461 let capabilities = lsp::ServerCapabilities {
23462 rename_provider: Some(lsp::OneOf::Left(true)),
23463 ..Default::default()
23464 };
23465 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23466
23467 cx.set_state(indoc! {"
23468 struct Fˇoo {}
23469 "});
23470
23471 cx.update_editor(|editor, _window, cx| {
23472 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23473 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23474 editor.highlight_background::<DocumentHighlightRead>(
23475 &[highlight_range],
23476 |theme| theme.colors().editor_document_highlight_read_background,
23477 cx,
23478 );
23479 });
23480
23481 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23482 .expect("Prepare rename was not started")
23483 .await
23484 .expect("Prepare rename failed");
23485
23486 let mut rename_handler =
23487 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23488 let edit = lsp::TextEdit {
23489 range: lsp::Range {
23490 start: lsp::Position {
23491 line: 0,
23492 character: 7,
23493 },
23494 end: lsp::Position {
23495 line: 0,
23496 character: 10,
23497 },
23498 },
23499 new_text: "FooRenamed".to_string(),
23500 };
23501 Ok(Some(lsp::WorkspaceEdit::new(
23502 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23503 )))
23504 });
23505 let rename_task = cx
23506 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23507 .expect("Confirm rename was not started");
23508 rename_handler.next().await.unwrap();
23509 rename_task.await.expect("Confirm rename failed");
23510 cx.run_until_parked();
23511
23512 // Correct range is renamed, as `surrounding_word` is used to find it.
23513 cx.assert_editor_state(indoc! {"
23514 struct FooRenamedˇ {}
23515 "});
23516}
23517
23518#[gpui::test]
23519async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23520 init_test(cx, |_| {});
23521 let mut cx = EditorTestContext::new(cx).await;
23522
23523 let language = Arc::new(
23524 Language::new(
23525 LanguageConfig::default(),
23526 Some(tree_sitter_html::LANGUAGE.into()),
23527 )
23528 .with_brackets_query(
23529 r#"
23530 ("<" @open "/>" @close)
23531 ("</" @open ">" @close)
23532 ("<" @open ">" @close)
23533 ("\"" @open "\"" @close)
23534 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23535 "#,
23536 )
23537 .unwrap(),
23538 );
23539 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23540
23541 cx.set_state(indoc! {"
23542 <span>ˇ</span>
23543 "});
23544 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23545 cx.assert_editor_state(indoc! {"
23546 <span>
23547 ˇ
23548 </span>
23549 "});
23550
23551 cx.set_state(indoc! {"
23552 <span><span></span>ˇ</span>
23553 "});
23554 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23555 cx.assert_editor_state(indoc! {"
23556 <span><span></span>
23557 ˇ</span>
23558 "});
23559
23560 cx.set_state(indoc! {"
23561 <span>ˇ
23562 </span>
23563 "});
23564 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23565 cx.assert_editor_state(indoc! {"
23566 <span>
23567 ˇ
23568 </span>
23569 "});
23570}
23571
23572#[gpui::test(iterations = 10)]
23573async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23574 init_test(cx, |_| {});
23575
23576 let fs = FakeFs::new(cx.executor());
23577 fs.insert_tree(
23578 path!("/dir"),
23579 json!({
23580 "a.ts": "a",
23581 }),
23582 )
23583 .await;
23584
23585 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23586 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23587 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23588
23589 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23590 language_registry.add(Arc::new(Language::new(
23591 LanguageConfig {
23592 name: "TypeScript".into(),
23593 matcher: LanguageMatcher {
23594 path_suffixes: vec!["ts".to_string()],
23595 ..Default::default()
23596 },
23597 ..Default::default()
23598 },
23599 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23600 )));
23601 let mut fake_language_servers = language_registry.register_fake_lsp(
23602 "TypeScript",
23603 FakeLspAdapter {
23604 capabilities: lsp::ServerCapabilities {
23605 code_lens_provider: Some(lsp::CodeLensOptions {
23606 resolve_provider: Some(true),
23607 }),
23608 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23609 commands: vec!["_the/command".to_string()],
23610 ..lsp::ExecuteCommandOptions::default()
23611 }),
23612 ..lsp::ServerCapabilities::default()
23613 },
23614 ..FakeLspAdapter::default()
23615 },
23616 );
23617
23618 let editor = workspace
23619 .update(cx, |workspace, window, cx| {
23620 workspace.open_abs_path(
23621 PathBuf::from(path!("/dir/a.ts")),
23622 OpenOptions::default(),
23623 window,
23624 cx,
23625 )
23626 })
23627 .unwrap()
23628 .await
23629 .unwrap()
23630 .downcast::<Editor>()
23631 .unwrap();
23632 cx.executor().run_until_parked();
23633
23634 let fake_server = fake_language_servers.next().await.unwrap();
23635
23636 let buffer = editor.update(cx, |editor, cx| {
23637 editor
23638 .buffer()
23639 .read(cx)
23640 .as_singleton()
23641 .expect("have opened a single file by path")
23642 });
23643
23644 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23645 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23646 drop(buffer_snapshot);
23647 let actions = cx
23648 .update_window(*workspace, |_, window, cx| {
23649 project.code_actions(&buffer, anchor..anchor, window, cx)
23650 })
23651 .unwrap();
23652
23653 fake_server
23654 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23655 Ok(Some(vec![
23656 lsp::CodeLens {
23657 range: lsp::Range::default(),
23658 command: Some(lsp::Command {
23659 title: "Code lens command".to_owned(),
23660 command: "_the/command".to_owned(),
23661 arguments: None,
23662 }),
23663 data: None,
23664 },
23665 lsp::CodeLens {
23666 range: lsp::Range::default(),
23667 command: Some(lsp::Command {
23668 title: "Command not in capabilities".to_owned(),
23669 command: "not in capabilities".to_owned(),
23670 arguments: None,
23671 }),
23672 data: None,
23673 },
23674 lsp::CodeLens {
23675 range: lsp::Range {
23676 start: lsp::Position {
23677 line: 1,
23678 character: 1,
23679 },
23680 end: lsp::Position {
23681 line: 1,
23682 character: 1,
23683 },
23684 },
23685 command: Some(lsp::Command {
23686 title: "Command not in range".to_owned(),
23687 command: "_the/command".to_owned(),
23688 arguments: None,
23689 }),
23690 data: None,
23691 },
23692 ]))
23693 })
23694 .next()
23695 .await;
23696
23697 let actions = actions.await.unwrap();
23698 assert_eq!(
23699 actions.len(),
23700 1,
23701 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23702 );
23703 let action = actions[0].clone();
23704 let apply = project.update(cx, |project, cx| {
23705 project.apply_code_action(buffer.clone(), action, true, cx)
23706 });
23707
23708 // Resolving the code action does not populate its edits. In absence of
23709 // edits, we must execute the given command.
23710 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23711 |mut lens, _| async move {
23712 let lens_command = lens.command.as_mut().expect("should have a command");
23713 assert_eq!(lens_command.title, "Code lens command");
23714 lens_command.arguments = Some(vec![json!("the-argument")]);
23715 Ok(lens)
23716 },
23717 );
23718
23719 // While executing the command, the language server sends the editor
23720 // a `workspaceEdit` request.
23721 fake_server
23722 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23723 let fake = fake_server.clone();
23724 move |params, _| {
23725 assert_eq!(params.command, "_the/command");
23726 let fake = fake.clone();
23727 async move {
23728 fake.server
23729 .request::<lsp::request::ApplyWorkspaceEdit>(
23730 lsp::ApplyWorkspaceEditParams {
23731 label: None,
23732 edit: lsp::WorkspaceEdit {
23733 changes: Some(
23734 [(
23735 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23736 vec![lsp::TextEdit {
23737 range: lsp::Range::new(
23738 lsp::Position::new(0, 0),
23739 lsp::Position::new(0, 0),
23740 ),
23741 new_text: "X".into(),
23742 }],
23743 )]
23744 .into_iter()
23745 .collect(),
23746 ),
23747 ..lsp::WorkspaceEdit::default()
23748 },
23749 },
23750 )
23751 .await
23752 .into_response()
23753 .unwrap();
23754 Ok(Some(json!(null)))
23755 }
23756 }
23757 })
23758 .next()
23759 .await;
23760
23761 // Applying the code lens command returns a project transaction containing the edits
23762 // sent by the language server in its `workspaceEdit` request.
23763 let transaction = apply.await.unwrap();
23764 assert!(transaction.0.contains_key(&buffer));
23765 buffer.update(cx, |buffer, cx| {
23766 assert_eq!(buffer.text(), "Xa");
23767 buffer.undo(cx);
23768 assert_eq!(buffer.text(), "a");
23769 });
23770
23771 let actions_after_edits = cx
23772 .update_window(*workspace, |_, window, cx| {
23773 project.code_actions(&buffer, anchor..anchor, window, cx)
23774 })
23775 .unwrap()
23776 .await
23777 .unwrap();
23778 assert_eq!(
23779 actions, actions_after_edits,
23780 "For the same selection, same code lens actions should be returned"
23781 );
23782
23783 let _responses =
23784 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23785 panic!("No more code lens requests are expected");
23786 });
23787 editor.update_in(cx, |editor, window, cx| {
23788 editor.select_all(&SelectAll, window, cx);
23789 });
23790 cx.executor().run_until_parked();
23791 let new_actions = cx
23792 .update_window(*workspace, |_, window, cx| {
23793 project.code_actions(&buffer, anchor..anchor, window, cx)
23794 })
23795 .unwrap()
23796 .await
23797 .unwrap();
23798 assert_eq!(
23799 actions, new_actions,
23800 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23801 );
23802}
23803
23804#[gpui::test]
23805async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23806 init_test(cx, |_| {});
23807
23808 let fs = FakeFs::new(cx.executor());
23809 let main_text = r#"fn main() {
23810println!("1");
23811println!("2");
23812println!("3");
23813println!("4");
23814println!("5");
23815}"#;
23816 let lib_text = "mod foo {}";
23817 fs.insert_tree(
23818 path!("/a"),
23819 json!({
23820 "lib.rs": lib_text,
23821 "main.rs": main_text,
23822 }),
23823 )
23824 .await;
23825
23826 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23827 let (workspace, cx) =
23828 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23829 let worktree_id = workspace.update(cx, |workspace, cx| {
23830 workspace.project().update(cx, |project, cx| {
23831 project.worktrees(cx).next().unwrap().read(cx).id()
23832 })
23833 });
23834
23835 let expected_ranges = vec![
23836 Point::new(0, 0)..Point::new(0, 0),
23837 Point::new(1, 0)..Point::new(1, 1),
23838 Point::new(2, 0)..Point::new(2, 2),
23839 Point::new(3, 0)..Point::new(3, 3),
23840 ];
23841
23842 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23843 let editor_1 = workspace
23844 .update_in(cx, |workspace, window, cx| {
23845 workspace.open_path(
23846 (worktree_id, rel_path("main.rs")),
23847 Some(pane_1.downgrade()),
23848 true,
23849 window,
23850 cx,
23851 )
23852 })
23853 .unwrap()
23854 .await
23855 .downcast::<Editor>()
23856 .unwrap();
23857 pane_1.update(cx, |pane, cx| {
23858 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23859 open_editor.update(cx, |editor, cx| {
23860 assert_eq!(
23861 editor.display_text(cx),
23862 main_text,
23863 "Original main.rs text on initial open",
23864 );
23865 assert_eq!(
23866 editor
23867 .selections
23868 .all::<Point>(&editor.display_snapshot(cx))
23869 .into_iter()
23870 .map(|s| s.range())
23871 .collect::<Vec<_>>(),
23872 vec![Point::zero()..Point::zero()],
23873 "Default selections on initial open",
23874 );
23875 })
23876 });
23877 editor_1.update_in(cx, |editor, window, cx| {
23878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23879 s.select_ranges(expected_ranges.clone());
23880 });
23881 });
23882
23883 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23884 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23885 });
23886 let editor_2 = workspace
23887 .update_in(cx, |workspace, window, cx| {
23888 workspace.open_path(
23889 (worktree_id, rel_path("main.rs")),
23890 Some(pane_2.downgrade()),
23891 true,
23892 window,
23893 cx,
23894 )
23895 })
23896 .unwrap()
23897 .await
23898 .downcast::<Editor>()
23899 .unwrap();
23900 pane_2.update(cx, |pane, cx| {
23901 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23902 open_editor.update(cx, |editor, cx| {
23903 assert_eq!(
23904 editor.display_text(cx),
23905 main_text,
23906 "Original main.rs text on initial open in another panel",
23907 );
23908 assert_eq!(
23909 editor
23910 .selections
23911 .all::<Point>(&editor.display_snapshot(cx))
23912 .into_iter()
23913 .map(|s| s.range())
23914 .collect::<Vec<_>>(),
23915 vec![Point::zero()..Point::zero()],
23916 "Default selections on initial open in another panel",
23917 );
23918 })
23919 });
23920
23921 editor_2.update_in(cx, |editor, window, cx| {
23922 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23923 });
23924
23925 let _other_editor_1 = workspace
23926 .update_in(cx, |workspace, window, cx| {
23927 workspace.open_path(
23928 (worktree_id, rel_path("lib.rs")),
23929 Some(pane_1.downgrade()),
23930 true,
23931 window,
23932 cx,
23933 )
23934 })
23935 .unwrap()
23936 .await
23937 .downcast::<Editor>()
23938 .unwrap();
23939 pane_1
23940 .update_in(cx, |pane, window, cx| {
23941 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23942 })
23943 .await
23944 .unwrap();
23945 drop(editor_1);
23946 pane_1.update(cx, |pane, cx| {
23947 pane.active_item()
23948 .unwrap()
23949 .downcast::<Editor>()
23950 .unwrap()
23951 .update(cx, |editor, cx| {
23952 assert_eq!(
23953 editor.display_text(cx),
23954 lib_text,
23955 "Other file should be open and active",
23956 );
23957 });
23958 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23959 });
23960
23961 let _other_editor_2 = workspace
23962 .update_in(cx, |workspace, window, cx| {
23963 workspace.open_path(
23964 (worktree_id, rel_path("lib.rs")),
23965 Some(pane_2.downgrade()),
23966 true,
23967 window,
23968 cx,
23969 )
23970 })
23971 .unwrap()
23972 .await
23973 .downcast::<Editor>()
23974 .unwrap();
23975 pane_2
23976 .update_in(cx, |pane, window, cx| {
23977 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23978 })
23979 .await
23980 .unwrap();
23981 drop(editor_2);
23982 pane_2.update(cx, |pane, cx| {
23983 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23984 open_editor.update(cx, |editor, cx| {
23985 assert_eq!(
23986 editor.display_text(cx),
23987 lib_text,
23988 "Other file should be open and active in another panel too",
23989 );
23990 });
23991 assert_eq!(
23992 pane.items().count(),
23993 1,
23994 "No other editors should be open in another pane",
23995 );
23996 });
23997
23998 let _editor_1_reopened = workspace
23999 .update_in(cx, |workspace, window, cx| {
24000 workspace.open_path(
24001 (worktree_id, rel_path("main.rs")),
24002 Some(pane_1.downgrade()),
24003 true,
24004 window,
24005 cx,
24006 )
24007 })
24008 .unwrap()
24009 .await
24010 .downcast::<Editor>()
24011 .unwrap();
24012 let _editor_2_reopened = workspace
24013 .update_in(cx, |workspace, window, cx| {
24014 workspace.open_path(
24015 (worktree_id, rel_path("main.rs")),
24016 Some(pane_2.downgrade()),
24017 true,
24018 window,
24019 cx,
24020 )
24021 })
24022 .unwrap()
24023 .await
24024 .downcast::<Editor>()
24025 .unwrap();
24026 pane_1.update(cx, |pane, cx| {
24027 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24028 open_editor.update(cx, |editor, cx| {
24029 assert_eq!(
24030 editor.display_text(cx),
24031 main_text,
24032 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24033 );
24034 assert_eq!(
24035 editor
24036 .selections
24037 .all::<Point>(&editor.display_snapshot(cx))
24038 .into_iter()
24039 .map(|s| s.range())
24040 .collect::<Vec<_>>(),
24041 expected_ranges,
24042 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24043 );
24044 })
24045 });
24046 pane_2.update(cx, |pane, cx| {
24047 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24048 open_editor.update(cx, |editor, cx| {
24049 assert_eq!(
24050 editor.display_text(cx),
24051 r#"fn main() {
24052⋯rintln!("1");
24053⋯intln!("2");
24054⋯ntln!("3");
24055println!("4");
24056println!("5");
24057}"#,
24058 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24059 );
24060 assert_eq!(
24061 editor
24062 .selections
24063 .all::<Point>(&editor.display_snapshot(cx))
24064 .into_iter()
24065 .map(|s| s.range())
24066 .collect::<Vec<_>>(),
24067 vec![Point::zero()..Point::zero()],
24068 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24069 );
24070 })
24071 });
24072}
24073
24074#[gpui::test]
24075async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24076 init_test(cx, |_| {});
24077
24078 let fs = FakeFs::new(cx.executor());
24079 let main_text = r#"fn main() {
24080println!("1");
24081println!("2");
24082println!("3");
24083println!("4");
24084println!("5");
24085}"#;
24086 let lib_text = "mod foo {}";
24087 fs.insert_tree(
24088 path!("/a"),
24089 json!({
24090 "lib.rs": lib_text,
24091 "main.rs": main_text,
24092 }),
24093 )
24094 .await;
24095
24096 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24097 let (workspace, cx) =
24098 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24099 let worktree_id = workspace.update(cx, |workspace, cx| {
24100 workspace.project().update(cx, |project, cx| {
24101 project.worktrees(cx).next().unwrap().read(cx).id()
24102 })
24103 });
24104
24105 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24106 let editor = workspace
24107 .update_in(cx, |workspace, window, cx| {
24108 workspace.open_path(
24109 (worktree_id, rel_path("main.rs")),
24110 Some(pane.downgrade()),
24111 true,
24112 window,
24113 cx,
24114 )
24115 })
24116 .unwrap()
24117 .await
24118 .downcast::<Editor>()
24119 .unwrap();
24120 pane.update(cx, |pane, cx| {
24121 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24122 open_editor.update(cx, |editor, cx| {
24123 assert_eq!(
24124 editor.display_text(cx),
24125 main_text,
24126 "Original main.rs text on initial open",
24127 );
24128 })
24129 });
24130 editor.update_in(cx, |editor, window, cx| {
24131 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24132 });
24133
24134 cx.update_global(|store: &mut SettingsStore, cx| {
24135 store.update_user_settings(cx, |s| {
24136 s.workspace.restore_on_file_reopen = Some(false);
24137 });
24138 });
24139 editor.update_in(cx, |editor, window, cx| {
24140 editor.fold_ranges(
24141 vec![
24142 Point::new(1, 0)..Point::new(1, 1),
24143 Point::new(2, 0)..Point::new(2, 2),
24144 Point::new(3, 0)..Point::new(3, 3),
24145 ],
24146 false,
24147 window,
24148 cx,
24149 );
24150 });
24151 pane.update_in(cx, |pane, window, cx| {
24152 pane.close_all_items(&CloseAllItems::default(), window, cx)
24153 })
24154 .await
24155 .unwrap();
24156 pane.update(cx, |pane, _| {
24157 assert!(pane.active_item().is_none());
24158 });
24159 cx.update_global(|store: &mut SettingsStore, cx| {
24160 store.update_user_settings(cx, |s| {
24161 s.workspace.restore_on_file_reopen = Some(true);
24162 });
24163 });
24164
24165 let _editor_reopened = workspace
24166 .update_in(cx, |workspace, window, cx| {
24167 workspace.open_path(
24168 (worktree_id, rel_path("main.rs")),
24169 Some(pane.downgrade()),
24170 true,
24171 window,
24172 cx,
24173 )
24174 })
24175 .unwrap()
24176 .await
24177 .downcast::<Editor>()
24178 .unwrap();
24179 pane.update(cx, |pane, cx| {
24180 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24181 open_editor.update(cx, |editor, cx| {
24182 assert_eq!(
24183 editor.display_text(cx),
24184 main_text,
24185 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24186 );
24187 })
24188 });
24189}
24190
24191#[gpui::test]
24192async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24193 struct EmptyModalView {
24194 focus_handle: gpui::FocusHandle,
24195 }
24196 impl EventEmitter<DismissEvent> for EmptyModalView {}
24197 impl Render for EmptyModalView {
24198 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24199 div()
24200 }
24201 }
24202 impl Focusable for EmptyModalView {
24203 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24204 self.focus_handle.clone()
24205 }
24206 }
24207 impl workspace::ModalView for EmptyModalView {}
24208 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24209 EmptyModalView {
24210 focus_handle: cx.focus_handle(),
24211 }
24212 }
24213
24214 init_test(cx, |_| {});
24215
24216 let fs = FakeFs::new(cx.executor());
24217 let project = Project::test(fs, [], cx).await;
24218 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24219 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24220 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24221 let editor = cx.new_window_entity(|window, cx| {
24222 Editor::new(
24223 EditorMode::full(),
24224 buffer,
24225 Some(project.clone()),
24226 window,
24227 cx,
24228 )
24229 });
24230 workspace
24231 .update(cx, |workspace, window, cx| {
24232 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24233 })
24234 .unwrap();
24235 editor.update_in(cx, |editor, window, cx| {
24236 editor.open_context_menu(&OpenContextMenu, window, cx);
24237 assert!(editor.mouse_context_menu.is_some());
24238 });
24239 workspace
24240 .update(cx, |workspace, window, cx| {
24241 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24242 })
24243 .unwrap();
24244 cx.read(|cx| {
24245 assert!(editor.read(cx).mouse_context_menu.is_none());
24246 });
24247}
24248
24249fn set_linked_edit_ranges(
24250 opening: (Point, Point),
24251 closing: (Point, Point),
24252 editor: &mut Editor,
24253 cx: &mut Context<Editor>,
24254) {
24255 let Some((buffer, _)) = editor
24256 .buffer
24257 .read(cx)
24258 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24259 else {
24260 panic!("Failed to get buffer for selection position");
24261 };
24262 let buffer = buffer.read(cx);
24263 let buffer_id = buffer.remote_id();
24264 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24265 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24266 let mut linked_ranges = HashMap::default();
24267 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24268 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24269}
24270
24271#[gpui::test]
24272async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24273 init_test(cx, |_| {});
24274
24275 let fs = FakeFs::new(cx.executor());
24276 fs.insert_file(path!("/file.html"), Default::default())
24277 .await;
24278
24279 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24280
24281 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24282 let html_language = Arc::new(Language::new(
24283 LanguageConfig {
24284 name: "HTML".into(),
24285 matcher: LanguageMatcher {
24286 path_suffixes: vec!["html".to_string()],
24287 ..LanguageMatcher::default()
24288 },
24289 brackets: BracketPairConfig {
24290 pairs: vec![BracketPair {
24291 start: "<".into(),
24292 end: ">".into(),
24293 close: true,
24294 ..Default::default()
24295 }],
24296 ..Default::default()
24297 },
24298 ..Default::default()
24299 },
24300 Some(tree_sitter_html::LANGUAGE.into()),
24301 ));
24302 language_registry.add(html_language);
24303 let mut fake_servers = language_registry.register_fake_lsp(
24304 "HTML",
24305 FakeLspAdapter {
24306 capabilities: lsp::ServerCapabilities {
24307 completion_provider: Some(lsp::CompletionOptions {
24308 resolve_provider: Some(true),
24309 ..Default::default()
24310 }),
24311 ..Default::default()
24312 },
24313 ..Default::default()
24314 },
24315 );
24316
24317 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24318 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24319
24320 let worktree_id = workspace
24321 .update(cx, |workspace, _window, cx| {
24322 workspace.project().update(cx, |project, cx| {
24323 project.worktrees(cx).next().unwrap().read(cx).id()
24324 })
24325 })
24326 .unwrap();
24327 project
24328 .update(cx, |project, cx| {
24329 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24330 })
24331 .await
24332 .unwrap();
24333 let editor = workspace
24334 .update(cx, |workspace, window, cx| {
24335 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24336 })
24337 .unwrap()
24338 .await
24339 .unwrap()
24340 .downcast::<Editor>()
24341 .unwrap();
24342
24343 let fake_server = fake_servers.next().await.unwrap();
24344 editor.update_in(cx, |editor, window, cx| {
24345 editor.set_text("<ad></ad>", window, cx);
24346 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24347 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24348 });
24349 set_linked_edit_ranges(
24350 (Point::new(0, 1), Point::new(0, 3)),
24351 (Point::new(0, 6), Point::new(0, 8)),
24352 editor,
24353 cx,
24354 );
24355 });
24356 let mut completion_handle =
24357 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24358 Ok(Some(lsp::CompletionResponse::Array(vec![
24359 lsp::CompletionItem {
24360 label: "head".to_string(),
24361 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24362 lsp::InsertReplaceEdit {
24363 new_text: "head".to_string(),
24364 insert: lsp::Range::new(
24365 lsp::Position::new(0, 1),
24366 lsp::Position::new(0, 3),
24367 ),
24368 replace: lsp::Range::new(
24369 lsp::Position::new(0, 1),
24370 lsp::Position::new(0, 3),
24371 ),
24372 },
24373 )),
24374 ..Default::default()
24375 },
24376 ])))
24377 });
24378 editor.update_in(cx, |editor, window, cx| {
24379 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24380 });
24381 cx.run_until_parked();
24382 completion_handle.next().await.unwrap();
24383 editor.update(cx, |editor, _| {
24384 assert!(
24385 editor.context_menu_visible(),
24386 "Completion menu should be visible"
24387 );
24388 });
24389 editor.update_in(cx, |editor, window, cx| {
24390 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24391 });
24392 cx.executor().run_until_parked();
24393 editor.update(cx, |editor, cx| {
24394 assert_eq!(editor.text(cx), "<head></head>");
24395 });
24396}
24397
24398#[gpui::test]
24399async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24400 init_test(cx, |_| {});
24401
24402 let mut cx = EditorTestContext::new(cx).await;
24403 let language = Arc::new(Language::new(
24404 LanguageConfig {
24405 name: "TSX".into(),
24406 matcher: LanguageMatcher {
24407 path_suffixes: vec!["tsx".to_string()],
24408 ..LanguageMatcher::default()
24409 },
24410 brackets: BracketPairConfig {
24411 pairs: vec![BracketPair {
24412 start: "<".into(),
24413 end: ">".into(),
24414 close: true,
24415 ..Default::default()
24416 }],
24417 ..Default::default()
24418 },
24419 linked_edit_characters: HashSet::from_iter(['.']),
24420 ..Default::default()
24421 },
24422 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24423 ));
24424 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24425
24426 // Test typing > does not extend linked pair
24427 cx.set_state("<divˇ<div></div>");
24428 cx.update_editor(|editor, _, cx| {
24429 set_linked_edit_ranges(
24430 (Point::new(0, 1), Point::new(0, 4)),
24431 (Point::new(0, 11), Point::new(0, 14)),
24432 editor,
24433 cx,
24434 );
24435 });
24436 cx.update_editor(|editor, window, cx| {
24437 editor.handle_input(">", window, cx);
24438 });
24439 cx.assert_editor_state("<div>ˇ<div></div>");
24440
24441 // Test typing . do extend linked pair
24442 cx.set_state("<Animatedˇ></Animated>");
24443 cx.update_editor(|editor, _, cx| {
24444 set_linked_edit_ranges(
24445 (Point::new(0, 1), Point::new(0, 9)),
24446 (Point::new(0, 12), Point::new(0, 20)),
24447 editor,
24448 cx,
24449 );
24450 });
24451 cx.update_editor(|editor, window, cx| {
24452 editor.handle_input(".", window, cx);
24453 });
24454 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24455 cx.update_editor(|editor, _, cx| {
24456 set_linked_edit_ranges(
24457 (Point::new(0, 1), Point::new(0, 10)),
24458 (Point::new(0, 13), Point::new(0, 21)),
24459 editor,
24460 cx,
24461 );
24462 });
24463 cx.update_editor(|editor, window, cx| {
24464 editor.handle_input("V", window, cx);
24465 });
24466 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24467}
24468
24469#[gpui::test]
24470async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24471 init_test(cx, |_| {});
24472
24473 let fs = FakeFs::new(cx.executor());
24474 fs.insert_tree(
24475 path!("/root"),
24476 json!({
24477 "a": {
24478 "main.rs": "fn main() {}",
24479 },
24480 "foo": {
24481 "bar": {
24482 "external_file.rs": "pub mod external {}",
24483 }
24484 }
24485 }),
24486 )
24487 .await;
24488
24489 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24490 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24491 language_registry.add(rust_lang());
24492 let _fake_servers = language_registry.register_fake_lsp(
24493 "Rust",
24494 FakeLspAdapter {
24495 ..FakeLspAdapter::default()
24496 },
24497 );
24498 let (workspace, cx) =
24499 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24500 let worktree_id = workspace.update(cx, |workspace, cx| {
24501 workspace.project().update(cx, |project, cx| {
24502 project.worktrees(cx).next().unwrap().read(cx).id()
24503 })
24504 });
24505
24506 let assert_language_servers_count =
24507 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24508 project.update(cx, |project, cx| {
24509 let current = project
24510 .lsp_store()
24511 .read(cx)
24512 .as_local()
24513 .unwrap()
24514 .language_servers
24515 .len();
24516 assert_eq!(expected, current, "{context}");
24517 });
24518 };
24519
24520 assert_language_servers_count(
24521 0,
24522 "No servers should be running before any file is open",
24523 cx,
24524 );
24525 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24526 let main_editor = workspace
24527 .update_in(cx, |workspace, window, cx| {
24528 workspace.open_path(
24529 (worktree_id, rel_path("main.rs")),
24530 Some(pane.downgrade()),
24531 true,
24532 window,
24533 cx,
24534 )
24535 })
24536 .unwrap()
24537 .await
24538 .downcast::<Editor>()
24539 .unwrap();
24540 pane.update(cx, |pane, cx| {
24541 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24542 open_editor.update(cx, |editor, cx| {
24543 assert_eq!(
24544 editor.display_text(cx),
24545 "fn main() {}",
24546 "Original main.rs text on initial open",
24547 );
24548 });
24549 assert_eq!(open_editor, main_editor);
24550 });
24551 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24552
24553 let external_editor = workspace
24554 .update_in(cx, |workspace, window, cx| {
24555 workspace.open_abs_path(
24556 PathBuf::from("/root/foo/bar/external_file.rs"),
24557 OpenOptions::default(),
24558 window,
24559 cx,
24560 )
24561 })
24562 .await
24563 .expect("opening external file")
24564 .downcast::<Editor>()
24565 .expect("downcasted external file's open element to editor");
24566 pane.update(cx, |pane, cx| {
24567 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24568 open_editor.update(cx, |editor, cx| {
24569 assert_eq!(
24570 editor.display_text(cx),
24571 "pub mod external {}",
24572 "External file is open now",
24573 );
24574 });
24575 assert_eq!(open_editor, external_editor);
24576 });
24577 assert_language_servers_count(
24578 1,
24579 "Second, external, *.rs file should join the existing server",
24580 cx,
24581 );
24582
24583 pane.update_in(cx, |pane, window, cx| {
24584 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24585 })
24586 .await
24587 .unwrap();
24588 pane.update_in(cx, |pane, window, cx| {
24589 pane.navigate_backward(&Default::default(), window, cx);
24590 });
24591 cx.run_until_parked();
24592 pane.update(cx, |pane, cx| {
24593 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24594 open_editor.update(cx, |editor, cx| {
24595 assert_eq!(
24596 editor.display_text(cx),
24597 "pub mod external {}",
24598 "External file is open now",
24599 );
24600 });
24601 });
24602 assert_language_servers_count(
24603 1,
24604 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24605 cx,
24606 );
24607
24608 cx.update(|_, cx| {
24609 workspace::reload(cx);
24610 });
24611 assert_language_servers_count(
24612 1,
24613 "After reloading the worktree with local and external files opened, only one project should be started",
24614 cx,
24615 );
24616}
24617
24618#[gpui::test]
24619async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24620 init_test(cx, |_| {});
24621
24622 let mut cx = EditorTestContext::new(cx).await;
24623 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24624 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24625
24626 // test cursor move to start of each line on tab
24627 // for `if`, `elif`, `else`, `while`, `with` and `for`
24628 cx.set_state(indoc! {"
24629 def main():
24630 ˇ for item in items:
24631 ˇ while item.active:
24632 ˇ if item.value > 10:
24633 ˇ continue
24634 ˇ elif item.value < 0:
24635 ˇ break
24636 ˇ else:
24637 ˇ with item.context() as ctx:
24638 ˇ yield count
24639 ˇ else:
24640 ˇ log('while else')
24641 ˇ else:
24642 ˇ log('for else')
24643 "});
24644 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24645 cx.assert_editor_state(indoc! {"
24646 def main():
24647 ˇfor item in items:
24648 ˇwhile item.active:
24649 ˇif item.value > 10:
24650 ˇcontinue
24651 ˇelif item.value < 0:
24652 ˇbreak
24653 ˇelse:
24654 ˇwith item.context() as ctx:
24655 ˇyield count
24656 ˇelse:
24657 ˇlog('while else')
24658 ˇelse:
24659 ˇlog('for else')
24660 "});
24661 // test relative indent is preserved when tab
24662 // for `if`, `elif`, `else`, `while`, `with` and `for`
24663 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24664 cx.assert_editor_state(indoc! {"
24665 def main():
24666 ˇfor item in items:
24667 ˇwhile item.active:
24668 ˇif item.value > 10:
24669 ˇcontinue
24670 ˇelif item.value < 0:
24671 ˇbreak
24672 ˇelse:
24673 ˇwith item.context() as ctx:
24674 ˇyield count
24675 ˇelse:
24676 ˇlog('while else')
24677 ˇelse:
24678 ˇlog('for else')
24679 "});
24680
24681 // test cursor move to start of each line on tab
24682 // for `try`, `except`, `else`, `finally`, `match` and `def`
24683 cx.set_state(indoc! {"
24684 def main():
24685 ˇ try:
24686 ˇ fetch()
24687 ˇ except ValueError:
24688 ˇ handle_error()
24689 ˇ else:
24690 ˇ match value:
24691 ˇ case _:
24692 ˇ finally:
24693 ˇ def status():
24694 ˇ return 0
24695 "});
24696 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24697 cx.assert_editor_state(indoc! {"
24698 def main():
24699 ˇtry:
24700 ˇfetch()
24701 ˇexcept ValueError:
24702 ˇhandle_error()
24703 ˇelse:
24704 ˇmatch value:
24705 ˇcase _:
24706 ˇfinally:
24707 ˇdef status():
24708 ˇreturn 0
24709 "});
24710 // test relative indent is preserved when tab
24711 // for `try`, `except`, `else`, `finally`, `match` and `def`
24712 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24713 cx.assert_editor_state(indoc! {"
24714 def main():
24715 ˇtry:
24716 ˇfetch()
24717 ˇexcept ValueError:
24718 ˇhandle_error()
24719 ˇelse:
24720 ˇmatch value:
24721 ˇcase _:
24722 ˇfinally:
24723 ˇdef status():
24724 ˇreturn 0
24725 "});
24726}
24727
24728#[gpui::test]
24729async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24730 init_test(cx, |_| {});
24731
24732 let mut cx = EditorTestContext::new(cx).await;
24733 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24734 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24735
24736 // test `else` auto outdents when typed inside `if` block
24737 cx.set_state(indoc! {"
24738 def main():
24739 if i == 2:
24740 return
24741 ˇ
24742 "});
24743 cx.update_editor(|editor, window, cx| {
24744 editor.handle_input("else:", window, cx);
24745 });
24746 cx.assert_editor_state(indoc! {"
24747 def main():
24748 if i == 2:
24749 return
24750 else:ˇ
24751 "});
24752
24753 // test `except` auto outdents when typed inside `try` block
24754 cx.set_state(indoc! {"
24755 def main():
24756 try:
24757 i = 2
24758 ˇ
24759 "});
24760 cx.update_editor(|editor, window, cx| {
24761 editor.handle_input("except:", window, cx);
24762 });
24763 cx.assert_editor_state(indoc! {"
24764 def main():
24765 try:
24766 i = 2
24767 except:ˇ
24768 "});
24769
24770 // test `else` auto outdents when typed inside `except` block
24771 cx.set_state(indoc! {"
24772 def main():
24773 try:
24774 i = 2
24775 except:
24776 j = 2
24777 ˇ
24778 "});
24779 cx.update_editor(|editor, window, cx| {
24780 editor.handle_input("else:", window, cx);
24781 });
24782 cx.assert_editor_state(indoc! {"
24783 def main():
24784 try:
24785 i = 2
24786 except:
24787 j = 2
24788 else:ˇ
24789 "});
24790
24791 // test `finally` auto outdents when typed inside `else` block
24792 cx.set_state(indoc! {"
24793 def main():
24794 try:
24795 i = 2
24796 except:
24797 j = 2
24798 else:
24799 k = 2
24800 ˇ
24801 "});
24802 cx.update_editor(|editor, window, cx| {
24803 editor.handle_input("finally:", window, cx);
24804 });
24805 cx.assert_editor_state(indoc! {"
24806 def main():
24807 try:
24808 i = 2
24809 except:
24810 j = 2
24811 else:
24812 k = 2
24813 finally:ˇ
24814 "});
24815
24816 // test `else` does not outdents when typed inside `except` block right after for block
24817 cx.set_state(indoc! {"
24818 def main():
24819 try:
24820 i = 2
24821 except:
24822 for i in range(n):
24823 pass
24824 ˇ
24825 "});
24826 cx.update_editor(|editor, window, cx| {
24827 editor.handle_input("else:", window, cx);
24828 });
24829 cx.assert_editor_state(indoc! {"
24830 def main():
24831 try:
24832 i = 2
24833 except:
24834 for i in range(n):
24835 pass
24836 else:ˇ
24837 "});
24838
24839 // test `finally` auto outdents when typed inside `else` block right after for block
24840 cx.set_state(indoc! {"
24841 def main():
24842 try:
24843 i = 2
24844 except:
24845 j = 2
24846 else:
24847 for i in range(n):
24848 pass
24849 ˇ
24850 "});
24851 cx.update_editor(|editor, window, cx| {
24852 editor.handle_input("finally:", window, cx);
24853 });
24854 cx.assert_editor_state(indoc! {"
24855 def main():
24856 try:
24857 i = 2
24858 except:
24859 j = 2
24860 else:
24861 for i in range(n):
24862 pass
24863 finally:ˇ
24864 "});
24865
24866 // test `except` outdents to inner "try" block
24867 cx.set_state(indoc! {"
24868 def main():
24869 try:
24870 i = 2
24871 if i == 2:
24872 try:
24873 i = 3
24874 ˇ
24875 "});
24876 cx.update_editor(|editor, window, cx| {
24877 editor.handle_input("except:", window, cx);
24878 });
24879 cx.assert_editor_state(indoc! {"
24880 def main():
24881 try:
24882 i = 2
24883 if i == 2:
24884 try:
24885 i = 3
24886 except:ˇ
24887 "});
24888
24889 // test `except` outdents to outer "try" block
24890 cx.set_state(indoc! {"
24891 def main():
24892 try:
24893 i = 2
24894 if i == 2:
24895 try:
24896 i = 3
24897 ˇ
24898 "});
24899 cx.update_editor(|editor, window, cx| {
24900 editor.handle_input("except:", window, cx);
24901 });
24902 cx.assert_editor_state(indoc! {"
24903 def main():
24904 try:
24905 i = 2
24906 if i == 2:
24907 try:
24908 i = 3
24909 except:ˇ
24910 "});
24911
24912 // test `else` stays at correct indent when typed after `for` block
24913 cx.set_state(indoc! {"
24914 def main():
24915 for i in range(10):
24916 if i == 3:
24917 break
24918 ˇ
24919 "});
24920 cx.update_editor(|editor, window, cx| {
24921 editor.handle_input("else:", window, cx);
24922 });
24923 cx.assert_editor_state(indoc! {"
24924 def main():
24925 for i in range(10):
24926 if i == 3:
24927 break
24928 else:ˇ
24929 "});
24930
24931 // test does not outdent on typing after line with square brackets
24932 cx.set_state(indoc! {"
24933 def f() -> list[str]:
24934 ˇ
24935 "});
24936 cx.update_editor(|editor, window, cx| {
24937 editor.handle_input("a", window, cx);
24938 });
24939 cx.assert_editor_state(indoc! {"
24940 def f() -> list[str]:
24941 aˇ
24942 "});
24943
24944 // test does not outdent on typing : after case keyword
24945 cx.set_state(indoc! {"
24946 match 1:
24947 caseˇ
24948 "});
24949 cx.update_editor(|editor, window, cx| {
24950 editor.handle_input(":", window, cx);
24951 });
24952 cx.assert_editor_state(indoc! {"
24953 match 1:
24954 case:ˇ
24955 "});
24956}
24957
24958#[gpui::test]
24959async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24960 init_test(cx, |_| {});
24961 update_test_language_settings(cx, |settings| {
24962 settings.defaults.extend_comment_on_newline = Some(false);
24963 });
24964 let mut cx = EditorTestContext::new(cx).await;
24965 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24966 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24967
24968 // test correct indent after newline on comment
24969 cx.set_state(indoc! {"
24970 # COMMENT:ˇ
24971 "});
24972 cx.update_editor(|editor, window, cx| {
24973 editor.newline(&Newline, window, cx);
24974 });
24975 cx.assert_editor_state(indoc! {"
24976 # COMMENT:
24977 ˇ
24978 "});
24979
24980 // test correct indent after newline in brackets
24981 cx.set_state(indoc! {"
24982 {ˇ}
24983 "});
24984 cx.update_editor(|editor, window, cx| {
24985 editor.newline(&Newline, window, cx);
24986 });
24987 cx.run_until_parked();
24988 cx.assert_editor_state(indoc! {"
24989 {
24990 ˇ
24991 }
24992 "});
24993
24994 cx.set_state(indoc! {"
24995 (ˇ)
24996 "});
24997 cx.update_editor(|editor, window, cx| {
24998 editor.newline(&Newline, window, cx);
24999 });
25000 cx.run_until_parked();
25001 cx.assert_editor_state(indoc! {"
25002 (
25003 ˇ
25004 )
25005 "});
25006
25007 // do not indent after empty lists or dictionaries
25008 cx.set_state(indoc! {"
25009 a = []ˇ
25010 "});
25011 cx.update_editor(|editor, window, cx| {
25012 editor.newline(&Newline, window, cx);
25013 });
25014 cx.run_until_parked();
25015 cx.assert_editor_state(indoc! {"
25016 a = []
25017 ˇ
25018 "});
25019}
25020
25021#[gpui::test]
25022async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25023 init_test(cx, |_| {});
25024
25025 let mut cx = EditorTestContext::new(cx).await;
25026 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25027 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25028
25029 // test cursor move to start of each line on tab
25030 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25031 cx.set_state(indoc! {"
25032 function main() {
25033 ˇ for item in $items; do
25034 ˇ while [ -n \"$item\" ]; do
25035 ˇ if [ \"$value\" -gt 10 ]; then
25036 ˇ continue
25037 ˇ elif [ \"$value\" -lt 0 ]; then
25038 ˇ break
25039 ˇ else
25040 ˇ echo \"$item\"
25041 ˇ fi
25042 ˇ done
25043 ˇ done
25044 ˇ}
25045 "});
25046 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25047 cx.assert_editor_state(indoc! {"
25048 function main() {
25049 ˇfor item in $items; do
25050 ˇwhile [ -n \"$item\" ]; do
25051 ˇif [ \"$value\" -gt 10 ]; then
25052 ˇcontinue
25053 ˇelif [ \"$value\" -lt 0 ]; then
25054 ˇbreak
25055 ˇelse
25056 ˇecho \"$item\"
25057 ˇfi
25058 ˇdone
25059 ˇdone
25060 ˇ}
25061 "});
25062 // test relative indent is preserved when tab
25063 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25064 cx.assert_editor_state(indoc! {"
25065 function main() {
25066 ˇfor item in $items; do
25067 ˇwhile [ -n \"$item\" ]; do
25068 ˇif [ \"$value\" -gt 10 ]; then
25069 ˇcontinue
25070 ˇelif [ \"$value\" -lt 0 ]; then
25071 ˇbreak
25072 ˇelse
25073 ˇecho \"$item\"
25074 ˇfi
25075 ˇdone
25076 ˇdone
25077 ˇ}
25078 "});
25079
25080 // test cursor move to start of each line on tab
25081 // for `case` statement with patterns
25082 cx.set_state(indoc! {"
25083 function handle() {
25084 ˇ case \"$1\" in
25085 ˇ start)
25086 ˇ echo \"a\"
25087 ˇ ;;
25088 ˇ stop)
25089 ˇ echo \"b\"
25090 ˇ ;;
25091 ˇ *)
25092 ˇ echo \"c\"
25093 ˇ ;;
25094 ˇ esac
25095 ˇ}
25096 "});
25097 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25098 cx.assert_editor_state(indoc! {"
25099 function handle() {
25100 ˇcase \"$1\" in
25101 ˇstart)
25102 ˇecho \"a\"
25103 ˇ;;
25104 ˇstop)
25105 ˇecho \"b\"
25106 ˇ;;
25107 ˇ*)
25108 ˇecho \"c\"
25109 ˇ;;
25110 ˇesac
25111 ˇ}
25112 "});
25113}
25114
25115#[gpui::test]
25116async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25117 init_test(cx, |_| {});
25118
25119 let mut cx = EditorTestContext::new(cx).await;
25120 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25121 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25122
25123 // test indents on comment insert
25124 cx.set_state(indoc! {"
25125 function main() {
25126 ˇ for item in $items; do
25127 ˇ while [ -n \"$item\" ]; do
25128 ˇ if [ \"$value\" -gt 10 ]; then
25129 ˇ continue
25130 ˇ elif [ \"$value\" -lt 0 ]; then
25131 ˇ break
25132 ˇ else
25133 ˇ echo \"$item\"
25134 ˇ fi
25135 ˇ done
25136 ˇ done
25137 ˇ}
25138 "});
25139 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25140 cx.assert_editor_state(indoc! {"
25141 function main() {
25142 #ˇ for item in $items; do
25143 #ˇ while [ -n \"$item\" ]; do
25144 #ˇ if [ \"$value\" -gt 10 ]; then
25145 #ˇ continue
25146 #ˇ elif [ \"$value\" -lt 0 ]; then
25147 #ˇ break
25148 #ˇ else
25149 #ˇ echo \"$item\"
25150 #ˇ fi
25151 #ˇ done
25152 #ˇ done
25153 #ˇ}
25154 "});
25155}
25156
25157#[gpui::test]
25158async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25159 init_test(cx, |_| {});
25160
25161 let mut cx = EditorTestContext::new(cx).await;
25162 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25163 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25164
25165 // test `else` auto outdents when typed inside `if` block
25166 cx.set_state(indoc! {"
25167 if [ \"$1\" = \"test\" ]; then
25168 echo \"foo bar\"
25169 ˇ
25170 "});
25171 cx.update_editor(|editor, window, cx| {
25172 editor.handle_input("else", window, cx);
25173 });
25174 cx.assert_editor_state(indoc! {"
25175 if [ \"$1\" = \"test\" ]; then
25176 echo \"foo bar\"
25177 elseˇ
25178 "});
25179
25180 // test `elif` auto outdents when typed inside `if` block
25181 cx.set_state(indoc! {"
25182 if [ \"$1\" = \"test\" ]; then
25183 echo \"foo bar\"
25184 ˇ
25185 "});
25186 cx.update_editor(|editor, window, cx| {
25187 editor.handle_input("elif", window, cx);
25188 });
25189 cx.assert_editor_state(indoc! {"
25190 if [ \"$1\" = \"test\" ]; then
25191 echo \"foo bar\"
25192 elifˇ
25193 "});
25194
25195 // test `fi` auto outdents when typed inside `else` block
25196 cx.set_state(indoc! {"
25197 if [ \"$1\" = \"test\" ]; then
25198 echo \"foo bar\"
25199 else
25200 echo \"bar baz\"
25201 ˇ
25202 "});
25203 cx.update_editor(|editor, window, cx| {
25204 editor.handle_input("fi", window, cx);
25205 });
25206 cx.assert_editor_state(indoc! {"
25207 if [ \"$1\" = \"test\" ]; then
25208 echo \"foo bar\"
25209 else
25210 echo \"bar baz\"
25211 fiˇ
25212 "});
25213
25214 // test `done` auto outdents when typed inside `while` block
25215 cx.set_state(indoc! {"
25216 while read line; do
25217 echo \"$line\"
25218 ˇ
25219 "});
25220 cx.update_editor(|editor, window, cx| {
25221 editor.handle_input("done", window, cx);
25222 });
25223 cx.assert_editor_state(indoc! {"
25224 while read line; do
25225 echo \"$line\"
25226 doneˇ
25227 "});
25228
25229 // test `done` auto outdents when typed inside `for` block
25230 cx.set_state(indoc! {"
25231 for file in *.txt; do
25232 cat \"$file\"
25233 ˇ
25234 "});
25235 cx.update_editor(|editor, window, cx| {
25236 editor.handle_input("done", window, cx);
25237 });
25238 cx.assert_editor_state(indoc! {"
25239 for file in *.txt; do
25240 cat \"$file\"
25241 doneˇ
25242 "});
25243
25244 // test `esac` auto outdents when typed inside `case` block
25245 cx.set_state(indoc! {"
25246 case \"$1\" in
25247 start)
25248 echo \"foo bar\"
25249 ;;
25250 stop)
25251 echo \"bar baz\"
25252 ;;
25253 ˇ
25254 "});
25255 cx.update_editor(|editor, window, cx| {
25256 editor.handle_input("esac", window, cx);
25257 });
25258 cx.assert_editor_state(indoc! {"
25259 case \"$1\" in
25260 start)
25261 echo \"foo bar\"
25262 ;;
25263 stop)
25264 echo \"bar baz\"
25265 ;;
25266 esacˇ
25267 "});
25268
25269 // test `*)` auto outdents when typed inside `case` block
25270 cx.set_state(indoc! {"
25271 case \"$1\" in
25272 start)
25273 echo \"foo bar\"
25274 ;;
25275 ˇ
25276 "});
25277 cx.update_editor(|editor, window, cx| {
25278 editor.handle_input("*)", window, cx);
25279 });
25280 cx.assert_editor_state(indoc! {"
25281 case \"$1\" in
25282 start)
25283 echo \"foo bar\"
25284 ;;
25285 *)ˇ
25286 "});
25287
25288 // test `fi` outdents to correct level with nested if blocks
25289 cx.set_state(indoc! {"
25290 if [ \"$1\" = \"test\" ]; then
25291 echo \"outer if\"
25292 if [ \"$2\" = \"debug\" ]; then
25293 echo \"inner if\"
25294 ˇ
25295 "});
25296 cx.update_editor(|editor, window, cx| {
25297 editor.handle_input("fi", window, cx);
25298 });
25299 cx.assert_editor_state(indoc! {"
25300 if [ \"$1\" = \"test\" ]; then
25301 echo \"outer if\"
25302 if [ \"$2\" = \"debug\" ]; then
25303 echo \"inner if\"
25304 fiˇ
25305 "});
25306}
25307
25308#[gpui::test]
25309async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25310 init_test(cx, |_| {});
25311 update_test_language_settings(cx, |settings| {
25312 settings.defaults.extend_comment_on_newline = Some(false);
25313 });
25314 let mut cx = EditorTestContext::new(cx).await;
25315 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25316 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25317
25318 // test correct indent after newline on comment
25319 cx.set_state(indoc! {"
25320 # COMMENT:ˇ
25321 "});
25322 cx.update_editor(|editor, window, cx| {
25323 editor.newline(&Newline, window, cx);
25324 });
25325 cx.assert_editor_state(indoc! {"
25326 # COMMENT:
25327 ˇ
25328 "});
25329
25330 // test correct indent after newline after `then`
25331 cx.set_state(indoc! {"
25332
25333 if [ \"$1\" = \"test\" ]; thenˇ
25334 "});
25335 cx.update_editor(|editor, window, cx| {
25336 editor.newline(&Newline, window, cx);
25337 });
25338 cx.run_until_parked();
25339 cx.assert_editor_state(indoc! {"
25340
25341 if [ \"$1\" = \"test\" ]; then
25342 ˇ
25343 "});
25344
25345 // test correct indent after newline after `else`
25346 cx.set_state(indoc! {"
25347 if [ \"$1\" = \"test\" ]; then
25348 elseˇ
25349 "});
25350 cx.update_editor(|editor, window, cx| {
25351 editor.newline(&Newline, window, cx);
25352 });
25353 cx.run_until_parked();
25354 cx.assert_editor_state(indoc! {"
25355 if [ \"$1\" = \"test\" ]; then
25356 else
25357 ˇ
25358 "});
25359
25360 // test correct indent after newline after `elif`
25361 cx.set_state(indoc! {"
25362 if [ \"$1\" = \"test\" ]; then
25363 elifˇ
25364 "});
25365 cx.update_editor(|editor, window, cx| {
25366 editor.newline(&Newline, window, cx);
25367 });
25368 cx.run_until_parked();
25369 cx.assert_editor_state(indoc! {"
25370 if [ \"$1\" = \"test\" ]; then
25371 elif
25372 ˇ
25373 "});
25374
25375 // test correct indent after newline after `do`
25376 cx.set_state(indoc! {"
25377 for file in *.txt; doˇ
25378 "});
25379 cx.update_editor(|editor, window, cx| {
25380 editor.newline(&Newline, window, cx);
25381 });
25382 cx.run_until_parked();
25383 cx.assert_editor_state(indoc! {"
25384 for file in *.txt; do
25385 ˇ
25386 "});
25387
25388 // test correct indent after newline after case pattern
25389 cx.set_state(indoc! {"
25390 case \"$1\" in
25391 start)ˇ
25392 "});
25393 cx.update_editor(|editor, window, cx| {
25394 editor.newline(&Newline, window, cx);
25395 });
25396 cx.run_until_parked();
25397 cx.assert_editor_state(indoc! {"
25398 case \"$1\" in
25399 start)
25400 ˇ
25401 "});
25402
25403 // test correct indent after newline after case pattern
25404 cx.set_state(indoc! {"
25405 case \"$1\" in
25406 start)
25407 ;;
25408 *)ˇ
25409 "});
25410 cx.update_editor(|editor, window, cx| {
25411 editor.newline(&Newline, window, cx);
25412 });
25413 cx.run_until_parked();
25414 cx.assert_editor_state(indoc! {"
25415 case \"$1\" in
25416 start)
25417 ;;
25418 *)
25419 ˇ
25420 "});
25421
25422 // test correct indent after newline after function opening brace
25423 cx.set_state(indoc! {"
25424 function test() {ˇ}
25425 "});
25426 cx.update_editor(|editor, window, cx| {
25427 editor.newline(&Newline, window, cx);
25428 });
25429 cx.run_until_parked();
25430 cx.assert_editor_state(indoc! {"
25431 function test() {
25432 ˇ
25433 }
25434 "});
25435
25436 // test no extra indent after semicolon on same line
25437 cx.set_state(indoc! {"
25438 echo \"test\";ˇ
25439 "});
25440 cx.update_editor(|editor, window, cx| {
25441 editor.newline(&Newline, window, cx);
25442 });
25443 cx.run_until_parked();
25444 cx.assert_editor_state(indoc! {"
25445 echo \"test\";
25446 ˇ
25447 "});
25448}
25449
25450fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25451 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25452 point..point
25453}
25454
25455#[track_caller]
25456fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25457 let (text, ranges) = marked_text_ranges(marked_text, true);
25458 assert_eq!(editor.text(cx), text);
25459 assert_eq!(
25460 editor.selections.ranges(&editor.display_snapshot(cx)),
25461 ranges,
25462 "Assert selections are {}",
25463 marked_text
25464 );
25465}
25466
25467pub fn handle_signature_help_request(
25468 cx: &mut EditorLspTestContext,
25469 mocked_response: lsp::SignatureHelp,
25470) -> impl Future<Output = ()> + use<> {
25471 let mut request =
25472 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25473 let mocked_response = mocked_response.clone();
25474 async move { Ok(Some(mocked_response)) }
25475 });
25476
25477 async move {
25478 request.next().await;
25479 }
25480}
25481
25482#[track_caller]
25483pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25484 cx.update_editor(|editor, _, _| {
25485 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25486 let entries = menu.entries.borrow();
25487 let entries = entries
25488 .iter()
25489 .map(|entry| entry.string.as_str())
25490 .collect::<Vec<_>>();
25491 assert_eq!(entries, expected);
25492 } else {
25493 panic!("Expected completions menu");
25494 }
25495 });
25496}
25497
25498/// Handle completion request passing a marked string specifying where the completion
25499/// should be triggered from using '|' character, what range should be replaced, and what completions
25500/// should be returned using '<' and '>' to delimit the range.
25501///
25502/// Also see `handle_completion_request_with_insert_and_replace`.
25503#[track_caller]
25504pub fn handle_completion_request(
25505 marked_string: &str,
25506 completions: Vec<&'static str>,
25507 is_incomplete: bool,
25508 counter: Arc<AtomicUsize>,
25509 cx: &mut EditorLspTestContext,
25510) -> impl Future<Output = ()> {
25511 let complete_from_marker: TextRangeMarker = '|'.into();
25512 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25513 let (_, mut marked_ranges) = marked_text_ranges_by(
25514 marked_string,
25515 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25516 );
25517
25518 let complete_from_position =
25519 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25520 let replace_range =
25521 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25522
25523 let mut request =
25524 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25525 let completions = completions.clone();
25526 counter.fetch_add(1, atomic::Ordering::Release);
25527 async move {
25528 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25529 assert_eq!(
25530 params.text_document_position.position,
25531 complete_from_position
25532 );
25533 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25534 is_incomplete,
25535 item_defaults: None,
25536 items: completions
25537 .iter()
25538 .map(|completion_text| lsp::CompletionItem {
25539 label: completion_text.to_string(),
25540 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25541 range: replace_range,
25542 new_text: completion_text.to_string(),
25543 })),
25544 ..Default::default()
25545 })
25546 .collect(),
25547 })))
25548 }
25549 });
25550
25551 async move {
25552 request.next().await;
25553 }
25554}
25555
25556/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25557/// given instead, which also contains an `insert` range.
25558///
25559/// This function uses markers to define ranges:
25560/// - `|` marks the cursor position
25561/// - `<>` marks the replace range
25562/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25563pub fn handle_completion_request_with_insert_and_replace(
25564 cx: &mut EditorLspTestContext,
25565 marked_string: &str,
25566 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25567 counter: Arc<AtomicUsize>,
25568) -> impl Future<Output = ()> {
25569 let complete_from_marker: TextRangeMarker = '|'.into();
25570 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25571 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25572
25573 let (_, mut marked_ranges) = marked_text_ranges_by(
25574 marked_string,
25575 vec![
25576 complete_from_marker.clone(),
25577 replace_range_marker.clone(),
25578 insert_range_marker.clone(),
25579 ],
25580 );
25581
25582 let complete_from_position =
25583 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25584 let replace_range =
25585 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25586
25587 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25588 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25589 _ => lsp::Range {
25590 start: replace_range.start,
25591 end: complete_from_position,
25592 },
25593 };
25594
25595 let mut request =
25596 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25597 let completions = completions.clone();
25598 counter.fetch_add(1, atomic::Ordering::Release);
25599 async move {
25600 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25601 assert_eq!(
25602 params.text_document_position.position, complete_from_position,
25603 "marker `|` position doesn't match",
25604 );
25605 Ok(Some(lsp::CompletionResponse::Array(
25606 completions
25607 .iter()
25608 .map(|(label, new_text)| lsp::CompletionItem {
25609 label: label.to_string(),
25610 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25611 lsp::InsertReplaceEdit {
25612 insert: insert_range,
25613 replace: replace_range,
25614 new_text: new_text.to_string(),
25615 },
25616 )),
25617 ..Default::default()
25618 })
25619 .collect(),
25620 )))
25621 }
25622 });
25623
25624 async move {
25625 request.next().await;
25626 }
25627}
25628
25629fn handle_resolve_completion_request(
25630 cx: &mut EditorLspTestContext,
25631 edits: Option<Vec<(&'static str, &'static str)>>,
25632) -> impl Future<Output = ()> {
25633 let edits = edits.map(|edits| {
25634 edits
25635 .iter()
25636 .map(|(marked_string, new_text)| {
25637 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25638 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25639 lsp::TextEdit::new(replace_range, new_text.to_string())
25640 })
25641 .collect::<Vec<_>>()
25642 });
25643
25644 let mut request =
25645 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25646 let edits = edits.clone();
25647 async move {
25648 Ok(lsp::CompletionItem {
25649 additional_text_edits: edits,
25650 ..Default::default()
25651 })
25652 }
25653 });
25654
25655 async move {
25656 request.next().await;
25657 }
25658}
25659
25660pub(crate) fn update_test_language_settings(
25661 cx: &mut TestAppContext,
25662 f: impl Fn(&mut AllLanguageSettingsContent),
25663) {
25664 cx.update(|cx| {
25665 SettingsStore::update_global(cx, |store, cx| {
25666 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25667 });
25668 });
25669}
25670
25671pub(crate) fn update_test_project_settings(
25672 cx: &mut TestAppContext,
25673 f: impl Fn(&mut ProjectSettingsContent),
25674) {
25675 cx.update(|cx| {
25676 SettingsStore::update_global(cx, |store, cx| {
25677 store.update_user_settings(cx, |settings| f(&mut settings.project));
25678 });
25679 });
25680}
25681
25682pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25683 cx.update(|cx| {
25684 assets::Assets.load_test_fonts(cx);
25685 let store = SettingsStore::test(cx);
25686 cx.set_global(store);
25687 theme::init(theme::LoadThemes::JustBase, cx);
25688 release_channel::init(SemanticVersion::default(), cx);
25689 client::init_settings(cx);
25690 language::init(cx);
25691 Project::init_settings(cx);
25692 workspace::init_settings(cx);
25693 crate::init(cx);
25694 });
25695 zlog::init_test();
25696 update_test_language_settings(cx, f);
25697}
25698
25699#[track_caller]
25700fn assert_hunk_revert(
25701 not_reverted_text_with_selections: &str,
25702 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25703 expected_reverted_text_with_selections: &str,
25704 base_text: &str,
25705 cx: &mut EditorLspTestContext,
25706) {
25707 cx.set_state(not_reverted_text_with_selections);
25708 cx.set_head_text(base_text);
25709 cx.executor().run_until_parked();
25710
25711 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25712 let snapshot = editor.snapshot(window, cx);
25713 let reverted_hunk_statuses = snapshot
25714 .buffer_snapshot()
25715 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25716 .map(|hunk| hunk.status().kind)
25717 .collect::<Vec<_>>();
25718
25719 editor.git_restore(&Default::default(), window, cx);
25720 reverted_hunk_statuses
25721 });
25722 cx.executor().run_until_parked();
25723 cx.assert_editor_state(expected_reverted_text_with_selections);
25724 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25725}
25726
25727#[gpui::test(iterations = 10)]
25728async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25729 init_test(cx, |_| {});
25730
25731 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25732 let counter = diagnostic_requests.clone();
25733
25734 let fs = FakeFs::new(cx.executor());
25735 fs.insert_tree(
25736 path!("/a"),
25737 json!({
25738 "first.rs": "fn main() { let a = 5; }",
25739 "second.rs": "// Test file",
25740 }),
25741 )
25742 .await;
25743
25744 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25745 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25746 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25747
25748 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25749 language_registry.add(rust_lang());
25750 let mut fake_servers = language_registry.register_fake_lsp(
25751 "Rust",
25752 FakeLspAdapter {
25753 capabilities: lsp::ServerCapabilities {
25754 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25755 lsp::DiagnosticOptions {
25756 identifier: None,
25757 inter_file_dependencies: true,
25758 workspace_diagnostics: true,
25759 work_done_progress_options: Default::default(),
25760 },
25761 )),
25762 ..Default::default()
25763 },
25764 ..Default::default()
25765 },
25766 );
25767
25768 let editor = workspace
25769 .update(cx, |workspace, window, cx| {
25770 workspace.open_abs_path(
25771 PathBuf::from(path!("/a/first.rs")),
25772 OpenOptions::default(),
25773 window,
25774 cx,
25775 )
25776 })
25777 .unwrap()
25778 .await
25779 .unwrap()
25780 .downcast::<Editor>()
25781 .unwrap();
25782 let fake_server = fake_servers.next().await.unwrap();
25783 let server_id = fake_server.server.server_id();
25784 let mut first_request = fake_server
25785 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25786 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25787 let result_id = Some(new_result_id.to_string());
25788 assert_eq!(
25789 params.text_document.uri,
25790 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25791 );
25792 async move {
25793 Ok(lsp::DocumentDiagnosticReportResult::Report(
25794 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25795 related_documents: None,
25796 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25797 items: Vec::new(),
25798 result_id,
25799 },
25800 }),
25801 ))
25802 }
25803 });
25804
25805 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25806 project.update(cx, |project, cx| {
25807 let buffer_id = editor
25808 .read(cx)
25809 .buffer()
25810 .read(cx)
25811 .as_singleton()
25812 .expect("created a singleton buffer")
25813 .read(cx)
25814 .remote_id();
25815 let buffer_result_id = project
25816 .lsp_store()
25817 .read(cx)
25818 .result_id(server_id, buffer_id, cx);
25819 assert_eq!(expected, buffer_result_id);
25820 });
25821 };
25822
25823 ensure_result_id(None, cx);
25824 cx.executor().advance_clock(Duration::from_millis(60));
25825 cx.executor().run_until_parked();
25826 assert_eq!(
25827 diagnostic_requests.load(atomic::Ordering::Acquire),
25828 1,
25829 "Opening file should trigger diagnostic request"
25830 );
25831 first_request
25832 .next()
25833 .await
25834 .expect("should have sent the first diagnostics pull request");
25835 ensure_result_id(Some("1".to_string()), cx);
25836
25837 // Editing should trigger diagnostics
25838 editor.update_in(cx, |editor, window, cx| {
25839 editor.handle_input("2", window, cx)
25840 });
25841 cx.executor().advance_clock(Duration::from_millis(60));
25842 cx.executor().run_until_parked();
25843 assert_eq!(
25844 diagnostic_requests.load(atomic::Ordering::Acquire),
25845 2,
25846 "Editing should trigger diagnostic request"
25847 );
25848 ensure_result_id(Some("2".to_string()), cx);
25849
25850 // Moving cursor should not trigger diagnostic request
25851 editor.update_in(cx, |editor, window, cx| {
25852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25853 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25854 });
25855 });
25856 cx.executor().advance_clock(Duration::from_millis(60));
25857 cx.executor().run_until_parked();
25858 assert_eq!(
25859 diagnostic_requests.load(atomic::Ordering::Acquire),
25860 2,
25861 "Cursor movement should not trigger diagnostic request"
25862 );
25863 ensure_result_id(Some("2".to_string()), cx);
25864 // Multiple rapid edits should be debounced
25865 for _ in 0..5 {
25866 editor.update_in(cx, |editor, window, cx| {
25867 editor.handle_input("x", window, cx)
25868 });
25869 }
25870 cx.executor().advance_clock(Duration::from_millis(60));
25871 cx.executor().run_until_parked();
25872
25873 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25874 assert!(
25875 final_requests <= 4,
25876 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25877 );
25878 ensure_result_id(Some(final_requests.to_string()), cx);
25879}
25880
25881#[gpui::test]
25882async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25883 // Regression test for issue #11671
25884 // Previously, adding a cursor after moving multiple cursors would reset
25885 // the cursor count instead of adding to the existing cursors.
25886 init_test(cx, |_| {});
25887 let mut cx = EditorTestContext::new(cx).await;
25888
25889 // Create a simple buffer with cursor at start
25890 cx.set_state(indoc! {"
25891 ˇaaaa
25892 bbbb
25893 cccc
25894 dddd
25895 eeee
25896 ffff
25897 gggg
25898 hhhh"});
25899
25900 // Add 2 cursors below (so we have 3 total)
25901 cx.update_editor(|editor, window, cx| {
25902 editor.add_selection_below(&Default::default(), window, cx);
25903 editor.add_selection_below(&Default::default(), window, cx);
25904 });
25905
25906 // Verify we have 3 cursors
25907 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25908 assert_eq!(
25909 initial_count, 3,
25910 "Should have 3 cursors after adding 2 below"
25911 );
25912
25913 // Move down one line
25914 cx.update_editor(|editor, window, cx| {
25915 editor.move_down(&MoveDown, window, cx);
25916 });
25917
25918 // Add another cursor below
25919 cx.update_editor(|editor, window, cx| {
25920 editor.add_selection_below(&Default::default(), window, cx);
25921 });
25922
25923 // Should now have 4 cursors (3 original + 1 new)
25924 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25925 assert_eq!(
25926 final_count, 4,
25927 "Should have 4 cursors after moving and adding another"
25928 );
25929}
25930
25931#[gpui::test]
25932async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25933 init_test(cx, |_| {});
25934
25935 let mut cx = EditorTestContext::new(cx).await;
25936
25937 cx.set_state(indoc!(
25938 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25939 Second line here"#
25940 ));
25941
25942 cx.update_editor(|editor, window, cx| {
25943 // Enable soft wrapping with a narrow width to force soft wrapping and
25944 // confirm that more than 2 rows are being displayed.
25945 editor.set_wrap_width(Some(100.0.into()), cx);
25946 assert!(editor.display_text(cx).lines().count() > 2);
25947
25948 editor.add_selection_below(
25949 &AddSelectionBelow {
25950 skip_soft_wrap: true,
25951 },
25952 window,
25953 cx,
25954 );
25955
25956 assert_eq!(
25957 editor.selections.display_ranges(cx),
25958 &[
25959 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25960 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25961 ]
25962 );
25963
25964 editor.add_selection_above(
25965 &AddSelectionAbove {
25966 skip_soft_wrap: true,
25967 },
25968 window,
25969 cx,
25970 );
25971
25972 assert_eq!(
25973 editor.selections.display_ranges(cx),
25974 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25975 );
25976
25977 editor.add_selection_below(
25978 &AddSelectionBelow {
25979 skip_soft_wrap: false,
25980 },
25981 window,
25982 cx,
25983 );
25984
25985 assert_eq!(
25986 editor.selections.display_ranges(cx),
25987 &[
25988 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25989 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25990 ]
25991 );
25992
25993 editor.add_selection_above(
25994 &AddSelectionAbove {
25995 skip_soft_wrap: false,
25996 },
25997 window,
25998 cx,
25999 );
26000
26001 assert_eq!(
26002 editor.selections.display_ranges(cx),
26003 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26004 );
26005 });
26006}
26007
26008#[gpui::test(iterations = 10)]
26009async fn test_document_colors(cx: &mut TestAppContext) {
26010 let expected_color = Rgba {
26011 r: 0.33,
26012 g: 0.33,
26013 b: 0.33,
26014 a: 0.33,
26015 };
26016
26017 init_test(cx, |_| {});
26018
26019 let fs = FakeFs::new(cx.executor());
26020 fs.insert_tree(
26021 path!("/a"),
26022 json!({
26023 "first.rs": "fn main() { let a = 5; }",
26024 }),
26025 )
26026 .await;
26027
26028 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26029 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26030 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26031
26032 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26033 language_registry.add(rust_lang());
26034 let mut fake_servers = language_registry.register_fake_lsp(
26035 "Rust",
26036 FakeLspAdapter {
26037 capabilities: lsp::ServerCapabilities {
26038 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26039 ..lsp::ServerCapabilities::default()
26040 },
26041 name: "rust-analyzer",
26042 ..FakeLspAdapter::default()
26043 },
26044 );
26045 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26046 "Rust",
26047 FakeLspAdapter {
26048 capabilities: lsp::ServerCapabilities {
26049 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26050 ..lsp::ServerCapabilities::default()
26051 },
26052 name: "not-rust-analyzer",
26053 ..FakeLspAdapter::default()
26054 },
26055 );
26056
26057 let editor = workspace
26058 .update(cx, |workspace, window, cx| {
26059 workspace.open_abs_path(
26060 PathBuf::from(path!("/a/first.rs")),
26061 OpenOptions::default(),
26062 window,
26063 cx,
26064 )
26065 })
26066 .unwrap()
26067 .await
26068 .unwrap()
26069 .downcast::<Editor>()
26070 .unwrap();
26071 let fake_language_server = fake_servers.next().await.unwrap();
26072 let fake_language_server_without_capabilities =
26073 fake_servers_without_capabilities.next().await.unwrap();
26074 let requests_made = Arc::new(AtomicUsize::new(0));
26075 let closure_requests_made = Arc::clone(&requests_made);
26076 let mut color_request_handle = fake_language_server
26077 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26078 let requests_made = Arc::clone(&closure_requests_made);
26079 async move {
26080 assert_eq!(
26081 params.text_document.uri,
26082 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26083 );
26084 requests_made.fetch_add(1, atomic::Ordering::Release);
26085 Ok(vec![
26086 lsp::ColorInformation {
26087 range: lsp::Range {
26088 start: lsp::Position {
26089 line: 0,
26090 character: 0,
26091 },
26092 end: lsp::Position {
26093 line: 0,
26094 character: 1,
26095 },
26096 },
26097 color: lsp::Color {
26098 red: 0.33,
26099 green: 0.33,
26100 blue: 0.33,
26101 alpha: 0.33,
26102 },
26103 },
26104 lsp::ColorInformation {
26105 range: lsp::Range {
26106 start: lsp::Position {
26107 line: 0,
26108 character: 0,
26109 },
26110 end: lsp::Position {
26111 line: 0,
26112 character: 1,
26113 },
26114 },
26115 color: lsp::Color {
26116 red: 0.33,
26117 green: 0.33,
26118 blue: 0.33,
26119 alpha: 0.33,
26120 },
26121 },
26122 ])
26123 }
26124 });
26125
26126 let _handle = fake_language_server_without_capabilities
26127 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26128 panic!("Should not be called");
26129 });
26130 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26131 color_request_handle.next().await.unwrap();
26132 cx.run_until_parked();
26133 assert_eq!(
26134 1,
26135 requests_made.load(atomic::Ordering::Acquire),
26136 "Should query for colors once per editor open"
26137 );
26138 editor.update_in(cx, |editor, _, cx| {
26139 assert_eq!(
26140 vec![expected_color],
26141 extract_color_inlays(editor, cx),
26142 "Should have an initial inlay"
26143 );
26144 });
26145
26146 // opening another file in a split should not influence the LSP query counter
26147 workspace
26148 .update(cx, |workspace, window, cx| {
26149 assert_eq!(
26150 workspace.panes().len(),
26151 1,
26152 "Should have one pane with one editor"
26153 );
26154 workspace.move_item_to_pane_in_direction(
26155 &MoveItemToPaneInDirection {
26156 direction: SplitDirection::Right,
26157 focus: false,
26158 clone: true,
26159 },
26160 window,
26161 cx,
26162 );
26163 })
26164 .unwrap();
26165 cx.run_until_parked();
26166 workspace
26167 .update(cx, |workspace, _, cx| {
26168 let panes = workspace.panes();
26169 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26170 for pane in panes {
26171 let editor = pane
26172 .read(cx)
26173 .active_item()
26174 .and_then(|item| item.downcast::<Editor>())
26175 .expect("Should have opened an editor in each split");
26176 let editor_file = editor
26177 .read(cx)
26178 .buffer()
26179 .read(cx)
26180 .as_singleton()
26181 .expect("test deals with singleton buffers")
26182 .read(cx)
26183 .file()
26184 .expect("test buffese should have a file")
26185 .path();
26186 assert_eq!(
26187 editor_file.as_ref(),
26188 rel_path("first.rs"),
26189 "Both editors should be opened for the same file"
26190 )
26191 }
26192 })
26193 .unwrap();
26194
26195 cx.executor().advance_clock(Duration::from_millis(500));
26196 let save = editor.update_in(cx, |editor, window, cx| {
26197 editor.move_to_end(&MoveToEnd, window, cx);
26198 editor.handle_input("dirty", window, cx);
26199 editor.save(
26200 SaveOptions {
26201 format: true,
26202 autosave: true,
26203 },
26204 project.clone(),
26205 window,
26206 cx,
26207 )
26208 });
26209 save.await.unwrap();
26210
26211 color_request_handle.next().await.unwrap();
26212 cx.run_until_parked();
26213 assert_eq!(
26214 2,
26215 requests_made.load(atomic::Ordering::Acquire),
26216 "Should query for colors once per save (deduplicated) and once per formatting after save"
26217 );
26218
26219 drop(editor);
26220 let close = workspace
26221 .update(cx, |workspace, window, cx| {
26222 workspace.active_pane().update(cx, |pane, cx| {
26223 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26224 })
26225 })
26226 .unwrap();
26227 close.await.unwrap();
26228 let close = workspace
26229 .update(cx, |workspace, window, cx| {
26230 workspace.active_pane().update(cx, |pane, cx| {
26231 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26232 })
26233 })
26234 .unwrap();
26235 close.await.unwrap();
26236 assert_eq!(
26237 2,
26238 requests_made.load(atomic::Ordering::Acquire),
26239 "After saving and closing all editors, no extra requests should be made"
26240 );
26241 workspace
26242 .update(cx, |workspace, _, cx| {
26243 assert!(
26244 workspace.active_item(cx).is_none(),
26245 "Should close all editors"
26246 )
26247 })
26248 .unwrap();
26249
26250 workspace
26251 .update(cx, |workspace, window, cx| {
26252 workspace.active_pane().update(cx, |pane, cx| {
26253 pane.navigate_backward(&workspace::GoBack, window, cx);
26254 })
26255 })
26256 .unwrap();
26257 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26258 cx.run_until_parked();
26259 let editor = workspace
26260 .update(cx, |workspace, _, cx| {
26261 workspace
26262 .active_item(cx)
26263 .expect("Should have reopened the editor again after navigating back")
26264 .downcast::<Editor>()
26265 .expect("Should be an editor")
26266 })
26267 .unwrap();
26268
26269 assert_eq!(
26270 2,
26271 requests_made.load(atomic::Ordering::Acquire),
26272 "Cache should be reused on buffer close and reopen"
26273 );
26274 editor.update(cx, |editor, cx| {
26275 assert_eq!(
26276 vec![expected_color],
26277 extract_color_inlays(editor, cx),
26278 "Should have an initial inlay"
26279 );
26280 });
26281
26282 drop(color_request_handle);
26283 let closure_requests_made = Arc::clone(&requests_made);
26284 let mut empty_color_request_handle = fake_language_server
26285 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26286 let requests_made = Arc::clone(&closure_requests_made);
26287 async move {
26288 assert_eq!(
26289 params.text_document.uri,
26290 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26291 );
26292 requests_made.fetch_add(1, atomic::Ordering::Release);
26293 Ok(Vec::new())
26294 }
26295 });
26296 let save = editor.update_in(cx, |editor, window, cx| {
26297 editor.move_to_end(&MoveToEnd, window, cx);
26298 editor.handle_input("dirty_again", window, cx);
26299 editor.save(
26300 SaveOptions {
26301 format: false,
26302 autosave: true,
26303 },
26304 project.clone(),
26305 window,
26306 cx,
26307 )
26308 });
26309 save.await.unwrap();
26310
26311 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26312 empty_color_request_handle.next().await.unwrap();
26313 cx.run_until_parked();
26314 assert_eq!(
26315 3,
26316 requests_made.load(atomic::Ordering::Acquire),
26317 "Should query for colors once per save only, as formatting was not requested"
26318 );
26319 editor.update(cx, |editor, cx| {
26320 assert_eq!(
26321 Vec::<Rgba>::new(),
26322 extract_color_inlays(editor, cx),
26323 "Should clear all colors when the server returns an empty response"
26324 );
26325 });
26326}
26327
26328#[gpui::test]
26329async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26330 init_test(cx, |_| {});
26331 let (editor, cx) = cx.add_window_view(Editor::single_line);
26332 editor.update_in(cx, |editor, window, cx| {
26333 editor.set_text("oops\n\nwow\n", window, cx)
26334 });
26335 cx.run_until_parked();
26336 editor.update(cx, |editor, cx| {
26337 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26338 });
26339 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26340 cx.run_until_parked();
26341 editor.update(cx, |editor, cx| {
26342 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26343 });
26344}
26345
26346#[gpui::test]
26347async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26348 init_test(cx, |_| {});
26349
26350 cx.update(|cx| {
26351 register_project_item::<Editor>(cx);
26352 });
26353
26354 let fs = FakeFs::new(cx.executor());
26355 fs.insert_tree("/root1", json!({})).await;
26356 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26357 .await;
26358
26359 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26360 let (workspace, cx) =
26361 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26362
26363 let worktree_id = project.update(cx, |project, cx| {
26364 project.worktrees(cx).next().unwrap().read(cx).id()
26365 });
26366
26367 let handle = workspace
26368 .update_in(cx, |workspace, window, cx| {
26369 let project_path = (worktree_id, rel_path("one.pdf"));
26370 workspace.open_path(project_path, None, true, window, cx)
26371 })
26372 .await
26373 .unwrap();
26374
26375 assert_eq!(
26376 handle.to_any().entity_type(),
26377 TypeId::of::<InvalidItemView>()
26378 );
26379}
26380
26381#[gpui::test]
26382async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26383 init_test(cx, |_| {});
26384
26385 let language = Arc::new(Language::new(
26386 LanguageConfig::default(),
26387 Some(tree_sitter_rust::LANGUAGE.into()),
26388 ));
26389
26390 // Test hierarchical sibling navigation
26391 let text = r#"
26392 fn outer() {
26393 if condition {
26394 let a = 1;
26395 }
26396 let b = 2;
26397 }
26398
26399 fn another() {
26400 let c = 3;
26401 }
26402 "#;
26403
26404 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26405 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26406 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26407
26408 // Wait for parsing to complete
26409 editor
26410 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26411 .await;
26412
26413 editor.update_in(cx, |editor, window, cx| {
26414 // Start by selecting "let a = 1;" inside the if block
26415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26416 s.select_display_ranges([
26417 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26418 ]);
26419 });
26420
26421 let initial_selection = editor.selections.display_ranges(cx);
26422 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26423
26424 // Test select next sibling - should move up levels to find the next sibling
26425 // Since "let a = 1;" has no siblings in the if block, it should move up
26426 // to find "let b = 2;" which is a sibling of the if block
26427 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26428 let next_selection = editor.selections.display_ranges(cx);
26429
26430 // Should have a selection and it should be different from the initial
26431 assert_eq!(
26432 next_selection.len(),
26433 1,
26434 "Should have one selection after next"
26435 );
26436 assert_ne!(
26437 next_selection[0], initial_selection[0],
26438 "Next sibling selection should be different"
26439 );
26440
26441 // Test hierarchical navigation by going to the end of the current function
26442 // and trying to navigate to the next function
26443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26444 s.select_display_ranges([
26445 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26446 ]);
26447 });
26448
26449 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26450 let function_next_selection = editor.selections.display_ranges(cx);
26451
26452 // Should move to the next function
26453 assert_eq!(
26454 function_next_selection.len(),
26455 1,
26456 "Should have one selection after function next"
26457 );
26458
26459 // Test select previous sibling navigation
26460 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26461 let prev_selection = editor.selections.display_ranges(cx);
26462
26463 // Should have a selection and it should be different
26464 assert_eq!(
26465 prev_selection.len(),
26466 1,
26467 "Should have one selection after prev"
26468 );
26469 assert_ne!(
26470 prev_selection[0], function_next_selection[0],
26471 "Previous sibling selection should be different from next"
26472 );
26473 });
26474}
26475
26476#[gpui::test]
26477async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26478 init_test(cx, |_| {});
26479
26480 let mut cx = EditorTestContext::new(cx).await;
26481 cx.set_state(
26482 "let ˇvariable = 42;
26483let another = variable + 1;
26484let result = variable * 2;",
26485 );
26486
26487 // Set up document highlights manually (simulating LSP response)
26488 cx.update_editor(|editor, _window, cx| {
26489 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26490
26491 // Create highlights for "variable" occurrences
26492 let highlight_ranges = [
26493 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26494 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26495 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26496 ];
26497
26498 let anchor_ranges: Vec<_> = highlight_ranges
26499 .iter()
26500 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26501 .collect();
26502
26503 editor.highlight_background::<DocumentHighlightRead>(
26504 &anchor_ranges,
26505 |theme| theme.colors().editor_document_highlight_read_background,
26506 cx,
26507 );
26508 });
26509
26510 // Go to next highlight - should move to second "variable"
26511 cx.update_editor(|editor, window, cx| {
26512 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26513 });
26514 cx.assert_editor_state(
26515 "let variable = 42;
26516let another = ˇvariable + 1;
26517let result = variable * 2;",
26518 );
26519
26520 // Go to next highlight - should move to third "variable"
26521 cx.update_editor(|editor, window, cx| {
26522 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26523 });
26524 cx.assert_editor_state(
26525 "let variable = 42;
26526let another = variable + 1;
26527let result = ˇvariable * 2;",
26528 );
26529
26530 // Go to next highlight - should stay at third "variable" (no wrap-around)
26531 cx.update_editor(|editor, window, cx| {
26532 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26533 });
26534 cx.assert_editor_state(
26535 "let variable = 42;
26536let another = variable + 1;
26537let result = ˇvariable * 2;",
26538 );
26539
26540 // Now test going backwards from third position
26541 cx.update_editor(|editor, window, cx| {
26542 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26543 });
26544 cx.assert_editor_state(
26545 "let variable = 42;
26546let another = ˇvariable + 1;
26547let result = variable * 2;",
26548 );
26549
26550 // Go to previous highlight - should move to first "variable"
26551 cx.update_editor(|editor, window, cx| {
26552 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26553 });
26554 cx.assert_editor_state(
26555 "let ˇvariable = 42;
26556let another = variable + 1;
26557let result = variable * 2;",
26558 );
26559
26560 // Go to previous highlight - should stay on first "variable"
26561 cx.update_editor(|editor, window, cx| {
26562 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26563 });
26564 cx.assert_editor_state(
26565 "let ˇvariable = 42;
26566let another = variable + 1;
26567let result = variable * 2;",
26568 );
26569}
26570
26571#[gpui::test]
26572async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26573 cx: &mut gpui::TestAppContext,
26574) {
26575 init_test(cx, |_| {});
26576
26577 let url = "https://zed.dev";
26578
26579 let markdown_language = Arc::new(Language::new(
26580 LanguageConfig {
26581 name: "Markdown".into(),
26582 ..LanguageConfig::default()
26583 },
26584 None,
26585 ));
26586
26587 let mut cx = EditorTestContext::new(cx).await;
26588 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26589 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26590
26591 cx.update_editor(|editor, window, cx| {
26592 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26593 editor.paste(&Paste, window, cx);
26594 });
26595
26596 cx.assert_editor_state(&format!(
26597 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26598 ));
26599}
26600
26601#[gpui::test]
26602async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26603 cx: &mut gpui::TestAppContext,
26604) {
26605 init_test(cx, |_| {});
26606
26607 let url = "https://zed.dev";
26608
26609 let markdown_language = Arc::new(Language::new(
26610 LanguageConfig {
26611 name: "Markdown".into(),
26612 ..LanguageConfig::default()
26613 },
26614 None,
26615 ));
26616
26617 let mut cx = EditorTestContext::new(cx).await;
26618 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26619 cx.set_state(&format!(
26620 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26621 ));
26622
26623 cx.update_editor(|editor, window, cx| {
26624 editor.copy(&Copy, window, cx);
26625 });
26626
26627 cx.set_state(&format!(
26628 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26629 ));
26630
26631 cx.update_editor(|editor, window, cx| {
26632 editor.paste(&Paste, window, cx);
26633 });
26634
26635 cx.assert_editor_state(&format!(
26636 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26637 ));
26638}
26639
26640#[gpui::test]
26641async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26642 cx: &mut gpui::TestAppContext,
26643) {
26644 init_test(cx, |_| {});
26645
26646 let url = "https://zed.dev";
26647
26648 let markdown_language = Arc::new(Language::new(
26649 LanguageConfig {
26650 name: "Markdown".into(),
26651 ..LanguageConfig::default()
26652 },
26653 None,
26654 ));
26655
26656 let mut cx = EditorTestContext::new(cx).await;
26657 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26658 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26659
26660 cx.update_editor(|editor, window, cx| {
26661 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26662 editor.paste(&Paste, window, cx);
26663 });
26664
26665 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26666}
26667
26668#[gpui::test]
26669async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26670 cx: &mut gpui::TestAppContext,
26671) {
26672 init_test(cx, |_| {});
26673
26674 let text = "Awesome";
26675
26676 let markdown_language = Arc::new(Language::new(
26677 LanguageConfig {
26678 name: "Markdown".into(),
26679 ..LanguageConfig::default()
26680 },
26681 None,
26682 ));
26683
26684 let mut cx = EditorTestContext::new(cx).await;
26685 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26686 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26687
26688 cx.update_editor(|editor, window, cx| {
26689 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26690 editor.paste(&Paste, window, cx);
26691 });
26692
26693 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26694}
26695
26696#[gpui::test]
26697async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26698 cx: &mut gpui::TestAppContext,
26699) {
26700 init_test(cx, |_| {});
26701
26702 let url = "https://zed.dev";
26703
26704 let markdown_language = Arc::new(Language::new(
26705 LanguageConfig {
26706 name: "Rust".into(),
26707 ..LanguageConfig::default()
26708 },
26709 None,
26710 ));
26711
26712 let mut cx = EditorTestContext::new(cx).await;
26713 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26714 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26715
26716 cx.update_editor(|editor, window, cx| {
26717 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26718 editor.paste(&Paste, window, cx);
26719 });
26720
26721 cx.assert_editor_state(&format!(
26722 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26723 ));
26724}
26725
26726#[gpui::test]
26727async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26728 cx: &mut TestAppContext,
26729) {
26730 init_test(cx, |_| {});
26731
26732 let url = "https://zed.dev";
26733
26734 let markdown_language = Arc::new(Language::new(
26735 LanguageConfig {
26736 name: "Markdown".into(),
26737 ..LanguageConfig::default()
26738 },
26739 None,
26740 ));
26741
26742 let (editor, cx) = cx.add_window_view(|window, cx| {
26743 let multi_buffer = MultiBuffer::build_multi(
26744 [
26745 ("this will embed -> link", vec![Point::row_range(0..1)]),
26746 ("this will replace -> link", vec![Point::row_range(0..1)]),
26747 ],
26748 cx,
26749 );
26750 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26752 s.select_ranges(vec![
26753 Point::new(0, 19)..Point::new(0, 23),
26754 Point::new(1, 21)..Point::new(1, 25),
26755 ])
26756 });
26757 let first_buffer_id = multi_buffer
26758 .read(cx)
26759 .excerpt_buffer_ids()
26760 .into_iter()
26761 .next()
26762 .unwrap();
26763 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26764 first_buffer.update(cx, |buffer, cx| {
26765 buffer.set_language(Some(markdown_language.clone()), cx);
26766 });
26767
26768 editor
26769 });
26770 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26771
26772 cx.update_editor(|editor, window, cx| {
26773 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26774 editor.paste(&Paste, window, cx);
26775 });
26776
26777 cx.assert_editor_state(&format!(
26778 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26779 ));
26780}
26781
26782#[gpui::test]
26783async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26784 init_test(cx, |_| {});
26785
26786 let fs = FakeFs::new(cx.executor());
26787 fs.insert_tree(
26788 path!("/project"),
26789 json!({
26790 "first.rs": "# First Document\nSome content here.",
26791 "second.rs": "Plain text content for second file.",
26792 }),
26793 )
26794 .await;
26795
26796 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26797 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26798 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26799
26800 let language = rust_lang();
26801 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26802 language_registry.add(language.clone());
26803 let mut fake_servers = language_registry.register_fake_lsp(
26804 "Rust",
26805 FakeLspAdapter {
26806 ..FakeLspAdapter::default()
26807 },
26808 );
26809
26810 let buffer1 = project
26811 .update(cx, |project, cx| {
26812 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26813 })
26814 .await
26815 .unwrap();
26816 let buffer2 = project
26817 .update(cx, |project, cx| {
26818 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26819 })
26820 .await
26821 .unwrap();
26822
26823 let multi_buffer = cx.new(|cx| {
26824 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26825 multi_buffer.set_excerpts_for_path(
26826 PathKey::for_buffer(&buffer1, cx),
26827 buffer1.clone(),
26828 [Point::zero()..buffer1.read(cx).max_point()],
26829 3,
26830 cx,
26831 );
26832 multi_buffer.set_excerpts_for_path(
26833 PathKey::for_buffer(&buffer2, cx),
26834 buffer2.clone(),
26835 [Point::zero()..buffer1.read(cx).max_point()],
26836 3,
26837 cx,
26838 );
26839 multi_buffer
26840 });
26841
26842 let (editor, cx) = cx.add_window_view(|window, cx| {
26843 Editor::new(
26844 EditorMode::full(),
26845 multi_buffer,
26846 Some(project.clone()),
26847 window,
26848 cx,
26849 )
26850 });
26851
26852 let fake_language_server = fake_servers.next().await.unwrap();
26853
26854 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26855
26856 let save = editor.update_in(cx, |editor, window, cx| {
26857 assert!(editor.is_dirty(cx));
26858
26859 editor.save(
26860 SaveOptions {
26861 format: true,
26862 autosave: true,
26863 },
26864 project,
26865 window,
26866 cx,
26867 )
26868 });
26869 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26870 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26871 let mut done_edit_rx = Some(done_edit_rx);
26872 let mut start_edit_tx = Some(start_edit_tx);
26873
26874 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26875 start_edit_tx.take().unwrap().send(()).unwrap();
26876 let done_edit_rx = done_edit_rx.take().unwrap();
26877 async move {
26878 done_edit_rx.await.unwrap();
26879 Ok(None)
26880 }
26881 });
26882
26883 start_edit_rx.await.unwrap();
26884 buffer2
26885 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26886 .unwrap();
26887
26888 done_edit_tx.send(()).unwrap();
26889
26890 save.await.unwrap();
26891 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26892}
26893
26894#[track_caller]
26895fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26896 editor
26897 .all_inlays(cx)
26898 .into_iter()
26899 .filter_map(|inlay| inlay.get_color())
26900 .map(Rgba::from)
26901 .collect()
26902}
26903
26904#[gpui::test]
26905fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26906 init_test(cx, |_| {});
26907
26908 let editor = cx.add_window(|window, cx| {
26909 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26910 build_editor(buffer, window, cx)
26911 });
26912
26913 editor
26914 .update(cx, |editor, window, cx| {
26915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26916 s.select_display_ranges([
26917 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26918 ])
26919 });
26920
26921 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26922
26923 assert_eq!(
26924 editor.display_text(cx),
26925 "line1\nline2\nline2",
26926 "Duplicating last line upward should create duplicate above, not on same line"
26927 );
26928
26929 assert_eq!(
26930 editor.selections.display_ranges(cx),
26931 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26932 "Selection should move to the duplicated line"
26933 );
26934 })
26935 .unwrap();
26936}
26937
26938#[gpui::test]
26939async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26940 init_test(cx, |_| {});
26941
26942 let mut cx = EditorTestContext::new(cx).await;
26943
26944 cx.set_state("line1\nline2ˇ");
26945
26946 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26947
26948 let clipboard_text = cx
26949 .read_from_clipboard()
26950 .and_then(|item| item.text().as_deref().map(str::to_string));
26951
26952 assert_eq!(
26953 clipboard_text,
26954 Some("line2\n".to_string()),
26955 "Copying a line without trailing newline should include a newline"
26956 );
26957
26958 cx.set_state("line1\nˇ");
26959
26960 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26961
26962 cx.assert_editor_state("line1\nline2\nˇ");
26963}
26964
26965#[gpui::test]
26966async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26967 init_test(cx, |_| {});
26968
26969 let mut cx = EditorTestContext::new(cx).await;
26970
26971 cx.set_state("line1\nline2ˇ");
26972 cx.update_editor(|e, window, cx| {
26973 e.set_mode(EditorMode::SingleLine);
26974 assert!(e.key_context(window, cx).contains("end_of_input"));
26975 });
26976 cx.set_state("ˇline1\nline2");
26977 cx.update_editor(|e, window, cx| {
26978 assert!(!e.key_context(window, cx).contains("end_of_input"));
26979 });
26980 cx.set_state("line1ˇ\nline2");
26981 cx.update_editor(|e, window, cx| {
26982 assert!(!e.key_context(window, cx).contains("end_of_input"));
26983 });
26984}
26985
26986#[gpui::test]
26987async fn test_next_prev_reference(cx: &mut TestAppContext) {
26988 const CYCLE_POSITIONS: &[&'static str] = &[
26989 indoc! {"
26990 fn foo() {
26991 let ˇabc = 123;
26992 let x = abc + 1;
26993 let y = abc + 2;
26994 let z = abc + 2;
26995 }
26996 "},
26997 indoc! {"
26998 fn foo() {
26999 let abc = 123;
27000 let x = ˇabc + 1;
27001 let y = abc + 2;
27002 let z = abc + 2;
27003 }
27004 "},
27005 indoc! {"
27006 fn foo() {
27007 let abc = 123;
27008 let x = abc + 1;
27009 let y = ˇabc + 2;
27010 let z = abc + 2;
27011 }
27012 "},
27013 indoc! {"
27014 fn foo() {
27015 let abc = 123;
27016 let x = abc + 1;
27017 let y = abc + 2;
27018 let z = ˇabc + 2;
27019 }
27020 "},
27021 ];
27022
27023 init_test(cx, |_| {});
27024
27025 let mut cx = EditorLspTestContext::new_rust(
27026 lsp::ServerCapabilities {
27027 references_provider: Some(lsp::OneOf::Left(true)),
27028 ..Default::default()
27029 },
27030 cx,
27031 )
27032 .await;
27033
27034 // importantly, the cursor is in the middle
27035 cx.set_state(indoc! {"
27036 fn foo() {
27037 let aˇbc = 123;
27038 let x = abc + 1;
27039 let y = abc + 2;
27040 let z = abc + 2;
27041 }
27042 "});
27043
27044 let reference_ranges = [
27045 lsp::Position::new(1, 8),
27046 lsp::Position::new(2, 12),
27047 lsp::Position::new(3, 12),
27048 lsp::Position::new(4, 12),
27049 ]
27050 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27051
27052 cx.lsp
27053 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27054 Ok(Some(
27055 reference_ranges
27056 .map(|range| lsp::Location {
27057 uri: params.text_document_position.text_document.uri.clone(),
27058 range,
27059 })
27060 .to_vec(),
27061 ))
27062 });
27063
27064 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27065 cx.update_editor(|editor, window, cx| {
27066 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27067 })
27068 .unwrap()
27069 .await
27070 .unwrap()
27071 };
27072
27073 _move(Direction::Next, 1, &mut cx).await;
27074 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27075
27076 _move(Direction::Next, 1, &mut cx).await;
27077 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27078
27079 _move(Direction::Next, 1, &mut cx).await;
27080 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27081
27082 // loops back to the start
27083 _move(Direction::Next, 1, &mut cx).await;
27084 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27085
27086 // loops back to the end
27087 _move(Direction::Prev, 1, &mut cx).await;
27088 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27089
27090 _move(Direction::Prev, 1, &mut cx).await;
27091 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27092
27093 _move(Direction::Prev, 1, &mut cx).await;
27094 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27095
27096 _move(Direction::Prev, 1, &mut cx).await;
27097 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27098
27099 _move(Direction::Next, 3, &mut cx).await;
27100 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27101
27102 _move(Direction::Prev, 2, &mut cx).await;
27103 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27104}