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]
3140async fn test_newline_yaml(cx: &mut TestAppContext) {
3141 init_test(cx, |_| {});
3142
3143 let mut cx = EditorTestContext::new(cx).await;
3144 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3145 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3146
3147 // Object (between 2 fields)
3148 cx.set_state(indoc! {"
3149 test:ˇ
3150 hello: bye"});
3151 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3152 cx.assert_editor_state(indoc! {"
3153 test:
3154 ˇ
3155 hello: bye"});
3156
3157 // Object (first and single line)
3158 cx.set_state(indoc! {"
3159 test:ˇ"});
3160 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3161 cx.assert_editor_state(indoc! {"
3162 test:
3163 ˇ"});
3164
3165 // Array with objects (after first element)
3166 cx.set_state(indoc! {"
3167 test:
3168 - foo: barˇ"});
3169 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3170 cx.assert_editor_state(indoc! {"
3171 test:
3172 - foo: bar
3173 ˇ"});
3174
3175 // Array with objects and comment
3176 cx.set_state(indoc! {"
3177 test:
3178 - foo: bar
3179 - bar: # testˇ"});
3180 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 test:
3183 - foo: bar
3184 - bar: # test
3185 ˇ"});
3186
3187 // Array with objects (after second element)
3188 cx.set_state(indoc! {"
3189 test:
3190 - foo: bar
3191 - bar: fooˇ"});
3192 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 test:
3195 - foo: bar
3196 - bar: foo
3197 ˇ"});
3198
3199 // Array with strings (after first element)
3200 cx.set_state(indoc! {"
3201 test:
3202 - fooˇ"});
3203 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3204 cx.assert_editor_state(indoc! {"
3205 test:
3206 - foo
3207 ˇ"});
3208}
3209
3210#[gpui::test]
3211fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3212 init_test(cx, |_| {});
3213
3214 let editor = cx.add_window(|window, cx| {
3215 let buffer = MultiBuffer::build_simple(
3216 "
3217 a
3218 b(
3219 X
3220 )
3221 c(
3222 X
3223 )
3224 "
3225 .unindent()
3226 .as_str(),
3227 cx,
3228 );
3229 let mut editor = build_editor(buffer, window, cx);
3230 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3231 s.select_ranges([
3232 Point::new(2, 4)..Point::new(2, 5),
3233 Point::new(5, 4)..Point::new(5, 5),
3234 ])
3235 });
3236 editor
3237 });
3238
3239 _ = editor.update(cx, |editor, window, cx| {
3240 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3241 editor.buffer.update(cx, |buffer, cx| {
3242 buffer.edit(
3243 [
3244 (Point::new(1, 2)..Point::new(3, 0), ""),
3245 (Point::new(4, 2)..Point::new(6, 0), ""),
3246 ],
3247 None,
3248 cx,
3249 );
3250 assert_eq!(
3251 buffer.read(cx).text(),
3252 "
3253 a
3254 b()
3255 c()
3256 "
3257 .unindent()
3258 );
3259 });
3260 assert_eq!(
3261 editor.selections.ranges(&editor.display_snapshot(cx)),
3262 &[
3263 Point::new(1, 2)..Point::new(1, 2),
3264 Point::new(2, 2)..Point::new(2, 2),
3265 ],
3266 );
3267
3268 editor.newline(&Newline, window, cx);
3269 assert_eq!(
3270 editor.text(cx),
3271 "
3272 a
3273 b(
3274 )
3275 c(
3276 )
3277 "
3278 .unindent()
3279 );
3280
3281 // The selections are moved after the inserted newlines
3282 assert_eq!(
3283 editor.selections.ranges(&editor.display_snapshot(cx)),
3284 &[
3285 Point::new(2, 0)..Point::new(2, 0),
3286 Point::new(4, 0)..Point::new(4, 0),
3287 ],
3288 );
3289 });
3290}
3291
3292#[gpui::test]
3293async fn test_newline_above(cx: &mut TestAppContext) {
3294 init_test(cx, |settings| {
3295 settings.defaults.tab_size = NonZeroU32::new(4)
3296 });
3297
3298 let language = Arc::new(
3299 Language::new(
3300 LanguageConfig::default(),
3301 Some(tree_sitter_rust::LANGUAGE.into()),
3302 )
3303 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3304 .unwrap(),
3305 );
3306
3307 let mut cx = EditorTestContext::new(cx).await;
3308 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3309 cx.set_state(indoc! {"
3310 const a: ˇA = (
3311 (ˇ
3312 «const_functionˇ»(ˇ),
3313 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3314 )ˇ
3315 ˇ);ˇ
3316 "});
3317
3318 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3319 cx.assert_editor_state(indoc! {"
3320 ˇ
3321 const a: A = (
3322 ˇ
3323 (
3324 ˇ
3325 ˇ
3326 const_function(),
3327 ˇ
3328 ˇ
3329 ˇ
3330 ˇ
3331 something_else,
3332 ˇ
3333 )
3334 ˇ
3335 ˇ
3336 );
3337 "});
3338}
3339
3340#[gpui::test]
3341async fn test_newline_below(cx: &mut TestAppContext) {
3342 init_test(cx, |settings| {
3343 settings.defaults.tab_size = NonZeroU32::new(4)
3344 });
3345
3346 let language = Arc::new(
3347 Language::new(
3348 LanguageConfig::default(),
3349 Some(tree_sitter_rust::LANGUAGE.into()),
3350 )
3351 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3352 .unwrap(),
3353 );
3354
3355 let mut cx = EditorTestContext::new(cx).await;
3356 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3357 cx.set_state(indoc! {"
3358 const a: ˇA = (
3359 (ˇ
3360 «const_functionˇ»(ˇ),
3361 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3362 )ˇ
3363 ˇ);ˇ
3364 "});
3365
3366 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3367 cx.assert_editor_state(indoc! {"
3368 const a: A = (
3369 ˇ
3370 (
3371 ˇ
3372 const_function(),
3373 ˇ
3374 ˇ
3375 something_else,
3376 ˇ
3377 ˇ
3378 ˇ
3379 ˇ
3380 )
3381 ˇ
3382 );
3383 ˇ
3384 ˇ
3385 "});
3386}
3387
3388#[gpui::test]
3389async fn test_newline_comments(cx: &mut TestAppContext) {
3390 init_test(cx, |settings| {
3391 settings.defaults.tab_size = NonZeroU32::new(4)
3392 });
3393
3394 let language = Arc::new(Language::new(
3395 LanguageConfig {
3396 line_comments: vec!["// ".into()],
3397 ..LanguageConfig::default()
3398 },
3399 None,
3400 ));
3401 {
3402 let mut cx = EditorTestContext::new(cx).await;
3403 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3404 cx.set_state(indoc! {"
3405 // Fooˇ
3406 "});
3407
3408 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 // Foo
3411 // ˇ
3412 "});
3413 // Ensure that we add comment prefix when existing line contains space
3414 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3415 cx.assert_editor_state(
3416 indoc! {"
3417 // Foo
3418 //s
3419 // ˇ
3420 "}
3421 .replace("s", " ") // s is used as space placeholder to prevent format on save
3422 .as_str(),
3423 );
3424 // Ensure that we add comment prefix when existing line does not contain space
3425 cx.set_state(indoc! {"
3426 // Foo
3427 //ˇ
3428 "});
3429 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3430 cx.assert_editor_state(indoc! {"
3431 // Foo
3432 //
3433 // ˇ
3434 "});
3435 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3436 cx.set_state(indoc! {"
3437 ˇ// Foo
3438 "});
3439 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3440 cx.assert_editor_state(indoc! {"
3441
3442 ˇ// Foo
3443 "});
3444 }
3445 // Ensure that comment continuations can be disabled.
3446 update_test_language_settings(cx, |settings| {
3447 settings.defaults.extend_comment_on_newline = Some(false);
3448 });
3449 let mut cx = EditorTestContext::new(cx).await;
3450 cx.set_state(indoc! {"
3451 // Fooˇ
3452 "});
3453 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3454 cx.assert_editor_state(indoc! {"
3455 // Foo
3456 ˇ
3457 "});
3458}
3459
3460#[gpui::test]
3461async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3462 init_test(cx, |settings| {
3463 settings.defaults.tab_size = NonZeroU32::new(4)
3464 });
3465
3466 let language = Arc::new(Language::new(
3467 LanguageConfig {
3468 line_comments: vec!["// ".into(), "/// ".into()],
3469 ..LanguageConfig::default()
3470 },
3471 None,
3472 ));
3473 {
3474 let mut cx = EditorTestContext::new(cx).await;
3475 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3476 cx.set_state(indoc! {"
3477 //ˇ
3478 "});
3479 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3480 cx.assert_editor_state(indoc! {"
3481 //
3482 // ˇ
3483 "});
3484
3485 cx.set_state(indoc! {"
3486 ///ˇ
3487 "});
3488 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3489 cx.assert_editor_state(indoc! {"
3490 ///
3491 /// ˇ
3492 "});
3493 }
3494}
3495
3496#[gpui::test]
3497async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3498 init_test(cx, |settings| {
3499 settings.defaults.tab_size = NonZeroU32::new(4)
3500 });
3501
3502 let language = Arc::new(
3503 Language::new(
3504 LanguageConfig {
3505 documentation_comment: Some(language::BlockCommentConfig {
3506 start: "/**".into(),
3507 end: "*/".into(),
3508 prefix: "* ".into(),
3509 tab_size: 1,
3510 }),
3511
3512 ..LanguageConfig::default()
3513 },
3514 Some(tree_sitter_rust::LANGUAGE.into()),
3515 )
3516 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3517 .unwrap(),
3518 );
3519
3520 {
3521 let mut cx = EditorTestContext::new(cx).await;
3522 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3523 cx.set_state(indoc! {"
3524 /**ˇ
3525 "});
3526
3527 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3528 cx.assert_editor_state(indoc! {"
3529 /**
3530 * ˇ
3531 "});
3532 // Ensure that if cursor is before the comment start,
3533 // we do not actually insert a comment prefix.
3534 cx.set_state(indoc! {"
3535 ˇ/**
3536 "});
3537 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3538 cx.assert_editor_state(indoc! {"
3539
3540 ˇ/**
3541 "});
3542 // Ensure that if cursor is between it doesn't add comment prefix.
3543 cx.set_state(indoc! {"
3544 /*ˇ*
3545 "});
3546 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3547 cx.assert_editor_state(indoc! {"
3548 /*
3549 ˇ*
3550 "});
3551 // Ensure that if suffix exists on same line after cursor it adds new line.
3552 cx.set_state(indoc! {"
3553 /**ˇ*/
3554 "});
3555 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3556 cx.assert_editor_state(indoc! {"
3557 /**
3558 * ˇ
3559 */
3560 "});
3561 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3562 cx.set_state(indoc! {"
3563 /**ˇ */
3564 "});
3565 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3566 cx.assert_editor_state(indoc! {"
3567 /**
3568 * ˇ
3569 */
3570 "});
3571 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3572 cx.set_state(indoc! {"
3573 /** ˇ*/
3574 "});
3575 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3576 cx.assert_editor_state(
3577 indoc! {"
3578 /**s
3579 * ˇ
3580 */
3581 "}
3582 .replace("s", " ") // s is used as space placeholder to prevent format on save
3583 .as_str(),
3584 );
3585 // Ensure that delimiter space is preserved when newline on already
3586 // spaced delimiter.
3587 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3588 cx.assert_editor_state(
3589 indoc! {"
3590 /**s
3591 *s
3592 * ˇ
3593 */
3594 "}
3595 .replace("s", " ") // s is used as space placeholder to prevent format on save
3596 .as_str(),
3597 );
3598 // Ensure that delimiter space is preserved when space is not
3599 // on existing delimiter.
3600 cx.set_state(indoc! {"
3601 /**
3602 *ˇ
3603 */
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /**
3608 *
3609 * ˇ
3610 */
3611 "});
3612 // Ensure that if suffix exists on same line after cursor it
3613 // doesn't add extra new line if prefix is not on same line.
3614 cx.set_state(indoc! {"
3615 /**
3616 ˇ*/
3617 "});
3618 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3619 cx.assert_editor_state(indoc! {"
3620 /**
3621
3622 ˇ*/
3623 "});
3624 // Ensure that it detects suffix after existing prefix.
3625 cx.set_state(indoc! {"
3626 /**ˇ/
3627 "});
3628 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3629 cx.assert_editor_state(indoc! {"
3630 /**
3631 ˇ/
3632 "});
3633 // Ensure that if suffix exists on same line before
3634 // cursor it does not add comment prefix.
3635 cx.set_state(indoc! {"
3636 /** */ˇ
3637 "});
3638 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3639 cx.assert_editor_state(indoc! {"
3640 /** */
3641 ˇ
3642 "});
3643 // Ensure that if suffix exists on same line before
3644 // cursor it does not add comment prefix.
3645 cx.set_state(indoc! {"
3646 /**
3647 *
3648 */ˇ
3649 "});
3650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 /**
3653 *
3654 */
3655 ˇ
3656 "});
3657
3658 // Ensure that inline comment followed by code
3659 // doesn't add comment prefix on newline
3660 cx.set_state(indoc! {"
3661 /** */ textˇ
3662 "});
3663 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3664 cx.assert_editor_state(indoc! {"
3665 /** */ text
3666 ˇ
3667 "});
3668
3669 // Ensure that text after comment end tag
3670 // doesn't add comment prefix on newline
3671 cx.set_state(indoc! {"
3672 /**
3673 *
3674 */ˇtext
3675 "});
3676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 /**
3679 *
3680 */
3681 ˇtext
3682 "});
3683
3684 // Ensure if not comment block it doesn't
3685 // add comment prefix on newline
3686 cx.set_state(indoc! {"
3687 * textˇ
3688 "});
3689 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3690 cx.assert_editor_state(indoc! {"
3691 * text
3692 ˇ
3693 "});
3694 }
3695 // Ensure that comment continuations can be disabled.
3696 update_test_language_settings(cx, |settings| {
3697 settings.defaults.extend_comment_on_newline = Some(false);
3698 });
3699 let mut cx = EditorTestContext::new(cx).await;
3700 cx.set_state(indoc! {"
3701 /**ˇ
3702 "});
3703 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3704 cx.assert_editor_state(indoc! {"
3705 /**
3706 ˇ
3707 "});
3708}
3709
3710#[gpui::test]
3711async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3712 init_test(cx, |settings| {
3713 settings.defaults.tab_size = NonZeroU32::new(4)
3714 });
3715
3716 let lua_language = Arc::new(Language::new(
3717 LanguageConfig {
3718 line_comments: vec!["--".into()],
3719 block_comment: Some(language::BlockCommentConfig {
3720 start: "--[[".into(),
3721 prefix: "".into(),
3722 end: "]]".into(),
3723 tab_size: 0,
3724 }),
3725 ..LanguageConfig::default()
3726 },
3727 None,
3728 ));
3729
3730 let mut cx = EditorTestContext::new(cx).await;
3731 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3732
3733 // Line with line comment should extend
3734 cx.set_state(indoc! {"
3735 --ˇ
3736 "});
3737 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3738 cx.assert_editor_state(indoc! {"
3739 --
3740 --ˇ
3741 "});
3742
3743 // Line with block comment that matches line comment should not extend
3744 cx.set_state(indoc! {"
3745 --[[ˇ
3746 "});
3747 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3748 cx.assert_editor_state(indoc! {"
3749 --[[
3750 ˇ
3751 "});
3752}
3753
3754#[gpui::test]
3755fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3756 init_test(cx, |_| {});
3757
3758 let editor = cx.add_window(|window, cx| {
3759 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3760 let mut editor = build_editor(buffer, window, cx);
3761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3762 s.select_ranges([3..4, 11..12, 19..20])
3763 });
3764 editor
3765 });
3766
3767 _ = editor.update(cx, |editor, window, cx| {
3768 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3769 editor.buffer.update(cx, |buffer, cx| {
3770 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3771 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3772 });
3773 assert_eq!(
3774 editor.selections.ranges(&editor.display_snapshot(cx)),
3775 &[2..2, 7..7, 12..12],
3776 );
3777
3778 editor.insert("Z", window, cx);
3779 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3780
3781 // The selections are moved after the inserted characters
3782 assert_eq!(
3783 editor.selections.ranges(&editor.display_snapshot(cx)),
3784 &[3..3, 9..9, 15..15],
3785 );
3786 });
3787}
3788
3789#[gpui::test]
3790async fn test_tab(cx: &mut TestAppContext) {
3791 init_test(cx, |settings| {
3792 settings.defaults.tab_size = NonZeroU32::new(3)
3793 });
3794
3795 let mut cx = EditorTestContext::new(cx).await;
3796 cx.set_state(indoc! {"
3797 ˇabˇc
3798 ˇ🏀ˇ🏀ˇefg
3799 dˇ
3800 "});
3801 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3802 cx.assert_editor_state(indoc! {"
3803 ˇab ˇc
3804 ˇ🏀 ˇ🏀 ˇefg
3805 d ˇ
3806 "});
3807
3808 cx.set_state(indoc! {"
3809 a
3810 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3811 "});
3812 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3813 cx.assert_editor_state(indoc! {"
3814 a
3815 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3816 "});
3817}
3818
3819#[gpui::test]
3820async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824 let language = Arc::new(
3825 Language::new(
3826 LanguageConfig::default(),
3827 Some(tree_sitter_rust::LANGUAGE.into()),
3828 )
3829 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3830 .unwrap(),
3831 );
3832 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3833
3834 // test when all cursors are not at suggested indent
3835 // then simply move to their suggested indent location
3836 cx.set_state(indoc! {"
3837 const a: B = (
3838 c(
3839 ˇ
3840 ˇ )
3841 );
3842 "});
3843 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3844 cx.assert_editor_state(indoc! {"
3845 const a: B = (
3846 c(
3847 ˇ
3848 ˇ)
3849 );
3850 "});
3851
3852 // test cursor already at suggested indent not moving when
3853 // other cursors are yet to reach their suggested indents
3854 cx.set_state(indoc! {"
3855 ˇ
3856 const a: B = (
3857 c(
3858 d(
3859 ˇ
3860 )
3861 ˇ
3862 ˇ )
3863 );
3864 "});
3865 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3866 cx.assert_editor_state(indoc! {"
3867 ˇ
3868 const a: B = (
3869 c(
3870 d(
3871 ˇ
3872 )
3873 ˇ
3874 ˇ)
3875 );
3876 "});
3877 // test when all cursors are at suggested indent then tab is inserted
3878 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3879 cx.assert_editor_state(indoc! {"
3880 ˇ
3881 const a: B = (
3882 c(
3883 d(
3884 ˇ
3885 )
3886 ˇ
3887 ˇ)
3888 );
3889 "});
3890
3891 // test when current indent is less than suggested indent,
3892 // we adjust line to match suggested indent and move cursor to it
3893 //
3894 // when no other cursor is at word boundary, all of them should move
3895 cx.set_state(indoc! {"
3896 const a: B = (
3897 c(
3898 d(
3899 ˇ
3900 ˇ )
3901 ˇ )
3902 );
3903 "});
3904 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3905 cx.assert_editor_state(indoc! {"
3906 const a: B = (
3907 c(
3908 d(
3909 ˇ
3910 ˇ)
3911 ˇ)
3912 );
3913 "});
3914
3915 // test when current indent is less than suggested indent,
3916 // we adjust line to match suggested indent and move cursor to it
3917 //
3918 // when some other cursor is at word boundary, it should not move
3919 cx.set_state(indoc! {"
3920 const a: B = (
3921 c(
3922 d(
3923 ˇ
3924 ˇ )
3925 ˇ)
3926 );
3927 "});
3928 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3929 cx.assert_editor_state(indoc! {"
3930 const a: B = (
3931 c(
3932 d(
3933 ˇ
3934 ˇ)
3935 ˇ)
3936 );
3937 "});
3938
3939 // test when current indent is more than suggested indent,
3940 // we just move cursor to current indent instead of suggested indent
3941 //
3942 // when no other cursor is at word boundary, all of them should move
3943 cx.set_state(indoc! {"
3944 const a: B = (
3945 c(
3946 d(
3947 ˇ
3948 ˇ )
3949 ˇ )
3950 );
3951 "});
3952 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3953 cx.assert_editor_state(indoc! {"
3954 const a: B = (
3955 c(
3956 d(
3957 ˇ
3958 ˇ)
3959 ˇ)
3960 );
3961 "});
3962 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 const a: B = (
3965 c(
3966 d(
3967 ˇ
3968 ˇ)
3969 ˇ)
3970 );
3971 "});
3972
3973 // test when current indent is more than suggested indent,
3974 // we just move cursor to current indent instead of suggested indent
3975 //
3976 // when some other cursor is at word boundary, it doesn't move
3977 cx.set_state(indoc! {"
3978 const a: B = (
3979 c(
3980 d(
3981 ˇ
3982 ˇ )
3983 ˇ)
3984 );
3985 "});
3986 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 const a: B = (
3989 c(
3990 d(
3991 ˇ
3992 ˇ)
3993 ˇ)
3994 );
3995 "});
3996
3997 // handle auto-indent when there are multiple cursors on the same line
3998 cx.set_state(indoc! {"
3999 const a: B = (
4000 c(
4001 ˇ ˇ
4002 ˇ )
4003 );
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 const a: B = (
4008 c(
4009 ˇ
4010 ˇ)
4011 );
4012 "});
4013}
4014
4015#[gpui::test]
4016async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4017 init_test(cx, |settings| {
4018 settings.defaults.tab_size = NonZeroU32::new(3)
4019 });
4020
4021 let mut cx = EditorTestContext::new(cx).await;
4022 cx.set_state(indoc! {"
4023 ˇ
4024 \t ˇ
4025 \t ˇ
4026 \t ˇ
4027 \t \t\t \t \t\t \t\t \t \t ˇ
4028 "});
4029
4030 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4031 cx.assert_editor_state(indoc! {"
4032 ˇ
4033 \t ˇ
4034 \t ˇ
4035 \t ˇ
4036 \t \t\t \t \t\t \t\t \t \t ˇ
4037 "});
4038}
4039
4040#[gpui::test]
4041async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4042 init_test(cx, |settings| {
4043 settings.defaults.tab_size = NonZeroU32::new(4)
4044 });
4045
4046 let language = Arc::new(
4047 Language::new(
4048 LanguageConfig::default(),
4049 Some(tree_sitter_rust::LANGUAGE.into()),
4050 )
4051 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4052 .unwrap(),
4053 );
4054
4055 let mut cx = EditorTestContext::new(cx).await;
4056 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4057 cx.set_state(indoc! {"
4058 fn a() {
4059 if b {
4060 \t ˇc
4061 }
4062 }
4063 "});
4064
4065 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4066 cx.assert_editor_state(indoc! {"
4067 fn a() {
4068 if b {
4069 ˇc
4070 }
4071 }
4072 "});
4073}
4074
4075#[gpui::test]
4076async fn test_indent_outdent(cx: &mut TestAppContext) {
4077 init_test(cx, |settings| {
4078 settings.defaults.tab_size = NonZeroU32::new(4);
4079 });
4080
4081 let mut cx = EditorTestContext::new(cx).await;
4082
4083 cx.set_state(indoc! {"
4084 «oneˇ» «twoˇ»
4085 three
4086 four
4087 "});
4088 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4089 cx.assert_editor_state(indoc! {"
4090 «oneˇ» «twoˇ»
4091 three
4092 four
4093 "});
4094
4095 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4096 cx.assert_editor_state(indoc! {"
4097 «oneˇ» «twoˇ»
4098 three
4099 four
4100 "});
4101
4102 // select across line ending
4103 cx.set_state(indoc! {"
4104 one two
4105 t«hree
4106 ˇ» four
4107 "});
4108 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4109 cx.assert_editor_state(indoc! {"
4110 one two
4111 t«hree
4112 ˇ» four
4113 "});
4114
4115 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4116 cx.assert_editor_state(indoc! {"
4117 one two
4118 t«hree
4119 ˇ» four
4120 "});
4121
4122 // Ensure that indenting/outdenting works when the cursor is at column 0.
4123 cx.set_state(indoc! {"
4124 one two
4125 ˇthree
4126 four
4127 "});
4128 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4129 cx.assert_editor_state(indoc! {"
4130 one two
4131 ˇthree
4132 four
4133 "});
4134
4135 cx.set_state(indoc! {"
4136 one two
4137 ˇ three
4138 four
4139 "});
4140 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4141 cx.assert_editor_state(indoc! {"
4142 one two
4143 ˇthree
4144 four
4145 "});
4146}
4147
4148#[gpui::test]
4149async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4150 // This is a regression test for issue #33761
4151 init_test(cx, |_| {});
4152
4153 let mut cx = EditorTestContext::new(cx).await;
4154 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4156
4157 cx.set_state(
4158 r#"ˇ# ingress:
4159ˇ# api:
4160ˇ# enabled: false
4161ˇ# pathType: Prefix
4162ˇ# console:
4163ˇ# enabled: false
4164ˇ# pathType: Prefix
4165"#,
4166 );
4167
4168 // Press tab to indent all lines
4169 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4170
4171 cx.assert_editor_state(
4172 r#" ˇ# ingress:
4173 ˇ# api:
4174 ˇ# enabled: false
4175 ˇ# pathType: Prefix
4176 ˇ# console:
4177 ˇ# enabled: false
4178 ˇ# pathType: Prefix
4179"#,
4180 );
4181}
4182
4183#[gpui::test]
4184async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4185 // This is a test to make sure our fix for issue #33761 didn't break anything
4186 init_test(cx, |_| {});
4187
4188 let mut cx = EditorTestContext::new(cx).await;
4189 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4190 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4191
4192 cx.set_state(
4193 r#"ˇingress:
4194ˇ api:
4195ˇ enabled: false
4196ˇ pathType: Prefix
4197"#,
4198 );
4199
4200 // Press tab to indent all lines
4201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4202
4203 cx.assert_editor_state(
4204 r#"ˇingress:
4205 ˇapi:
4206 ˇenabled: false
4207 ˇpathType: Prefix
4208"#,
4209 );
4210}
4211
4212#[gpui::test]
4213async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4214 init_test(cx, |settings| {
4215 settings.defaults.hard_tabs = Some(true);
4216 });
4217
4218 let mut cx = EditorTestContext::new(cx).await;
4219
4220 // select two ranges on one line
4221 cx.set_state(indoc! {"
4222 «oneˇ» «twoˇ»
4223 three
4224 four
4225 "});
4226 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4227 cx.assert_editor_state(indoc! {"
4228 \t«oneˇ» «twoˇ»
4229 three
4230 four
4231 "});
4232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4233 cx.assert_editor_state(indoc! {"
4234 \t\t«oneˇ» «twoˇ»
4235 three
4236 four
4237 "});
4238 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4239 cx.assert_editor_state(indoc! {"
4240 \t«oneˇ» «twoˇ»
4241 three
4242 four
4243 "});
4244 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4245 cx.assert_editor_state(indoc! {"
4246 «oneˇ» «twoˇ»
4247 three
4248 four
4249 "});
4250
4251 // select across a line ending
4252 cx.set_state(indoc! {"
4253 one two
4254 t«hree
4255 ˇ»four
4256 "});
4257 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4258 cx.assert_editor_state(indoc! {"
4259 one two
4260 \tt«hree
4261 ˇ»four
4262 "});
4263 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4264 cx.assert_editor_state(indoc! {"
4265 one two
4266 \t\tt«hree
4267 ˇ»four
4268 "});
4269 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4270 cx.assert_editor_state(indoc! {"
4271 one two
4272 \tt«hree
4273 ˇ»four
4274 "});
4275 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4276 cx.assert_editor_state(indoc! {"
4277 one two
4278 t«hree
4279 ˇ»four
4280 "});
4281
4282 // Ensure that indenting/outdenting works when the cursor is at column 0.
4283 cx.set_state(indoc! {"
4284 one two
4285 ˇthree
4286 four
4287 "});
4288 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4289 cx.assert_editor_state(indoc! {"
4290 one two
4291 ˇthree
4292 four
4293 "});
4294 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4295 cx.assert_editor_state(indoc! {"
4296 one two
4297 \tˇthree
4298 four
4299 "});
4300 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4301 cx.assert_editor_state(indoc! {"
4302 one two
4303 ˇthree
4304 four
4305 "});
4306}
4307
4308#[gpui::test]
4309fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4310 init_test(cx, |settings| {
4311 settings.languages.0.extend([
4312 (
4313 "TOML".into(),
4314 LanguageSettingsContent {
4315 tab_size: NonZeroU32::new(2),
4316 ..Default::default()
4317 },
4318 ),
4319 (
4320 "Rust".into(),
4321 LanguageSettingsContent {
4322 tab_size: NonZeroU32::new(4),
4323 ..Default::default()
4324 },
4325 ),
4326 ]);
4327 });
4328
4329 let toml_language = Arc::new(Language::new(
4330 LanguageConfig {
4331 name: "TOML".into(),
4332 ..Default::default()
4333 },
4334 None,
4335 ));
4336 let rust_language = Arc::new(Language::new(
4337 LanguageConfig {
4338 name: "Rust".into(),
4339 ..Default::default()
4340 },
4341 None,
4342 ));
4343
4344 let toml_buffer =
4345 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4346 let rust_buffer =
4347 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4348 let multibuffer = cx.new(|cx| {
4349 let mut multibuffer = MultiBuffer::new(ReadWrite);
4350 multibuffer.push_excerpts(
4351 toml_buffer.clone(),
4352 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4353 cx,
4354 );
4355 multibuffer.push_excerpts(
4356 rust_buffer.clone(),
4357 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4358 cx,
4359 );
4360 multibuffer
4361 });
4362
4363 cx.add_window(|window, cx| {
4364 let mut editor = build_editor(multibuffer, window, cx);
4365
4366 assert_eq!(
4367 editor.text(cx),
4368 indoc! {"
4369 a = 1
4370 b = 2
4371
4372 const c: usize = 3;
4373 "}
4374 );
4375
4376 select_ranges(
4377 &mut editor,
4378 indoc! {"
4379 «aˇ» = 1
4380 b = 2
4381
4382 «const c:ˇ» usize = 3;
4383 "},
4384 window,
4385 cx,
4386 );
4387
4388 editor.tab(&Tab, window, cx);
4389 assert_text_with_selections(
4390 &mut editor,
4391 indoc! {"
4392 «aˇ» = 1
4393 b = 2
4394
4395 «const c:ˇ» usize = 3;
4396 "},
4397 cx,
4398 );
4399 editor.backtab(&Backtab, window, cx);
4400 assert_text_with_selections(
4401 &mut editor,
4402 indoc! {"
4403 «aˇ» = 1
4404 b = 2
4405
4406 «const c:ˇ» usize = 3;
4407 "},
4408 cx,
4409 );
4410
4411 editor
4412 });
4413}
4414
4415#[gpui::test]
4416async fn test_backspace(cx: &mut TestAppContext) {
4417 init_test(cx, |_| {});
4418
4419 let mut cx = EditorTestContext::new(cx).await;
4420
4421 // Basic backspace
4422 cx.set_state(indoc! {"
4423 onˇe two three
4424 fou«rˇ» five six
4425 seven «ˇeight nine
4426 »ten
4427 "});
4428 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 oˇe two three
4431 fouˇ five six
4432 seven ˇten
4433 "});
4434
4435 // Test backspace inside and around indents
4436 cx.set_state(indoc! {"
4437 zero
4438 ˇone
4439 ˇtwo
4440 ˇ ˇ ˇ three
4441 ˇ ˇ four
4442 "});
4443 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4444 cx.assert_editor_state(indoc! {"
4445 zero
4446 ˇone
4447 ˇtwo
4448 ˇ threeˇ four
4449 "});
4450}
4451
4452#[gpui::test]
4453async fn test_delete(cx: &mut TestAppContext) {
4454 init_test(cx, |_| {});
4455
4456 let mut cx = EditorTestContext::new(cx).await;
4457 cx.set_state(indoc! {"
4458 onˇe two three
4459 fou«rˇ» five six
4460 seven «ˇeight nine
4461 »ten
4462 "});
4463 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4464 cx.assert_editor_state(indoc! {"
4465 onˇ two three
4466 fouˇ five six
4467 seven ˇten
4468 "});
4469}
4470
4471#[gpui::test]
4472fn test_delete_line(cx: &mut TestAppContext) {
4473 init_test(cx, |_| {});
4474
4475 let editor = cx.add_window(|window, cx| {
4476 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4477 build_editor(buffer, window, cx)
4478 });
4479 _ = editor.update(cx, |editor, window, cx| {
4480 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4481 s.select_display_ranges([
4482 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4483 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4484 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4485 ])
4486 });
4487 editor.delete_line(&DeleteLine, window, cx);
4488 assert_eq!(editor.display_text(cx), "ghi");
4489 assert_eq!(
4490 editor.selections.display_ranges(cx),
4491 vec![
4492 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4493 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4494 ]
4495 );
4496 });
4497
4498 let editor = cx.add_window(|window, cx| {
4499 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4500 build_editor(buffer, window, cx)
4501 });
4502 _ = editor.update(cx, |editor, window, cx| {
4503 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4504 s.select_display_ranges([
4505 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4506 ])
4507 });
4508 editor.delete_line(&DeleteLine, window, cx);
4509 assert_eq!(editor.display_text(cx), "ghi\n");
4510 assert_eq!(
4511 editor.selections.display_ranges(cx),
4512 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4513 );
4514 });
4515
4516 let editor = cx.add_window(|window, cx| {
4517 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4518 build_editor(buffer, window, cx)
4519 });
4520 _ = editor.update(cx, |editor, window, cx| {
4521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4522 s.select_display_ranges([
4523 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4524 ])
4525 });
4526 editor.delete_line(&DeleteLine, window, cx);
4527 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4528 assert_eq!(
4529 editor.selections.display_ranges(cx),
4530 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4531 );
4532 });
4533}
4534
4535#[gpui::test]
4536fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4537 init_test(cx, |_| {});
4538
4539 cx.add_window(|window, cx| {
4540 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4541 let mut editor = build_editor(buffer.clone(), window, cx);
4542 let buffer = buffer.read(cx).as_singleton().unwrap();
4543
4544 assert_eq!(
4545 editor
4546 .selections
4547 .ranges::<Point>(&editor.display_snapshot(cx)),
4548 &[Point::new(0, 0)..Point::new(0, 0)]
4549 );
4550
4551 // When on single line, replace newline at end by space
4552 editor.join_lines(&JoinLines, window, cx);
4553 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4554 assert_eq!(
4555 editor
4556 .selections
4557 .ranges::<Point>(&editor.display_snapshot(cx)),
4558 &[Point::new(0, 3)..Point::new(0, 3)]
4559 );
4560
4561 // When multiple lines are selected, remove newlines that are spanned by the selection
4562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4563 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4564 });
4565 editor.join_lines(&JoinLines, window, cx);
4566 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4567 assert_eq!(
4568 editor
4569 .selections
4570 .ranges::<Point>(&editor.display_snapshot(cx)),
4571 &[Point::new(0, 11)..Point::new(0, 11)]
4572 );
4573
4574 // Undo should be transactional
4575 editor.undo(&Undo, window, cx);
4576 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4577 assert_eq!(
4578 editor
4579 .selections
4580 .ranges::<Point>(&editor.display_snapshot(cx)),
4581 &[Point::new(0, 5)..Point::new(2, 2)]
4582 );
4583
4584 // When joining an empty line don't insert a space
4585 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4586 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4587 });
4588 editor.join_lines(&JoinLines, window, cx);
4589 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4590 assert_eq!(
4591 editor
4592 .selections
4593 .ranges::<Point>(&editor.display_snapshot(cx)),
4594 [Point::new(2, 3)..Point::new(2, 3)]
4595 );
4596
4597 // We can remove trailing newlines
4598 editor.join_lines(&JoinLines, window, cx);
4599 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4600 assert_eq!(
4601 editor
4602 .selections
4603 .ranges::<Point>(&editor.display_snapshot(cx)),
4604 [Point::new(2, 3)..Point::new(2, 3)]
4605 );
4606
4607 // We don't blow up on the last line
4608 editor.join_lines(&JoinLines, window, cx);
4609 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4610 assert_eq!(
4611 editor
4612 .selections
4613 .ranges::<Point>(&editor.display_snapshot(cx)),
4614 [Point::new(2, 3)..Point::new(2, 3)]
4615 );
4616
4617 // reset to test indentation
4618 editor.buffer.update(cx, |buffer, cx| {
4619 buffer.edit(
4620 [
4621 (Point::new(1, 0)..Point::new(1, 2), " "),
4622 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4623 ],
4624 None,
4625 cx,
4626 )
4627 });
4628
4629 // We remove any leading spaces
4630 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4632 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4633 });
4634 editor.join_lines(&JoinLines, window, cx);
4635 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4636
4637 // We don't insert a space for a line containing only spaces
4638 editor.join_lines(&JoinLines, window, cx);
4639 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4640
4641 // We ignore any leading tabs
4642 editor.join_lines(&JoinLines, window, cx);
4643 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4644
4645 editor
4646 });
4647}
4648
4649#[gpui::test]
4650fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4651 init_test(cx, |_| {});
4652
4653 cx.add_window(|window, cx| {
4654 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4655 let mut editor = build_editor(buffer.clone(), window, cx);
4656 let buffer = buffer.read(cx).as_singleton().unwrap();
4657
4658 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4659 s.select_ranges([
4660 Point::new(0, 2)..Point::new(1, 1),
4661 Point::new(1, 2)..Point::new(1, 2),
4662 Point::new(3, 1)..Point::new(3, 2),
4663 ])
4664 });
4665
4666 editor.join_lines(&JoinLines, window, cx);
4667 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4668
4669 assert_eq!(
4670 editor
4671 .selections
4672 .ranges::<Point>(&editor.display_snapshot(cx)),
4673 [
4674 Point::new(0, 7)..Point::new(0, 7),
4675 Point::new(1, 3)..Point::new(1, 3)
4676 ]
4677 );
4678 editor
4679 });
4680}
4681
4682#[gpui::test]
4683async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4684 init_test(cx, |_| {});
4685
4686 let mut cx = EditorTestContext::new(cx).await;
4687
4688 let diff_base = r#"
4689 Line 0
4690 Line 1
4691 Line 2
4692 Line 3
4693 "#
4694 .unindent();
4695
4696 cx.set_state(
4697 &r#"
4698 ˇLine 0
4699 Line 1
4700 Line 2
4701 Line 3
4702 "#
4703 .unindent(),
4704 );
4705
4706 cx.set_head_text(&diff_base);
4707 executor.run_until_parked();
4708
4709 // Join lines
4710 cx.update_editor(|editor, window, cx| {
4711 editor.join_lines(&JoinLines, window, cx);
4712 });
4713 executor.run_until_parked();
4714
4715 cx.assert_editor_state(
4716 &r#"
4717 Line 0ˇ Line 1
4718 Line 2
4719 Line 3
4720 "#
4721 .unindent(),
4722 );
4723 // Join again
4724 cx.update_editor(|editor, window, cx| {
4725 editor.join_lines(&JoinLines, window, cx);
4726 });
4727 executor.run_until_parked();
4728
4729 cx.assert_editor_state(
4730 &r#"
4731 Line 0 Line 1ˇ Line 2
4732 Line 3
4733 "#
4734 .unindent(),
4735 );
4736}
4737
4738#[gpui::test]
4739async fn test_custom_newlines_cause_no_false_positive_diffs(
4740 executor: BackgroundExecutor,
4741 cx: &mut TestAppContext,
4742) {
4743 init_test(cx, |_| {});
4744 let mut cx = EditorTestContext::new(cx).await;
4745 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4746 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4747 executor.run_until_parked();
4748
4749 cx.update_editor(|editor, window, cx| {
4750 let snapshot = editor.snapshot(window, cx);
4751 assert_eq!(
4752 snapshot
4753 .buffer_snapshot()
4754 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4755 .collect::<Vec<_>>(),
4756 Vec::new(),
4757 "Should not have any diffs for files with custom newlines"
4758 );
4759 });
4760}
4761
4762#[gpui::test]
4763async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4764 init_test(cx, |_| {});
4765
4766 let mut cx = EditorTestContext::new(cx).await;
4767
4768 // Test sort_lines_case_insensitive()
4769 cx.set_state(indoc! {"
4770 «z
4771 y
4772 x
4773 Z
4774 Y
4775 Xˇ»
4776 "});
4777 cx.update_editor(|e, window, cx| {
4778 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4779 });
4780 cx.assert_editor_state(indoc! {"
4781 «x
4782 X
4783 y
4784 Y
4785 z
4786 Zˇ»
4787 "});
4788
4789 // Test sort_lines_by_length()
4790 //
4791 // Demonstrates:
4792 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4793 // - sort is stable
4794 cx.set_state(indoc! {"
4795 «123
4796 æ
4797 12
4798 ∞
4799 1
4800 æˇ»
4801 "});
4802 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4803 cx.assert_editor_state(indoc! {"
4804 «æ
4805 ∞
4806 1
4807 æ
4808 12
4809 123ˇ»
4810 "});
4811
4812 // Test reverse_lines()
4813 cx.set_state(indoc! {"
4814 «5
4815 4
4816 3
4817 2
4818 1ˇ»
4819 "});
4820 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4821 cx.assert_editor_state(indoc! {"
4822 «1
4823 2
4824 3
4825 4
4826 5ˇ»
4827 "});
4828
4829 // Skip testing shuffle_line()
4830
4831 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4832 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4833
4834 // Don't manipulate when cursor is on single line, but expand the selection
4835 cx.set_state(indoc! {"
4836 ddˇdd
4837 ccc
4838 bb
4839 a
4840 "});
4841 cx.update_editor(|e, window, cx| {
4842 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4843 });
4844 cx.assert_editor_state(indoc! {"
4845 «ddddˇ»
4846 ccc
4847 bb
4848 a
4849 "});
4850
4851 // Basic manipulate case
4852 // Start selection moves to column 0
4853 // End of selection shrinks to fit shorter line
4854 cx.set_state(indoc! {"
4855 dd«d
4856 ccc
4857 bb
4858 aaaaaˇ»
4859 "});
4860 cx.update_editor(|e, window, cx| {
4861 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4862 });
4863 cx.assert_editor_state(indoc! {"
4864 «aaaaa
4865 bb
4866 ccc
4867 dddˇ»
4868 "});
4869
4870 // Manipulate case with newlines
4871 cx.set_state(indoc! {"
4872 dd«d
4873 ccc
4874
4875 bb
4876 aaaaa
4877
4878 ˇ»
4879 "});
4880 cx.update_editor(|e, window, cx| {
4881 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4882 });
4883 cx.assert_editor_state(indoc! {"
4884 «
4885
4886 aaaaa
4887 bb
4888 ccc
4889 dddˇ»
4890
4891 "});
4892
4893 // Adding new line
4894 cx.set_state(indoc! {"
4895 aa«a
4896 bbˇ»b
4897 "});
4898 cx.update_editor(|e, window, cx| {
4899 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4900 });
4901 cx.assert_editor_state(indoc! {"
4902 «aaa
4903 bbb
4904 added_lineˇ»
4905 "});
4906
4907 // Removing line
4908 cx.set_state(indoc! {"
4909 aa«a
4910 bbbˇ»
4911 "});
4912 cx.update_editor(|e, window, cx| {
4913 e.manipulate_immutable_lines(window, cx, |lines| {
4914 lines.pop();
4915 })
4916 });
4917 cx.assert_editor_state(indoc! {"
4918 «aaaˇ»
4919 "});
4920
4921 // Removing all lines
4922 cx.set_state(indoc! {"
4923 aa«a
4924 bbbˇ»
4925 "});
4926 cx.update_editor(|e, window, cx| {
4927 e.manipulate_immutable_lines(window, cx, |lines| {
4928 lines.drain(..);
4929 })
4930 });
4931 cx.assert_editor_state(indoc! {"
4932 ˇ
4933 "});
4934}
4935
4936#[gpui::test]
4937async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4938 init_test(cx, |_| {});
4939
4940 let mut cx = EditorTestContext::new(cx).await;
4941
4942 // Consider continuous selection as single selection
4943 cx.set_state(indoc! {"
4944 Aaa«aa
4945 cˇ»c«c
4946 bb
4947 aaaˇ»aa
4948 "});
4949 cx.update_editor(|e, window, cx| {
4950 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4951 });
4952 cx.assert_editor_state(indoc! {"
4953 «Aaaaa
4954 ccc
4955 bb
4956 aaaaaˇ»
4957 "});
4958
4959 cx.set_state(indoc! {"
4960 Aaa«aa
4961 cˇ»c«c
4962 bb
4963 aaaˇ»aa
4964 "});
4965 cx.update_editor(|e, window, cx| {
4966 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4967 });
4968 cx.assert_editor_state(indoc! {"
4969 «Aaaaa
4970 ccc
4971 bbˇ»
4972 "});
4973
4974 // Consider non continuous selection as distinct dedup operations
4975 cx.set_state(indoc! {"
4976 «aaaaa
4977 bb
4978 aaaaa
4979 aaaaaˇ»
4980
4981 aaa«aaˇ»
4982 "});
4983 cx.update_editor(|e, window, cx| {
4984 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4985 });
4986 cx.assert_editor_state(indoc! {"
4987 «aaaaa
4988 bbˇ»
4989
4990 «aaaaaˇ»
4991 "});
4992}
4993
4994#[gpui::test]
4995async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4996 init_test(cx, |_| {});
4997
4998 let mut cx = EditorTestContext::new(cx).await;
4999
5000 cx.set_state(indoc! {"
5001 «Aaa
5002 aAa
5003 Aaaˇ»
5004 "});
5005 cx.update_editor(|e, window, cx| {
5006 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5007 });
5008 cx.assert_editor_state(indoc! {"
5009 «Aaa
5010 aAaˇ»
5011 "});
5012
5013 cx.set_state(indoc! {"
5014 «Aaa
5015 aAa
5016 aaAˇ»
5017 "});
5018 cx.update_editor(|e, window, cx| {
5019 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5020 });
5021 cx.assert_editor_state(indoc! {"
5022 «Aaaˇ»
5023 "});
5024}
5025
5026#[gpui::test]
5027async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5028 init_test(cx, |_| {});
5029
5030 let mut cx = EditorTestContext::new(cx).await;
5031
5032 let js_language = Arc::new(Language::new(
5033 LanguageConfig {
5034 name: "JavaScript".into(),
5035 wrap_characters: Some(language::WrapCharactersConfig {
5036 start_prefix: "<".into(),
5037 start_suffix: ">".into(),
5038 end_prefix: "</".into(),
5039 end_suffix: ">".into(),
5040 }),
5041 ..LanguageConfig::default()
5042 },
5043 None,
5044 ));
5045
5046 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5047
5048 cx.set_state(indoc! {"
5049 «testˇ»
5050 "});
5051 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5052 cx.assert_editor_state(indoc! {"
5053 <«ˇ»>test</«ˇ»>
5054 "});
5055
5056 cx.set_state(indoc! {"
5057 «test
5058 testˇ»
5059 "});
5060 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5061 cx.assert_editor_state(indoc! {"
5062 <«ˇ»>test
5063 test</«ˇ»>
5064 "});
5065
5066 cx.set_state(indoc! {"
5067 teˇst
5068 "});
5069 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5070 cx.assert_editor_state(indoc! {"
5071 te<«ˇ»></«ˇ»>st
5072 "});
5073}
5074
5075#[gpui::test]
5076async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5077 init_test(cx, |_| {});
5078
5079 let mut cx = EditorTestContext::new(cx).await;
5080
5081 let js_language = Arc::new(Language::new(
5082 LanguageConfig {
5083 name: "JavaScript".into(),
5084 wrap_characters: Some(language::WrapCharactersConfig {
5085 start_prefix: "<".into(),
5086 start_suffix: ">".into(),
5087 end_prefix: "</".into(),
5088 end_suffix: ">".into(),
5089 }),
5090 ..LanguageConfig::default()
5091 },
5092 None,
5093 ));
5094
5095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5096
5097 cx.set_state(indoc! {"
5098 «testˇ»
5099 «testˇ» «testˇ»
5100 «testˇ»
5101 "});
5102 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5103 cx.assert_editor_state(indoc! {"
5104 <«ˇ»>test</«ˇ»>
5105 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5106 <«ˇ»>test</«ˇ»>
5107 "});
5108
5109 cx.set_state(indoc! {"
5110 «test
5111 testˇ»
5112 «test
5113 testˇ»
5114 "});
5115 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5116 cx.assert_editor_state(indoc! {"
5117 <«ˇ»>test
5118 test</«ˇ»>
5119 <«ˇ»>test
5120 test</«ˇ»>
5121 "});
5122}
5123
5124#[gpui::test]
5125async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5126 init_test(cx, |_| {});
5127
5128 let mut cx = EditorTestContext::new(cx).await;
5129
5130 let plaintext_language = Arc::new(Language::new(
5131 LanguageConfig {
5132 name: "Plain Text".into(),
5133 ..LanguageConfig::default()
5134 },
5135 None,
5136 ));
5137
5138 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5139
5140 cx.set_state(indoc! {"
5141 «testˇ»
5142 "});
5143 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5144 cx.assert_editor_state(indoc! {"
5145 «testˇ»
5146 "});
5147}
5148
5149#[gpui::test]
5150async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5151 init_test(cx, |_| {});
5152
5153 let mut cx = EditorTestContext::new(cx).await;
5154
5155 // Manipulate with multiple selections on a single line
5156 cx.set_state(indoc! {"
5157 dd«dd
5158 cˇ»c«c
5159 bb
5160 aaaˇ»aa
5161 "});
5162 cx.update_editor(|e, window, cx| {
5163 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5164 });
5165 cx.assert_editor_state(indoc! {"
5166 «aaaaa
5167 bb
5168 ccc
5169 ddddˇ»
5170 "});
5171
5172 // Manipulate with multiple disjoin selections
5173 cx.set_state(indoc! {"
5174 5«
5175 4
5176 3
5177 2
5178 1ˇ»
5179
5180 dd«dd
5181 ccc
5182 bb
5183 aaaˇ»aa
5184 "});
5185 cx.update_editor(|e, window, cx| {
5186 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5187 });
5188 cx.assert_editor_state(indoc! {"
5189 «1
5190 2
5191 3
5192 4
5193 5ˇ»
5194
5195 «aaaaa
5196 bb
5197 ccc
5198 ddddˇ»
5199 "});
5200
5201 // Adding lines on each selection
5202 cx.set_state(indoc! {"
5203 2«
5204 1ˇ»
5205
5206 bb«bb
5207 aaaˇ»aa
5208 "});
5209 cx.update_editor(|e, window, cx| {
5210 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5211 });
5212 cx.assert_editor_state(indoc! {"
5213 «2
5214 1
5215 added lineˇ»
5216
5217 «bbbb
5218 aaaaa
5219 added lineˇ»
5220 "});
5221
5222 // Removing lines on each selection
5223 cx.set_state(indoc! {"
5224 2«
5225 1ˇ»
5226
5227 bb«bb
5228 aaaˇ»aa
5229 "});
5230 cx.update_editor(|e, window, cx| {
5231 e.manipulate_immutable_lines(window, cx, |lines| {
5232 lines.pop();
5233 })
5234 });
5235 cx.assert_editor_state(indoc! {"
5236 «2ˇ»
5237
5238 «bbbbˇ»
5239 "});
5240}
5241
5242#[gpui::test]
5243async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5244 init_test(cx, |settings| {
5245 settings.defaults.tab_size = NonZeroU32::new(3)
5246 });
5247
5248 let mut cx = EditorTestContext::new(cx).await;
5249
5250 // MULTI SELECTION
5251 // Ln.1 "«" tests empty lines
5252 // Ln.9 tests just leading whitespace
5253 cx.set_state(indoc! {"
5254 «
5255 abc // No indentationˇ»
5256 «\tabc // 1 tabˇ»
5257 \t\tabc « ˇ» // 2 tabs
5258 \t ab«c // Tab followed by space
5259 \tabc // Space followed by tab (3 spaces should be the result)
5260 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5261 abˇ»ˇc ˇ ˇ // Already space indented«
5262 \t
5263 \tabc\tdef // Only the leading tab is manipulatedˇ»
5264 "});
5265 cx.update_editor(|e, window, cx| {
5266 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5267 });
5268 cx.assert_editor_state(
5269 indoc! {"
5270 «
5271 abc // No indentation
5272 abc // 1 tab
5273 abc // 2 tabs
5274 abc // Tab followed by space
5275 abc // Space followed by tab (3 spaces should be the result)
5276 abc // Mixed indentation (tab conversion depends on the column)
5277 abc // Already space indented
5278 ·
5279 abc\tdef // Only the leading tab is manipulatedˇ»
5280 "}
5281 .replace("·", "")
5282 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5283 );
5284
5285 // Test on just a few lines, the others should remain unchanged
5286 // Only lines (3, 5, 10, 11) should change
5287 cx.set_state(
5288 indoc! {"
5289 ·
5290 abc // No indentation
5291 \tabcˇ // 1 tab
5292 \t\tabc // 2 tabs
5293 \t abcˇ // Tab followed by space
5294 \tabc // Space followed by tab (3 spaces should be the result)
5295 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5296 abc // Already space indented
5297 «\t
5298 \tabc\tdef // Only the leading tab is manipulatedˇ»
5299 "}
5300 .replace("·", "")
5301 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5302 );
5303 cx.update_editor(|e, window, cx| {
5304 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5305 });
5306 cx.assert_editor_state(
5307 indoc! {"
5308 ·
5309 abc // No indentation
5310 « abc // 1 tabˇ»
5311 \t\tabc // 2 tabs
5312 « abc // Tab followed by spaceˇ»
5313 \tabc // Space followed by tab (3 spaces should be the result)
5314 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5315 abc // Already space indented
5316 « ·
5317 abc\tdef // Only the leading tab is manipulatedˇ»
5318 "}
5319 .replace("·", "")
5320 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5321 );
5322
5323 // SINGLE SELECTION
5324 // Ln.1 "«" tests empty lines
5325 // Ln.9 tests just leading whitespace
5326 cx.set_state(indoc! {"
5327 «
5328 abc // No indentation
5329 \tabc // 1 tab
5330 \t\tabc // 2 tabs
5331 \t abc // Tab followed by space
5332 \tabc // Space followed by tab (3 spaces should be the result)
5333 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5334 abc // Already space indented
5335 \t
5336 \tabc\tdef // Only the leading tab is manipulatedˇ»
5337 "});
5338 cx.update_editor(|e, window, cx| {
5339 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5340 });
5341 cx.assert_editor_state(
5342 indoc! {"
5343 «
5344 abc // No indentation
5345 abc // 1 tab
5346 abc // 2 tabs
5347 abc // Tab followed by space
5348 abc // Space followed by tab (3 spaces should be the result)
5349 abc // Mixed indentation (tab conversion depends on the column)
5350 abc // Already space indented
5351 ·
5352 abc\tdef // Only the leading tab is manipulatedˇ»
5353 "}
5354 .replace("·", "")
5355 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5356 );
5357}
5358
5359#[gpui::test]
5360async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5361 init_test(cx, |settings| {
5362 settings.defaults.tab_size = NonZeroU32::new(3)
5363 });
5364
5365 let mut cx = EditorTestContext::new(cx).await;
5366
5367 // MULTI SELECTION
5368 // Ln.1 "«" tests empty lines
5369 // Ln.11 tests just leading whitespace
5370 cx.set_state(indoc! {"
5371 «
5372 abˇ»ˇc // No indentation
5373 abc ˇ ˇ // 1 space (< 3 so dont convert)
5374 abc « // 2 spaces (< 3 so dont convert)
5375 abc // 3 spaces (convert)
5376 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5377 «\tˇ»\t«\tˇ»abc // Already tab indented
5378 «\t abc // Tab followed by space
5379 \tabc // Space followed by tab (should be consumed due to tab)
5380 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5381 \tˇ» «\t
5382 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5383 "});
5384 cx.update_editor(|e, window, cx| {
5385 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5386 });
5387 cx.assert_editor_state(indoc! {"
5388 «
5389 abc // No indentation
5390 abc // 1 space (< 3 so dont convert)
5391 abc // 2 spaces (< 3 so dont convert)
5392 \tabc // 3 spaces (convert)
5393 \t abc // 5 spaces (1 tab + 2 spaces)
5394 \t\t\tabc // Already tab indented
5395 \t abc // Tab followed by space
5396 \tabc // Space followed by tab (should be consumed due to tab)
5397 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5398 \t\t\t
5399 \tabc \t // Only the leading spaces should be convertedˇ»
5400 "});
5401
5402 // Test on just a few lines, the other should remain unchanged
5403 // Only lines (4, 8, 11, 12) should change
5404 cx.set_state(
5405 indoc! {"
5406 ·
5407 abc // No indentation
5408 abc // 1 space (< 3 so dont convert)
5409 abc // 2 spaces (< 3 so dont convert)
5410 « abc // 3 spaces (convert)ˇ»
5411 abc // 5 spaces (1 tab + 2 spaces)
5412 \t\t\tabc // Already tab indented
5413 \t abc // Tab followed by space
5414 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5415 \t\t \tabc // Mixed indentation
5416 \t \t \t \tabc // Mixed indentation
5417 \t \tˇ
5418 « abc \t // Only the leading spaces should be convertedˇ»
5419 "}
5420 .replace("·", "")
5421 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5422 );
5423 cx.update_editor(|e, window, cx| {
5424 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5425 });
5426 cx.assert_editor_state(
5427 indoc! {"
5428 ·
5429 abc // No indentation
5430 abc // 1 space (< 3 so dont convert)
5431 abc // 2 spaces (< 3 so dont convert)
5432 «\tabc // 3 spaces (convert)ˇ»
5433 abc // 5 spaces (1 tab + 2 spaces)
5434 \t\t\tabc // Already tab indented
5435 \t abc // Tab followed by space
5436 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5437 \t\t \tabc // Mixed indentation
5438 \t \t \t \tabc // Mixed indentation
5439 «\t\t\t
5440 \tabc \t // Only the leading spaces should be convertedˇ»
5441 "}
5442 .replace("·", "")
5443 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5444 );
5445
5446 // SINGLE SELECTION
5447 // Ln.1 "«" tests empty lines
5448 // Ln.11 tests just leading whitespace
5449 cx.set_state(indoc! {"
5450 «
5451 abc // No indentation
5452 abc // 1 space (< 3 so dont convert)
5453 abc // 2 spaces (< 3 so dont convert)
5454 abc // 3 spaces (convert)
5455 abc // 5 spaces (1 tab + 2 spaces)
5456 \t\t\tabc // Already tab indented
5457 \t abc // Tab followed by space
5458 \tabc // Space followed by tab (should be consumed due to tab)
5459 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5460 \t \t
5461 abc \t // Only the leading spaces should be convertedˇ»
5462 "});
5463 cx.update_editor(|e, window, cx| {
5464 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5465 });
5466 cx.assert_editor_state(indoc! {"
5467 «
5468 abc // No indentation
5469 abc // 1 space (< 3 so dont convert)
5470 abc // 2 spaces (< 3 so dont convert)
5471 \tabc // 3 spaces (convert)
5472 \t abc // 5 spaces (1 tab + 2 spaces)
5473 \t\t\tabc // Already tab indented
5474 \t abc // Tab followed by space
5475 \tabc // Space followed by tab (should be consumed due to tab)
5476 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5477 \t\t\t
5478 \tabc \t // Only the leading spaces should be convertedˇ»
5479 "});
5480}
5481
5482#[gpui::test]
5483async fn test_toggle_case(cx: &mut TestAppContext) {
5484 init_test(cx, |_| {});
5485
5486 let mut cx = EditorTestContext::new(cx).await;
5487
5488 // If all lower case -> upper case
5489 cx.set_state(indoc! {"
5490 «hello worldˇ»
5491 "});
5492 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5493 cx.assert_editor_state(indoc! {"
5494 «HELLO WORLDˇ»
5495 "});
5496
5497 // If all upper case -> lower case
5498 cx.set_state(indoc! {"
5499 «HELLO WORLDˇ»
5500 "});
5501 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5502 cx.assert_editor_state(indoc! {"
5503 «hello worldˇ»
5504 "});
5505
5506 // If any upper case characters are identified -> lower case
5507 // This matches JetBrains IDEs
5508 cx.set_state(indoc! {"
5509 «hEllo worldˇ»
5510 "});
5511 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5512 cx.assert_editor_state(indoc! {"
5513 «hello worldˇ»
5514 "});
5515}
5516
5517#[gpui::test]
5518async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5519 init_test(cx, |_| {});
5520
5521 let mut cx = EditorTestContext::new(cx).await;
5522
5523 cx.set_state(indoc! {"
5524 «implement-windows-supportˇ»
5525 "});
5526 cx.update_editor(|e, window, cx| {
5527 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5528 });
5529 cx.assert_editor_state(indoc! {"
5530 «Implement windows supportˇ»
5531 "});
5532}
5533
5534#[gpui::test]
5535async fn test_manipulate_text(cx: &mut TestAppContext) {
5536 init_test(cx, |_| {});
5537
5538 let mut cx = EditorTestContext::new(cx).await;
5539
5540 // Test convert_to_upper_case()
5541 cx.set_state(indoc! {"
5542 «hello worldˇ»
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «HELLO WORLDˇ»
5547 "});
5548
5549 // Test convert_to_lower_case()
5550 cx.set_state(indoc! {"
5551 «HELLO WORLDˇ»
5552 "});
5553 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5554 cx.assert_editor_state(indoc! {"
5555 «hello worldˇ»
5556 "});
5557
5558 // Test multiple line, single selection case
5559 cx.set_state(indoc! {"
5560 «The quick brown
5561 fox jumps over
5562 the lazy dogˇ»
5563 "});
5564 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5565 cx.assert_editor_state(indoc! {"
5566 «The Quick Brown
5567 Fox Jumps Over
5568 The Lazy Dogˇ»
5569 "});
5570
5571 // Test multiple line, single selection case
5572 cx.set_state(indoc! {"
5573 «The quick brown
5574 fox jumps over
5575 the lazy dogˇ»
5576 "});
5577 cx.update_editor(|e, window, cx| {
5578 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5579 });
5580 cx.assert_editor_state(indoc! {"
5581 «TheQuickBrown
5582 FoxJumpsOver
5583 TheLazyDogˇ»
5584 "});
5585
5586 // From here on out, test more complex cases of manipulate_text()
5587
5588 // Test no selection case - should affect words cursors are in
5589 // Cursor at beginning, middle, and end of word
5590 cx.set_state(indoc! {"
5591 ˇhello big beauˇtiful worldˇ
5592 "});
5593 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5594 cx.assert_editor_state(indoc! {"
5595 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5596 "});
5597
5598 // Test multiple selections on a single line and across multiple lines
5599 cx.set_state(indoc! {"
5600 «Theˇ» quick «brown
5601 foxˇ» jumps «overˇ»
5602 the «lazyˇ» dog
5603 "});
5604 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5605 cx.assert_editor_state(indoc! {"
5606 «THEˇ» quick «BROWN
5607 FOXˇ» jumps «OVERˇ»
5608 the «LAZYˇ» dog
5609 "});
5610
5611 // Test case where text length grows
5612 cx.set_state(indoc! {"
5613 «tschüߡ»
5614 "});
5615 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5616 cx.assert_editor_state(indoc! {"
5617 «TSCHÜSSˇ»
5618 "});
5619
5620 // Test to make sure we don't crash when text shrinks
5621 cx.set_state(indoc! {"
5622 aaa_bbbˇ
5623 "});
5624 cx.update_editor(|e, window, cx| {
5625 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5626 });
5627 cx.assert_editor_state(indoc! {"
5628 «aaaBbbˇ»
5629 "});
5630
5631 // Test to make sure we all aware of the fact that each word can grow and shrink
5632 // Final selections should be aware of this fact
5633 cx.set_state(indoc! {"
5634 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5635 "});
5636 cx.update_editor(|e, window, cx| {
5637 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5638 });
5639 cx.assert_editor_state(indoc! {"
5640 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5641 "});
5642
5643 cx.set_state(indoc! {"
5644 «hElLo, WoRld!ˇ»
5645 "});
5646 cx.update_editor(|e, window, cx| {
5647 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5648 });
5649 cx.assert_editor_state(indoc! {"
5650 «HeLlO, wOrLD!ˇ»
5651 "});
5652
5653 // Test selections with `line_mode() = true`.
5654 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5655 cx.set_state(indoc! {"
5656 «The quick brown
5657 fox jumps over
5658 tˇ»he lazy dog
5659 "});
5660 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5661 cx.assert_editor_state(indoc! {"
5662 «THE QUICK BROWN
5663 FOX JUMPS OVER
5664 THE LAZY DOGˇ»
5665 "});
5666}
5667
5668#[gpui::test]
5669fn test_duplicate_line(cx: &mut TestAppContext) {
5670 init_test(cx, |_| {});
5671
5672 let editor = cx.add_window(|window, cx| {
5673 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5674 build_editor(buffer, window, cx)
5675 });
5676 _ = editor.update(cx, |editor, window, cx| {
5677 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5678 s.select_display_ranges([
5679 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5681 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5682 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5683 ])
5684 });
5685 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5686 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5687 assert_eq!(
5688 editor.selections.display_ranges(cx),
5689 vec![
5690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5691 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5692 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5693 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5694 ]
5695 );
5696 });
5697
5698 let editor = cx.add_window(|window, cx| {
5699 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5700 build_editor(buffer, window, cx)
5701 });
5702 _ = editor.update(cx, |editor, window, cx| {
5703 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5704 s.select_display_ranges([
5705 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5706 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5707 ])
5708 });
5709 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5710 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5711 assert_eq!(
5712 editor.selections.display_ranges(cx),
5713 vec![
5714 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5715 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5716 ]
5717 );
5718 });
5719
5720 // With `duplicate_line_up` the selections move to the duplicated lines,
5721 // which are inserted above the original lines
5722 let editor = cx.add_window(|window, cx| {
5723 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5724 build_editor(buffer, window, cx)
5725 });
5726 _ = editor.update(cx, |editor, window, cx| {
5727 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5728 s.select_display_ranges([
5729 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5730 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5731 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5732 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5733 ])
5734 });
5735 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5736 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5737 assert_eq!(
5738 editor.selections.display_ranges(cx),
5739 vec![
5740 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5741 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5742 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5743 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5744 ]
5745 );
5746 });
5747
5748 let editor = cx.add_window(|window, cx| {
5749 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5750 build_editor(buffer, window, cx)
5751 });
5752 _ = editor.update(cx, |editor, window, cx| {
5753 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5754 s.select_display_ranges([
5755 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5756 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5757 ])
5758 });
5759 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5760 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5761 assert_eq!(
5762 editor.selections.display_ranges(cx),
5763 vec![
5764 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5765 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5766 ]
5767 );
5768 });
5769
5770 let editor = cx.add_window(|window, cx| {
5771 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5772 build_editor(buffer, window, cx)
5773 });
5774 _ = editor.update(cx, |editor, window, cx| {
5775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5776 s.select_display_ranges([
5777 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5778 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5779 ])
5780 });
5781 editor.duplicate_selection(&DuplicateSelection, window, cx);
5782 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5783 assert_eq!(
5784 editor.selections.display_ranges(cx),
5785 vec![
5786 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5787 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5788 ]
5789 );
5790 });
5791}
5792
5793#[gpui::test]
5794fn test_move_line_up_down(cx: &mut TestAppContext) {
5795 init_test(cx, |_| {});
5796
5797 let editor = cx.add_window(|window, cx| {
5798 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5799 build_editor(buffer, window, cx)
5800 });
5801 _ = editor.update(cx, |editor, window, cx| {
5802 editor.fold_creases(
5803 vec![
5804 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5805 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5806 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5807 ],
5808 true,
5809 window,
5810 cx,
5811 );
5812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5813 s.select_display_ranges([
5814 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5815 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5816 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5817 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5818 ])
5819 });
5820 assert_eq!(
5821 editor.display_text(cx),
5822 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5823 );
5824
5825 editor.move_line_up(&MoveLineUp, window, cx);
5826 assert_eq!(
5827 editor.display_text(cx),
5828 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5829 );
5830 assert_eq!(
5831 editor.selections.display_ranges(cx),
5832 vec![
5833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5834 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5835 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5836 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5837 ]
5838 );
5839 });
5840
5841 _ = editor.update(cx, |editor, window, cx| {
5842 editor.move_line_down(&MoveLineDown, window, cx);
5843 assert_eq!(
5844 editor.display_text(cx),
5845 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5846 );
5847 assert_eq!(
5848 editor.selections.display_ranges(cx),
5849 vec![
5850 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5851 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5852 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5853 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5854 ]
5855 );
5856 });
5857
5858 _ = editor.update(cx, |editor, window, cx| {
5859 editor.move_line_down(&MoveLineDown, window, cx);
5860 assert_eq!(
5861 editor.display_text(cx),
5862 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5863 );
5864 assert_eq!(
5865 editor.selections.display_ranges(cx),
5866 vec![
5867 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5868 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5869 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5870 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5871 ]
5872 );
5873 });
5874
5875 _ = editor.update(cx, |editor, window, cx| {
5876 editor.move_line_up(&MoveLineUp, window, cx);
5877 assert_eq!(
5878 editor.display_text(cx),
5879 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5880 );
5881 assert_eq!(
5882 editor.selections.display_ranges(cx),
5883 vec![
5884 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5885 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5886 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5887 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5888 ]
5889 );
5890 });
5891}
5892
5893#[gpui::test]
5894fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5895 init_test(cx, |_| {});
5896 let editor = cx.add_window(|window, cx| {
5897 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5898 build_editor(buffer, window, cx)
5899 });
5900 _ = editor.update(cx, |editor, window, cx| {
5901 editor.fold_creases(
5902 vec![Crease::simple(
5903 Point::new(6, 4)..Point::new(7, 4),
5904 FoldPlaceholder::test(),
5905 )],
5906 true,
5907 window,
5908 cx,
5909 );
5910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5911 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5912 });
5913 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5914 editor.move_line_up(&MoveLineUp, window, cx);
5915 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5916 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5917 });
5918}
5919
5920#[gpui::test]
5921fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5922 init_test(cx, |_| {});
5923
5924 let editor = cx.add_window(|window, cx| {
5925 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5926 build_editor(buffer, window, cx)
5927 });
5928 _ = editor.update(cx, |editor, window, cx| {
5929 let snapshot = editor.buffer.read(cx).snapshot(cx);
5930 editor.insert_blocks(
5931 [BlockProperties {
5932 style: BlockStyle::Fixed,
5933 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5934 height: Some(1),
5935 render: Arc::new(|_| div().into_any()),
5936 priority: 0,
5937 }],
5938 Some(Autoscroll::fit()),
5939 cx,
5940 );
5941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5942 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5943 });
5944 editor.move_line_down(&MoveLineDown, window, cx);
5945 });
5946}
5947
5948#[gpui::test]
5949async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5950 init_test(cx, |_| {});
5951
5952 let mut cx = EditorTestContext::new(cx).await;
5953 cx.set_state(
5954 &"
5955 ˇzero
5956 one
5957 two
5958 three
5959 four
5960 five
5961 "
5962 .unindent(),
5963 );
5964
5965 // Create a four-line block that replaces three lines of text.
5966 cx.update_editor(|editor, window, cx| {
5967 let snapshot = editor.snapshot(window, cx);
5968 let snapshot = &snapshot.buffer_snapshot();
5969 let placement = BlockPlacement::Replace(
5970 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5971 );
5972 editor.insert_blocks(
5973 [BlockProperties {
5974 placement,
5975 height: Some(4),
5976 style: BlockStyle::Sticky,
5977 render: Arc::new(|_| gpui::div().into_any_element()),
5978 priority: 0,
5979 }],
5980 None,
5981 cx,
5982 );
5983 });
5984
5985 // Move down so that the cursor touches the block.
5986 cx.update_editor(|editor, window, cx| {
5987 editor.move_down(&Default::default(), window, cx);
5988 });
5989 cx.assert_editor_state(
5990 &"
5991 zero
5992 «one
5993 two
5994 threeˇ»
5995 four
5996 five
5997 "
5998 .unindent(),
5999 );
6000
6001 // Move down past the block.
6002 cx.update_editor(|editor, window, cx| {
6003 editor.move_down(&Default::default(), window, cx);
6004 });
6005 cx.assert_editor_state(
6006 &"
6007 zero
6008 one
6009 two
6010 three
6011 ˇfour
6012 five
6013 "
6014 .unindent(),
6015 );
6016}
6017
6018#[gpui::test]
6019fn test_transpose(cx: &mut TestAppContext) {
6020 init_test(cx, |_| {});
6021
6022 _ = cx.add_window(|window, cx| {
6023 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6024 editor.set_style(EditorStyle::default(), window, cx);
6025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6026 s.select_ranges([1..1])
6027 });
6028 editor.transpose(&Default::default(), window, cx);
6029 assert_eq!(editor.text(cx), "bac");
6030 assert_eq!(
6031 editor.selections.ranges(&editor.display_snapshot(cx)),
6032 [2..2]
6033 );
6034
6035 editor.transpose(&Default::default(), window, cx);
6036 assert_eq!(editor.text(cx), "bca");
6037 assert_eq!(
6038 editor.selections.ranges(&editor.display_snapshot(cx)),
6039 [3..3]
6040 );
6041
6042 editor.transpose(&Default::default(), window, cx);
6043 assert_eq!(editor.text(cx), "bac");
6044 assert_eq!(
6045 editor.selections.ranges(&editor.display_snapshot(cx)),
6046 [3..3]
6047 );
6048
6049 editor
6050 });
6051
6052 _ = cx.add_window(|window, cx| {
6053 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6054 editor.set_style(EditorStyle::default(), window, cx);
6055 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6056 s.select_ranges([3..3])
6057 });
6058 editor.transpose(&Default::default(), window, cx);
6059 assert_eq!(editor.text(cx), "acb\nde");
6060 assert_eq!(
6061 editor.selections.ranges(&editor.display_snapshot(cx)),
6062 [3..3]
6063 );
6064
6065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6066 s.select_ranges([4..4])
6067 });
6068 editor.transpose(&Default::default(), window, cx);
6069 assert_eq!(editor.text(cx), "acbd\ne");
6070 assert_eq!(
6071 editor.selections.ranges(&editor.display_snapshot(cx)),
6072 [5..5]
6073 );
6074
6075 editor.transpose(&Default::default(), window, cx);
6076 assert_eq!(editor.text(cx), "acbde\n");
6077 assert_eq!(
6078 editor.selections.ranges(&editor.display_snapshot(cx)),
6079 [6..6]
6080 );
6081
6082 editor.transpose(&Default::default(), window, cx);
6083 assert_eq!(editor.text(cx), "acbd\ne");
6084 assert_eq!(
6085 editor.selections.ranges(&editor.display_snapshot(cx)),
6086 [6..6]
6087 );
6088
6089 editor
6090 });
6091
6092 _ = cx.add_window(|window, cx| {
6093 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6094 editor.set_style(EditorStyle::default(), window, cx);
6095 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6096 s.select_ranges([1..1, 2..2, 4..4])
6097 });
6098 editor.transpose(&Default::default(), window, cx);
6099 assert_eq!(editor.text(cx), "bacd\ne");
6100 assert_eq!(
6101 editor.selections.ranges(&editor.display_snapshot(cx)),
6102 [2..2, 3..3, 5..5]
6103 );
6104
6105 editor.transpose(&Default::default(), window, cx);
6106 assert_eq!(editor.text(cx), "bcade\n");
6107 assert_eq!(
6108 editor.selections.ranges(&editor.display_snapshot(cx)),
6109 [3..3, 4..4, 6..6]
6110 );
6111
6112 editor.transpose(&Default::default(), window, cx);
6113 assert_eq!(editor.text(cx), "bcda\ne");
6114 assert_eq!(
6115 editor.selections.ranges(&editor.display_snapshot(cx)),
6116 [4..4, 6..6]
6117 );
6118
6119 editor.transpose(&Default::default(), window, cx);
6120 assert_eq!(editor.text(cx), "bcade\n");
6121 assert_eq!(
6122 editor.selections.ranges(&editor.display_snapshot(cx)),
6123 [4..4, 6..6]
6124 );
6125
6126 editor.transpose(&Default::default(), window, cx);
6127 assert_eq!(editor.text(cx), "bcaed\n");
6128 assert_eq!(
6129 editor.selections.ranges(&editor.display_snapshot(cx)),
6130 [5..5, 6..6]
6131 );
6132
6133 editor
6134 });
6135
6136 _ = cx.add_window(|window, cx| {
6137 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6138 editor.set_style(EditorStyle::default(), window, cx);
6139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6140 s.select_ranges([4..4])
6141 });
6142 editor.transpose(&Default::default(), window, cx);
6143 assert_eq!(editor.text(cx), "🏀🍐✋");
6144 assert_eq!(
6145 editor.selections.ranges(&editor.display_snapshot(cx)),
6146 [8..8]
6147 );
6148
6149 editor.transpose(&Default::default(), window, cx);
6150 assert_eq!(editor.text(cx), "🏀✋🍐");
6151 assert_eq!(
6152 editor.selections.ranges(&editor.display_snapshot(cx)),
6153 [11..11]
6154 );
6155
6156 editor.transpose(&Default::default(), window, cx);
6157 assert_eq!(editor.text(cx), "🏀🍐✋");
6158 assert_eq!(
6159 editor.selections.ranges(&editor.display_snapshot(cx)),
6160 [11..11]
6161 );
6162
6163 editor
6164 });
6165}
6166
6167#[gpui::test]
6168async fn test_rewrap(cx: &mut TestAppContext) {
6169 init_test(cx, |settings| {
6170 settings.languages.0.extend([
6171 (
6172 "Markdown".into(),
6173 LanguageSettingsContent {
6174 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6175 preferred_line_length: Some(40),
6176 ..Default::default()
6177 },
6178 ),
6179 (
6180 "Plain Text".into(),
6181 LanguageSettingsContent {
6182 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6183 preferred_line_length: Some(40),
6184 ..Default::default()
6185 },
6186 ),
6187 (
6188 "C++".into(),
6189 LanguageSettingsContent {
6190 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6191 preferred_line_length: Some(40),
6192 ..Default::default()
6193 },
6194 ),
6195 (
6196 "Python".into(),
6197 LanguageSettingsContent {
6198 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6199 preferred_line_length: Some(40),
6200 ..Default::default()
6201 },
6202 ),
6203 (
6204 "Rust".into(),
6205 LanguageSettingsContent {
6206 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6207 preferred_line_length: Some(40),
6208 ..Default::default()
6209 },
6210 ),
6211 ])
6212 });
6213
6214 let mut cx = EditorTestContext::new(cx).await;
6215
6216 let cpp_language = Arc::new(Language::new(
6217 LanguageConfig {
6218 name: "C++".into(),
6219 line_comments: vec!["// ".into()],
6220 ..LanguageConfig::default()
6221 },
6222 None,
6223 ));
6224 let python_language = Arc::new(Language::new(
6225 LanguageConfig {
6226 name: "Python".into(),
6227 line_comments: vec!["# ".into()],
6228 ..LanguageConfig::default()
6229 },
6230 None,
6231 ));
6232 let markdown_language = Arc::new(Language::new(
6233 LanguageConfig {
6234 name: "Markdown".into(),
6235 rewrap_prefixes: vec![
6236 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6237 regex::Regex::new("[-*+]\\s+").unwrap(),
6238 ],
6239 ..LanguageConfig::default()
6240 },
6241 None,
6242 ));
6243 let rust_language = Arc::new(
6244 Language::new(
6245 LanguageConfig {
6246 name: "Rust".into(),
6247 line_comments: vec!["// ".into(), "/// ".into()],
6248 ..LanguageConfig::default()
6249 },
6250 Some(tree_sitter_rust::LANGUAGE.into()),
6251 )
6252 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6253 .unwrap(),
6254 );
6255
6256 let plaintext_language = Arc::new(Language::new(
6257 LanguageConfig {
6258 name: "Plain Text".into(),
6259 ..LanguageConfig::default()
6260 },
6261 None,
6262 ));
6263
6264 // Test basic rewrapping of a long line with a cursor
6265 assert_rewrap(
6266 indoc! {"
6267 // ˇThis is a long comment that needs to be wrapped.
6268 "},
6269 indoc! {"
6270 // ˇThis is a long comment that needs to
6271 // be wrapped.
6272 "},
6273 cpp_language.clone(),
6274 &mut cx,
6275 );
6276
6277 // Test rewrapping a full selection
6278 assert_rewrap(
6279 indoc! {"
6280 «// This selected long comment needs to be wrapped.ˇ»"
6281 },
6282 indoc! {"
6283 «// This selected long comment needs to
6284 // be wrapped.ˇ»"
6285 },
6286 cpp_language.clone(),
6287 &mut cx,
6288 );
6289
6290 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6291 assert_rewrap(
6292 indoc! {"
6293 // ˇThis is the first line.
6294 // Thisˇ is the second line.
6295 // This is the thirdˇ line, all part of one paragraph.
6296 "},
6297 indoc! {"
6298 // ˇThis is the first line. Thisˇ is the
6299 // second line. This is the thirdˇ line,
6300 // all part of one paragraph.
6301 "},
6302 cpp_language.clone(),
6303 &mut cx,
6304 );
6305
6306 // Test multiple cursors in different paragraphs trigger separate rewraps
6307 assert_rewrap(
6308 indoc! {"
6309 // ˇThis is the first paragraph, first line.
6310 // ˇThis is the first paragraph, second line.
6311
6312 // ˇThis is the second paragraph, first line.
6313 // ˇThis is the second paragraph, second line.
6314 "},
6315 indoc! {"
6316 // ˇThis is the first paragraph, first
6317 // line. ˇThis is the first paragraph,
6318 // second line.
6319
6320 // ˇThis is the second paragraph, first
6321 // line. ˇThis is the second paragraph,
6322 // second line.
6323 "},
6324 cpp_language.clone(),
6325 &mut cx,
6326 );
6327
6328 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6329 assert_rewrap(
6330 indoc! {"
6331 «// A regular long long comment to be wrapped.
6332 /// A documentation long comment to be wrapped.ˇ»
6333 "},
6334 indoc! {"
6335 «// A regular long long comment to be
6336 // wrapped.
6337 /// A documentation long comment to be
6338 /// wrapped.ˇ»
6339 "},
6340 rust_language.clone(),
6341 &mut cx,
6342 );
6343
6344 // Test that change in indentation level trigger seperate rewraps
6345 assert_rewrap(
6346 indoc! {"
6347 fn foo() {
6348 «// This is a long comment at the base indent.
6349 // This is a long comment at the next indent.ˇ»
6350 }
6351 "},
6352 indoc! {"
6353 fn foo() {
6354 «// This is a long comment at the
6355 // base indent.
6356 // This is a long comment at the
6357 // next indent.ˇ»
6358 }
6359 "},
6360 rust_language.clone(),
6361 &mut cx,
6362 );
6363
6364 // Test that different comment prefix characters (e.g., '#') are handled correctly
6365 assert_rewrap(
6366 indoc! {"
6367 # ˇThis is a long comment using a pound sign.
6368 "},
6369 indoc! {"
6370 # ˇThis is a long comment using a pound
6371 # sign.
6372 "},
6373 python_language,
6374 &mut cx,
6375 );
6376
6377 // Test rewrapping only affects comments, not code even when selected
6378 assert_rewrap(
6379 indoc! {"
6380 «/// This doc comment is long and should be wrapped.
6381 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6382 "},
6383 indoc! {"
6384 «/// This doc comment is long and should
6385 /// be wrapped.
6386 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6387 "},
6388 rust_language.clone(),
6389 &mut cx,
6390 );
6391
6392 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6393 assert_rewrap(
6394 indoc! {"
6395 # Header
6396
6397 A long long long line of markdown text to wrap.ˇ
6398 "},
6399 indoc! {"
6400 # Header
6401
6402 A long long long line of markdown text
6403 to wrap.ˇ
6404 "},
6405 markdown_language.clone(),
6406 &mut cx,
6407 );
6408
6409 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6410 assert_rewrap(
6411 indoc! {"
6412 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6413 2. This is a numbered list item that is very long and needs to be wrapped properly.
6414 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6415 "},
6416 indoc! {"
6417 «1. This is a numbered list item that is
6418 very long and needs to be wrapped
6419 properly.
6420 2. This is a numbered list item that is
6421 very long and needs to be wrapped
6422 properly.
6423 - This is an unordered list item that is
6424 also very long and should not merge
6425 with the numbered item.ˇ»
6426 "},
6427 markdown_language.clone(),
6428 &mut cx,
6429 );
6430
6431 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6432 assert_rewrap(
6433 indoc! {"
6434 «1. This is a numbered list item that is
6435 very long and needs to be wrapped
6436 properly.
6437 2. This is a numbered list item that is
6438 very long and needs to be wrapped
6439 properly.
6440 - This is an unordered list item that is
6441 also very long and should not merge with
6442 the numbered item.ˇ»
6443 "},
6444 indoc! {"
6445 «1. This is a numbered list item that is
6446 very long and needs to be wrapped
6447 properly.
6448 2. This is a numbered list item that is
6449 very long and needs to be wrapped
6450 properly.
6451 - This is an unordered list item that is
6452 also very long and should not merge
6453 with the numbered item.ˇ»
6454 "},
6455 markdown_language.clone(),
6456 &mut cx,
6457 );
6458
6459 // Test that rewrapping maintain indents even when they already exists.
6460 assert_rewrap(
6461 indoc! {"
6462 «1. This is a numbered list
6463 item that is very long and needs to be wrapped properly.
6464 2. This is a numbered list
6465 item that is very long and needs to be wrapped properly.
6466 - This is an unordered list item that is also very long and
6467 should not merge with the numbered item.ˇ»
6468 "},
6469 indoc! {"
6470 «1. This is a numbered list item that is
6471 very long and needs to be wrapped
6472 properly.
6473 2. This is a numbered list item that is
6474 very long and needs to be wrapped
6475 properly.
6476 - This is an unordered list item that is
6477 also very long and should not merge
6478 with the numbered item.ˇ»
6479 "},
6480 markdown_language,
6481 &mut cx,
6482 );
6483
6484 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6485 assert_rewrap(
6486 indoc! {"
6487 ˇThis is a very long line of plain text that will be wrapped.
6488 "},
6489 indoc! {"
6490 ˇThis is a very long line of plain text
6491 that will be wrapped.
6492 "},
6493 plaintext_language.clone(),
6494 &mut cx,
6495 );
6496
6497 // Test that non-commented code acts as a paragraph boundary within a selection
6498 assert_rewrap(
6499 indoc! {"
6500 «// This is the first long comment block to be wrapped.
6501 fn my_func(a: u32);
6502 // This is the second long comment block to be wrapped.ˇ»
6503 "},
6504 indoc! {"
6505 «// This is the first long comment block
6506 // to be wrapped.
6507 fn my_func(a: u32);
6508 // This is the second long comment block
6509 // to be wrapped.ˇ»
6510 "},
6511 rust_language,
6512 &mut cx,
6513 );
6514
6515 // Test rewrapping multiple selections, including ones with blank lines or tabs
6516 assert_rewrap(
6517 indoc! {"
6518 «ˇThis is a very long line that will be wrapped.
6519
6520 This is another paragraph in the same selection.»
6521
6522 «\tThis is a very long indented line that will be wrapped.ˇ»
6523 "},
6524 indoc! {"
6525 «ˇThis is a very long line that will be
6526 wrapped.
6527
6528 This is another paragraph in the same
6529 selection.»
6530
6531 «\tThis is a very long indented line
6532 \tthat will be wrapped.ˇ»
6533 "},
6534 plaintext_language,
6535 &mut cx,
6536 );
6537
6538 // Test that an empty comment line acts as a paragraph boundary
6539 assert_rewrap(
6540 indoc! {"
6541 // ˇThis is a long comment that will be wrapped.
6542 //
6543 // And this is another long comment that will also be wrapped.ˇ
6544 "},
6545 indoc! {"
6546 // ˇThis is a long comment that will be
6547 // wrapped.
6548 //
6549 // And this is another long comment that
6550 // will also be wrapped.ˇ
6551 "},
6552 cpp_language,
6553 &mut cx,
6554 );
6555
6556 #[track_caller]
6557 fn assert_rewrap(
6558 unwrapped_text: &str,
6559 wrapped_text: &str,
6560 language: Arc<Language>,
6561 cx: &mut EditorTestContext,
6562 ) {
6563 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6564 cx.set_state(unwrapped_text);
6565 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6566 cx.assert_editor_state(wrapped_text);
6567 }
6568}
6569
6570#[gpui::test]
6571async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6572 init_test(cx, |settings| {
6573 settings.languages.0.extend([(
6574 "Rust".into(),
6575 LanguageSettingsContent {
6576 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6577 preferred_line_length: Some(40),
6578 ..Default::default()
6579 },
6580 )])
6581 });
6582
6583 let mut cx = EditorTestContext::new(cx).await;
6584
6585 let rust_lang = Arc::new(
6586 Language::new(
6587 LanguageConfig {
6588 name: "Rust".into(),
6589 line_comments: vec!["// ".into()],
6590 block_comment: Some(BlockCommentConfig {
6591 start: "/*".into(),
6592 end: "*/".into(),
6593 prefix: "* ".into(),
6594 tab_size: 1,
6595 }),
6596 documentation_comment: Some(BlockCommentConfig {
6597 start: "/**".into(),
6598 end: "*/".into(),
6599 prefix: "* ".into(),
6600 tab_size: 1,
6601 }),
6602
6603 ..LanguageConfig::default()
6604 },
6605 Some(tree_sitter_rust::LANGUAGE.into()),
6606 )
6607 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6608 .unwrap(),
6609 );
6610
6611 // regular block comment
6612 assert_rewrap(
6613 indoc! {"
6614 /*
6615 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6616 */
6617 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6618 "},
6619 indoc! {"
6620 /*
6621 *ˇ Lorem ipsum dolor sit amet,
6622 * consectetur adipiscing elit.
6623 */
6624 /*
6625 *ˇ Lorem ipsum dolor sit amet,
6626 * consectetur adipiscing elit.
6627 */
6628 "},
6629 rust_lang.clone(),
6630 &mut cx,
6631 );
6632
6633 // indent is respected
6634 assert_rewrap(
6635 indoc! {"
6636 {}
6637 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6638 "},
6639 indoc! {"
6640 {}
6641 /*
6642 *ˇ Lorem ipsum dolor sit amet,
6643 * consectetur adipiscing elit.
6644 */
6645 "},
6646 rust_lang.clone(),
6647 &mut cx,
6648 );
6649
6650 // short block comments with inline delimiters
6651 assert_rewrap(
6652 indoc! {"
6653 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6654 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6655 */
6656 /*
6657 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6658 "},
6659 indoc! {"
6660 /*
6661 *ˇ Lorem ipsum dolor sit amet,
6662 * consectetur adipiscing elit.
6663 */
6664 /*
6665 *ˇ Lorem ipsum dolor sit amet,
6666 * consectetur adipiscing elit.
6667 */
6668 /*
6669 *ˇ Lorem ipsum dolor sit amet,
6670 * consectetur adipiscing elit.
6671 */
6672 "},
6673 rust_lang.clone(),
6674 &mut cx,
6675 );
6676
6677 // multiline block comment with inline start/end delimiters
6678 assert_rewrap(
6679 indoc! {"
6680 /*ˇ Lorem ipsum dolor sit amet,
6681 * consectetur adipiscing elit. */
6682 "},
6683 indoc! {"
6684 /*
6685 *ˇ Lorem ipsum dolor sit amet,
6686 * consectetur adipiscing elit.
6687 */
6688 "},
6689 rust_lang.clone(),
6690 &mut cx,
6691 );
6692
6693 // block comment rewrap still respects paragraph bounds
6694 assert_rewrap(
6695 indoc! {"
6696 /*
6697 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6698 *
6699 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6700 */
6701 "},
6702 indoc! {"
6703 /*
6704 *ˇ Lorem ipsum dolor sit amet,
6705 * consectetur adipiscing elit.
6706 *
6707 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6708 */
6709 "},
6710 rust_lang.clone(),
6711 &mut cx,
6712 );
6713
6714 // documentation comments
6715 assert_rewrap(
6716 indoc! {"
6717 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6718 /**
6719 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6720 */
6721 "},
6722 indoc! {"
6723 /**
6724 *ˇ Lorem ipsum dolor sit amet,
6725 * consectetur adipiscing elit.
6726 */
6727 /**
6728 *ˇ Lorem ipsum dolor sit amet,
6729 * consectetur adipiscing elit.
6730 */
6731 "},
6732 rust_lang.clone(),
6733 &mut cx,
6734 );
6735
6736 // different, adjacent comments
6737 assert_rewrap(
6738 indoc! {"
6739 /**
6740 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6741 */
6742 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6743 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6744 "},
6745 indoc! {"
6746 /**
6747 *ˇ Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit.
6749 */
6750 /*
6751 *ˇ Lorem ipsum dolor sit amet,
6752 * consectetur adipiscing elit.
6753 */
6754 //ˇ Lorem ipsum dolor sit amet,
6755 // consectetur adipiscing elit.
6756 "},
6757 rust_lang.clone(),
6758 &mut cx,
6759 );
6760
6761 // selection w/ single short block comment
6762 assert_rewrap(
6763 indoc! {"
6764 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6765 "},
6766 indoc! {"
6767 «/*
6768 * Lorem ipsum dolor sit amet,
6769 * consectetur adipiscing elit.
6770 */ˇ»
6771 "},
6772 rust_lang.clone(),
6773 &mut cx,
6774 );
6775
6776 // rewrapping a single comment w/ abutting comments
6777 assert_rewrap(
6778 indoc! {"
6779 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6780 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6781 "},
6782 indoc! {"
6783 /*
6784 * ˇLorem ipsum dolor sit amet,
6785 * consectetur adipiscing elit.
6786 */
6787 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6788 "},
6789 rust_lang.clone(),
6790 &mut cx,
6791 );
6792
6793 // selection w/ non-abutting short block comments
6794 assert_rewrap(
6795 indoc! {"
6796 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6797
6798 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6799 "},
6800 indoc! {"
6801 «/*
6802 * Lorem ipsum dolor sit amet,
6803 * consectetur adipiscing elit.
6804 */
6805
6806 /*
6807 * Lorem ipsum dolor sit amet,
6808 * consectetur adipiscing elit.
6809 */ˇ»
6810 "},
6811 rust_lang.clone(),
6812 &mut cx,
6813 );
6814
6815 // selection of multiline block comments
6816 assert_rewrap(
6817 indoc! {"
6818 «/* Lorem ipsum dolor sit amet,
6819 * consectetur adipiscing elit. */ˇ»
6820 "},
6821 indoc! {"
6822 «/*
6823 * Lorem ipsum dolor sit amet,
6824 * consectetur adipiscing elit.
6825 */ˇ»
6826 "},
6827 rust_lang.clone(),
6828 &mut cx,
6829 );
6830
6831 // partial selection of multiline block comments
6832 assert_rewrap(
6833 indoc! {"
6834 «/* Lorem ipsum dolor sit amet,ˇ»
6835 * consectetur adipiscing elit. */
6836 /* Lorem ipsum dolor sit amet,
6837 «* consectetur adipiscing elit. */ˇ»
6838 "},
6839 indoc! {"
6840 «/*
6841 * Lorem ipsum dolor sit amet,ˇ»
6842 * consectetur adipiscing elit. */
6843 /* Lorem ipsum dolor sit amet,
6844 «* consectetur adipiscing elit.
6845 */ˇ»
6846 "},
6847 rust_lang.clone(),
6848 &mut cx,
6849 );
6850
6851 // selection w/ abutting short block comments
6852 // TODO: should not be combined; should rewrap as 2 comments
6853 assert_rewrap(
6854 indoc! {"
6855 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6856 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6857 "},
6858 // desired behavior:
6859 // indoc! {"
6860 // «/*
6861 // * Lorem ipsum dolor sit amet,
6862 // * consectetur adipiscing elit.
6863 // */
6864 // /*
6865 // * Lorem ipsum dolor sit amet,
6866 // * consectetur adipiscing elit.
6867 // */ˇ»
6868 // "},
6869 // actual behaviour:
6870 indoc! {"
6871 «/*
6872 * Lorem ipsum dolor sit amet,
6873 * consectetur adipiscing elit. Lorem
6874 * ipsum dolor sit amet, consectetur
6875 * adipiscing elit.
6876 */ˇ»
6877 "},
6878 rust_lang.clone(),
6879 &mut cx,
6880 );
6881
6882 // TODO: same as above, but with delimiters on separate line
6883 // assert_rewrap(
6884 // indoc! {"
6885 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6886 // */
6887 // /*
6888 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6889 // "},
6890 // // desired:
6891 // // indoc! {"
6892 // // «/*
6893 // // * Lorem ipsum dolor sit amet,
6894 // // * consectetur adipiscing elit.
6895 // // */
6896 // // /*
6897 // // * Lorem ipsum dolor sit amet,
6898 // // * consectetur adipiscing elit.
6899 // // */ˇ»
6900 // // "},
6901 // // actual: (but with trailing w/s on the empty lines)
6902 // indoc! {"
6903 // «/*
6904 // * Lorem ipsum dolor sit amet,
6905 // * consectetur adipiscing elit.
6906 // *
6907 // */
6908 // /*
6909 // *
6910 // * Lorem ipsum dolor sit amet,
6911 // * consectetur adipiscing elit.
6912 // */ˇ»
6913 // "},
6914 // rust_lang.clone(),
6915 // &mut cx,
6916 // );
6917
6918 // TODO these are unhandled edge cases; not correct, just documenting known issues
6919 assert_rewrap(
6920 indoc! {"
6921 /*
6922 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6923 */
6924 /*
6925 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6926 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6927 "},
6928 // desired:
6929 // indoc! {"
6930 // /*
6931 // *ˇ Lorem ipsum dolor sit amet,
6932 // * consectetur adipiscing elit.
6933 // */
6934 // /*
6935 // *ˇ Lorem ipsum dolor sit amet,
6936 // * consectetur adipiscing elit.
6937 // */
6938 // /*
6939 // *ˇ Lorem ipsum dolor sit amet
6940 // */ /* consectetur adipiscing elit. */
6941 // "},
6942 // actual:
6943 indoc! {"
6944 /*
6945 //ˇ Lorem ipsum dolor sit amet,
6946 // consectetur adipiscing elit.
6947 */
6948 /*
6949 * //ˇ Lorem ipsum dolor sit amet,
6950 * consectetur adipiscing elit.
6951 */
6952 /*
6953 *ˇ Lorem ipsum dolor sit amet */ /*
6954 * consectetur adipiscing elit.
6955 */
6956 "},
6957 rust_lang,
6958 &mut cx,
6959 );
6960
6961 #[track_caller]
6962 fn assert_rewrap(
6963 unwrapped_text: &str,
6964 wrapped_text: &str,
6965 language: Arc<Language>,
6966 cx: &mut EditorTestContext,
6967 ) {
6968 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6969 cx.set_state(unwrapped_text);
6970 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6971 cx.assert_editor_state(wrapped_text);
6972 }
6973}
6974
6975#[gpui::test]
6976async fn test_hard_wrap(cx: &mut TestAppContext) {
6977 init_test(cx, |_| {});
6978 let mut cx = EditorTestContext::new(cx).await;
6979
6980 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6981 cx.update_editor(|editor, _, cx| {
6982 editor.set_hard_wrap(Some(14), cx);
6983 });
6984
6985 cx.set_state(indoc!(
6986 "
6987 one two three ˇ
6988 "
6989 ));
6990 cx.simulate_input("four");
6991 cx.run_until_parked();
6992
6993 cx.assert_editor_state(indoc!(
6994 "
6995 one two three
6996 fourˇ
6997 "
6998 ));
6999
7000 cx.update_editor(|editor, window, cx| {
7001 editor.newline(&Default::default(), window, cx);
7002 });
7003 cx.run_until_parked();
7004 cx.assert_editor_state(indoc!(
7005 "
7006 one two three
7007 four
7008 ˇ
7009 "
7010 ));
7011
7012 cx.simulate_input("five");
7013 cx.run_until_parked();
7014 cx.assert_editor_state(indoc!(
7015 "
7016 one two three
7017 four
7018 fiveˇ
7019 "
7020 ));
7021
7022 cx.update_editor(|editor, window, cx| {
7023 editor.newline(&Default::default(), window, cx);
7024 });
7025 cx.run_until_parked();
7026 cx.simulate_input("# ");
7027 cx.run_until_parked();
7028 cx.assert_editor_state(indoc!(
7029 "
7030 one two three
7031 four
7032 five
7033 # ˇ
7034 "
7035 ));
7036
7037 cx.update_editor(|editor, window, cx| {
7038 editor.newline(&Default::default(), window, cx);
7039 });
7040 cx.run_until_parked();
7041 cx.assert_editor_state(indoc!(
7042 "
7043 one two three
7044 four
7045 five
7046 #\x20
7047 #ˇ
7048 "
7049 ));
7050
7051 cx.simulate_input(" 6");
7052 cx.run_until_parked();
7053 cx.assert_editor_state(indoc!(
7054 "
7055 one two three
7056 four
7057 five
7058 #
7059 # 6ˇ
7060 "
7061 ));
7062}
7063
7064#[gpui::test]
7065async fn test_cut_line_ends(cx: &mut TestAppContext) {
7066 init_test(cx, |_| {});
7067
7068 let mut cx = EditorTestContext::new(cx).await;
7069
7070 cx.set_state(indoc! {"The quick brownˇ"});
7071 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7072 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7073
7074 cx.set_state(indoc! {"The emacs foxˇ"});
7075 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7076 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7077
7078 cx.set_state(indoc! {"
7079 The quick« brownˇ»
7080 fox jumps overˇ
7081 the lazy dog"});
7082 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7083 cx.assert_editor_state(indoc! {"
7084 The quickˇ
7085 ˇthe lazy dog"});
7086
7087 cx.set_state(indoc! {"
7088 The quick« brownˇ»
7089 fox jumps overˇ
7090 the lazy dog"});
7091 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7092 cx.assert_editor_state(indoc! {"
7093 The quickˇ
7094 fox jumps overˇthe lazy dog"});
7095
7096 cx.set_state(indoc! {"
7097 The quick« brownˇ»
7098 fox jumps overˇ
7099 the lazy dog"});
7100 cx.update_editor(|e, window, cx| {
7101 e.cut_to_end_of_line(
7102 &CutToEndOfLine {
7103 stop_at_newlines: true,
7104 },
7105 window,
7106 cx,
7107 )
7108 });
7109 cx.assert_editor_state(indoc! {"
7110 The quickˇ
7111 fox jumps overˇ
7112 the lazy dog"});
7113
7114 cx.set_state(indoc! {"
7115 The quick« brownˇ»
7116 fox jumps overˇ
7117 the lazy dog"});
7118 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7119 cx.assert_editor_state(indoc! {"
7120 The quickˇ
7121 fox jumps overˇthe lazy dog"});
7122}
7123
7124#[gpui::test]
7125async fn test_clipboard(cx: &mut TestAppContext) {
7126 init_test(cx, |_| {});
7127
7128 let mut cx = EditorTestContext::new(cx).await;
7129
7130 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7131 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7132 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7133
7134 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7135 cx.set_state("two ˇfour ˇsix ˇ");
7136 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7137 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7138
7139 // Paste again but with only two cursors. Since the number of cursors doesn't
7140 // match the number of slices in the clipboard, the entire clipboard text
7141 // is pasted at each cursor.
7142 cx.set_state("ˇtwo one✅ four three six five ˇ");
7143 cx.update_editor(|e, window, cx| {
7144 e.handle_input("( ", window, cx);
7145 e.paste(&Paste, window, cx);
7146 e.handle_input(") ", window, cx);
7147 });
7148 cx.assert_editor_state(
7149 &([
7150 "( one✅ ",
7151 "three ",
7152 "five ) ˇtwo one✅ four three six five ( one✅ ",
7153 "three ",
7154 "five ) ˇ",
7155 ]
7156 .join("\n")),
7157 );
7158
7159 // Cut with three selections, one of which is full-line.
7160 cx.set_state(indoc! {"
7161 1«2ˇ»3
7162 4ˇ567
7163 «8ˇ»9"});
7164 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7165 cx.assert_editor_state(indoc! {"
7166 1ˇ3
7167 ˇ9"});
7168
7169 // Paste with three selections, noticing how the copied selection that was full-line
7170 // gets inserted before the second cursor.
7171 cx.set_state(indoc! {"
7172 1ˇ3
7173 9ˇ
7174 «oˇ»ne"});
7175 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7176 cx.assert_editor_state(indoc! {"
7177 12ˇ3
7178 4567
7179 9ˇ
7180 8ˇne"});
7181
7182 // Copy with a single cursor only, which writes the whole line into the clipboard.
7183 cx.set_state(indoc! {"
7184 The quick brown
7185 fox juˇmps over
7186 the lazy dog"});
7187 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7188 assert_eq!(
7189 cx.read_from_clipboard()
7190 .and_then(|item| item.text().as_deref().map(str::to_string)),
7191 Some("fox jumps over\n".to_string())
7192 );
7193
7194 // Paste with three selections, noticing how the copied full-line selection is inserted
7195 // before the empty selections but replaces the selection that is non-empty.
7196 cx.set_state(indoc! {"
7197 Tˇhe quick brown
7198 «foˇ»x jumps over
7199 tˇhe lazy dog"});
7200 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7201 cx.assert_editor_state(indoc! {"
7202 fox jumps over
7203 Tˇhe quick brown
7204 fox jumps over
7205 ˇx jumps over
7206 fox jumps over
7207 tˇhe lazy dog"});
7208}
7209
7210#[gpui::test]
7211async fn test_copy_trim(cx: &mut TestAppContext) {
7212 init_test(cx, |_| {});
7213
7214 let mut cx = EditorTestContext::new(cx).await;
7215 cx.set_state(
7216 r#" «for selection in selections.iter() {
7217 let mut start = selection.start;
7218 let mut end = selection.end;
7219 let is_entire_line = selection.is_empty();
7220 if is_entire_line {
7221 start = Point::new(start.row, 0);ˇ»
7222 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7223 }
7224 "#,
7225 );
7226 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7227 assert_eq!(
7228 cx.read_from_clipboard()
7229 .and_then(|item| item.text().as_deref().map(str::to_string)),
7230 Some(
7231 "for selection in selections.iter() {
7232 let mut start = selection.start;
7233 let mut end = selection.end;
7234 let is_entire_line = selection.is_empty();
7235 if is_entire_line {
7236 start = Point::new(start.row, 0);"
7237 .to_string()
7238 ),
7239 "Regular copying preserves all indentation selected",
7240 );
7241 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7242 assert_eq!(
7243 cx.read_from_clipboard()
7244 .and_then(|item| item.text().as_deref().map(str::to_string)),
7245 Some(
7246 "for selection in selections.iter() {
7247let mut start = selection.start;
7248let mut end = selection.end;
7249let is_entire_line = selection.is_empty();
7250if is_entire_line {
7251 start = Point::new(start.row, 0);"
7252 .to_string()
7253 ),
7254 "Copying with stripping should strip all leading whitespaces"
7255 );
7256
7257 cx.set_state(
7258 r#" « for selection in selections.iter() {
7259 let mut start = selection.start;
7260 let mut end = selection.end;
7261 let is_entire_line = selection.is_empty();
7262 if is_entire_line {
7263 start = Point::new(start.row, 0);ˇ»
7264 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7265 }
7266 "#,
7267 );
7268 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7269 assert_eq!(
7270 cx.read_from_clipboard()
7271 .and_then(|item| item.text().as_deref().map(str::to_string)),
7272 Some(
7273 " for selection in selections.iter() {
7274 let mut start = selection.start;
7275 let mut end = selection.end;
7276 let is_entire_line = selection.is_empty();
7277 if is_entire_line {
7278 start = Point::new(start.row, 0);"
7279 .to_string()
7280 ),
7281 "Regular copying preserves all indentation selected",
7282 );
7283 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7284 assert_eq!(
7285 cx.read_from_clipboard()
7286 .and_then(|item| item.text().as_deref().map(str::to_string)),
7287 Some(
7288 "for selection in selections.iter() {
7289let mut start = selection.start;
7290let mut end = selection.end;
7291let is_entire_line = selection.is_empty();
7292if is_entire_line {
7293 start = Point::new(start.row, 0);"
7294 .to_string()
7295 ),
7296 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7297 );
7298
7299 cx.set_state(
7300 r#" «ˇ for selection in selections.iter() {
7301 let mut start = selection.start;
7302 let mut end = selection.end;
7303 let is_entire_line = selection.is_empty();
7304 if is_entire_line {
7305 start = Point::new(start.row, 0);»
7306 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7307 }
7308 "#,
7309 );
7310 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7311 assert_eq!(
7312 cx.read_from_clipboard()
7313 .and_then(|item| item.text().as_deref().map(str::to_string)),
7314 Some(
7315 " for selection in selections.iter() {
7316 let mut start = selection.start;
7317 let mut end = selection.end;
7318 let is_entire_line = selection.is_empty();
7319 if is_entire_line {
7320 start = Point::new(start.row, 0);"
7321 .to_string()
7322 ),
7323 "Regular copying for reverse selection works the same",
7324 );
7325 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7326 assert_eq!(
7327 cx.read_from_clipboard()
7328 .and_then(|item| item.text().as_deref().map(str::to_string)),
7329 Some(
7330 "for selection in selections.iter() {
7331let mut start = selection.start;
7332let mut end = selection.end;
7333let is_entire_line = selection.is_empty();
7334if is_entire_line {
7335 start = Point::new(start.row, 0);"
7336 .to_string()
7337 ),
7338 "Copying with stripping for reverse selection works the same"
7339 );
7340
7341 cx.set_state(
7342 r#" for selection «in selections.iter() {
7343 let mut start = selection.start;
7344 let mut end = selection.end;
7345 let is_entire_line = selection.is_empty();
7346 if is_entire_line {
7347 start = Point::new(start.row, 0);ˇ»
7348 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7349 }
7350 "#,
7351 );
7352 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7353 assert_eq!(
7354 cx.read_from_clipboard()
7355 .and_then(|item| item.text().as_deref().map(str::to_string)),
7356 Some(
7357 "in selections.iter() {
7358 let mut start = selection.start;
7359 let mut end = selection.end;
7360 let is_entire_line = selection.is_empty();
7361 if is_entire_line {
7362 start = Point::new(start.row, 0);"
7363 .to_string()
7364 ),
7365 "When selecting past the indent, the copying works as usual",
7366 );
7367 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7368 assert_eq!(
7369 cx.read_from_clipboard()
7370 .and_then(|item| item.text().as_deref().map(str::to_string)),
7371 Some(
7372 "in selections.iter() {
7373 let mut start = selection.start;
7374 let mut end = selection.end;
7375 let is_entire_line = selection.is_empty();
7376 if is_entire_line {
7377 start = Point::new(start.row, 0);"
7378 .to_string()
7379 ),
7380 "When selecting past the indent, nothing is trimmed"
7381 );
7382
7383 cx.set_state(
7384 r#" «for selection in selections.iter() {
7385 let mut start = selection.start;
7386
7387 let mut end = selection.end;
7388 let is_entire_line = selection.is_empty();
7389 if is_entire_line {
7390 start = Point::new(start.row, 0);
7391ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7392 }
7393 "#,
7394 );
7395 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7396 assert_eq!(
7397 cx.read_from_clipboard()
7398 .and_then(|item| item.text().as_deref().map(str::to_string)),
7399 Some(
7400 "for selection in selections.iter() {
7401let mut start = selection.start;
7402
7403let mut end = selection.end;
7404let is_entire_line = selection.is_empty();
7405if is_entire_line {
7406 start = Point::new(start.row, 0);
7407"
7408 .to_string()
7409 ),
7410 "Copying with stripping should ignore empty lines"
7411 );
7412}
7413
7414#[gpui::test]
7415async fn test_paste_multiline(cx: &mut TestAppContext) {
7416 init_test(cx, |_| {});
7417
7418 let mut cx = EditorTestContext::new(cx).await;
7419 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7420
7421 // Cut an indented block, without the leading whitespace.
7422 cx.set_state(indoc! {"
7423 const a: B = (
7424 c(),
7425 «d(
7426 e,
7427 f
7428 )ˇ»
7429 );
7430 "});
7431 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7432 cx.assert_editor_state(indoc! {"
7433 const a: B = (
7434 c(),
7435 ˇ
7436 );
7437 "});
7438
7439 // Paste it at the same position.
7440 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7441 cx.assert_editor_state(indoc! {"
7442 const a: B = (
7443 c(),
7444 d(
7445 e,
7446 f
7447 )ˇ
7448 );
7449 "});
7450
7451 // Paste it at a line with a lower indent level.
7452 cx.set_state(indoc! {"
7453 ˇ
7454 const a: B = (
7455 c(),
7456 );
7457 "});
7458 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7459 cx.assert_editor_state(indoc! {"
7460 d(
7461 e,
7462 f
7463 )ˇ
7464 const a: B = (
7465 c(),
7466 );
7467 "});
7468
7469 // Cut an indented block, with the leading whitespace.
7470 cx.set_state(indoc! {"
7471 const a: B = (
7472 c(),
7473 « d(
7474 e,
7475 f
7476 )
7477 ˇ»);
7478 "});
7479 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7480 cx.assert_editor_state(indoc! {"
7481 const a: B = (
7482 c(),
7483 ˇ);
7484 "});
7485
7486 // Paste it at the same position.
7487 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7488 cx.assert_editor_state(indoc! {"
7489 const a: B = (
7490 c(),
7491 d(
7492 e,
7493 f
7494 )
7495 ˇ);
7496 "});
7497
7498 // Paste it at a line with a higher indent level.
7499 cx.set_state(indoc! {"
7500 const a: B = (
7501 c(),
7502 d(
7503 e,
7504 fˇ
7505 )
7506 );
7507 "});
7508 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7509 cx.assert_editor_state(indoc! {"
7510 const a: B = (
7511 c(),
7512 d(
7513 e,
7514 f d(
7515 e,
7516 f
7517 )
7518 ˇ
7519 )
7520 );
7521 "});
7522
7523 // Copy an indented block, starting mid-line
7524 cx.set_state(indoc! {"
7525 const a: B = (
7526 c(),
7527 somethin«g(
7528 e,
7529 f
7530 )ˇ»
7531 );
7532 "});
7533 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7534
7535 // Paste it on a line with a lower indent level
7536 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7537 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7538 cx.assert_editor_state(indoc! {"
7539 const a: B = (
7540 c(),
7541 something(
7542 e,
7543 f
7544 )
7545 );
7546 g(
7547 e,
7548 f
7549 )ˇ"});
7550}
7551
7552#[gpui::test]
7553async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7554 init_test(cx, |_| {});
7555
7556 cx.write_to_clipboard(ClipboardItem::new_string(
7557 " d(\n e\n );\n".into(),
7558 ));
7559
7560 let mut cx = EditorTestContext::new(cx).await;
7561 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7562
7563 cx.set_state(indoc! {"
7564 fn a() {
7565 b();
7566 if c() {
7567 ˇ
7568 }
7569 }
7570 "});
7571
7572 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7573 cx.assert_editor_state(indoc! {"
7574 fn a() {
7575 b();
7576 if c() {
7577 d(
7578 e
7579 );
7580 ˇ
7581 }
7582 }
7583 "});
7584
7585 cx.set_state(indoc! {"
7586 fn a() {
7587 b();
7588 ˇ
7589 }
7590 "});
7591
7592 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7593 cx.assert_editor_state(indoc! {"
7594 fn a() {
7595 b();
7596 d(
7597 e
7598 );
7599 ˇ
7600 }
7601 "});
7602}
7603
7604#[gpui::test]
7605fn test_select_all(cx: &mut TestAppContext) {
7606 init_test(cx, |_| {});
7607
7608 let editor = cx.add_window(|window, cx| {
7609 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7610 build_editor(buffer, window, cx)
7611 });
7612 _ = editor.update(cx, |editor, window, cx| {
7613 editor.select_all(&SelectAll, window, cx);
7614 assert_eq!(
7615 editor.selections.display_ranges(cx),
7616 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7617 );
7618 });
7619}
7620
7621#[gpui::test]
7622fn test_select_line(cx: &mut TestAppContext) {
7623 init_test(cx, |_| {});
7624
7625 let editor = cx.add_window(|window, cx| {
7626 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7627 build_editor(buffer, window, cx)
7628 });
7629 _ = editor.update(cx, |editor, window, cx| {
7630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7631 s.select_display_ranges([
7632 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7634 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7635 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7636 ])
7637 });
7638 editor.select_line(&SelectLine, window, cx);
7639 assert_eq!(
7640 editor.selections.display_ranges(cx),
7641 vec![
7642 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7643 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7644 ]
7645 );
7646 });
7647
7648 _ = editor.update(cx, |editor, window, cx| {
7649 editor.select_line(&SelectLine, window, cx);
7650 assert_eq!(
7651 editor.selections.display_ranges(cx),
7652 vec![
7653 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7654 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7655 ]
7656 );
7657 });
7658
7659 _ = editor.update(cx, |editor, window, cx| {
7660 editor.select_line(&SelectLine, window, cx);
7661 assert_eq!(
7662 editor.selections.display_ranges(cx),
7663 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7664 );
7665 });
7666}
7667
7668#[gpui::test]
7669async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7670 init_test(cx, |_| {});
7671 let mut cx = EditorTestContext::new(cx).await;
7672
7673 #[track_caller]
7674 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7675 cx.set_state(initial_state);
7676 cx.update_editor(|e, window, cx| {
7677 e.split_selection_into_lines(&Default::default(), window, cx)
7678 });
7679 cx.assert_editor_state(expected_state);
7680 }
7681
7682 // Selection starts and ends at the middle of lines, left-to-right
7683 test(
7684 &mut cx,
7685 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7686 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7687 );
7688 // Same thing, right-to-left
7689 test(
7690 &mut cx,
7691 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7692 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7693 );
7694
7695 // Whole buffer, left-to-right, last line *doesn't* end with newline
7696 test(
7697 &mut cx,
7698 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7699 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7700 );
7701 // Same thing, right-to-left
7702 test(
7703 &mut cx,
7704 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7705 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7706 );
7707
7708 // Whole buffer, left-to-right, last line ends with newline
7709 test(
7710 &mut cx,
7711 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7712 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7713 );
7714 // Same thing, right-to-left
7715 test(
7716 &mut cx,
7717 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7718 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7719 );
7720
7721 // Starts at the end of a line, ends at the start of another
7722 test(
7723 &mut cx,
7724 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7725 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7726 );
7727}
7728
7729#[gpui::test]
7730async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7731 init_test(cx, |_| {});
7732
7733 let editor = cx.add_window(|window, cx| {
7734 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7735 build_editor(buffer, window, cx)
7736 });
7737
7738 // setup
7739 _ = editor.update(cx, |editor, window, cx| {
7740 editor.fold_creases(
7741 vec![
7742 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7743 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7744 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7745 ],
7746 true,
7747 window,
7748 cx,
7749 );
7750 assert_eq!(
7751 editor.display_text(cx),
7752 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7753 );
7754 });
7755
7756 _ = editor.update(cx, |editor, window, cx| {
7757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7758 s.select_display_ranges([
7759 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7760 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7761 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7762 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7763 ])
7764 });
7765 editor.split_selection_into_lines(&Default::default(), window, cx);
7766 assert_eq!(
7767 editor.display_text(cx),
7768 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7769 );
7770 });
7771 EditorTestContext::for_editor(editor, cx)
7772 .await
7773 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7774
7775 _ = editor.update(cx, |editor, window, cx| {
7776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7777 s.select_display_ranges([
7778 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7779 ])
7780 });
7781 editor.split_selection_into_lines(&Default::default(), window, cx);
7782 assert_eq!(
7783 editor.display_text(cx),
7784 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7785 );
7786 assert_eq!(
7787 editor.selections.display_ranges(cx),
7788 [
7789 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7790 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7791 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7792 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7793 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7794 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7795 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7796 ]
7797 );
7798 });
7799 EditorTestContext::for_editor(editor, cx)
7800 .await
7801 .assert_editor_state(
7802 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7803 );
7804}
7805
7806#[gpui::test]
7807async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7808 init_test(cx, |_| {});
7809
7810 let mut cx = EditorTestContext::new(cx).await;
7811
7812 cx.set_state(indoc!(
7813 r#"abc
7814 defˇghi
7815
7816 jk
7817 nlmo
7818 "#
7819 ));
7820
7821 cx.update_editor(|editor, window, cx| {
7822 editor.add_selection_above(&Default::default(), window, cx);
7823 });
7824
7825 cx.assert_editor_state(indoc!(
7826 r#"abcˇ
7827 defˇghi
7828
7829 jk
7830 nlmo
7831 "#
7832 ));
7833
7834 cx.update_editor(|editor, window, cx| {
7835 editor.add_selection_above(&Default::default(), window, cx);
7836 });
7837
7838 cx.assert_editor_state(indoc!(
7839 r#"abcˇ
7840 defˇghi
7841
7842 jk
7843 nlmo
7844 "#
7845 ));
7846
7847 cx.update_editor(|editor, window, cx| {
7848 editor.add_selection_below(&Default::default(), window, cx);
7849 });
7850
7851 cx.assert_editor_state(indoc!(
7852 r#"abc
7853 defˇghi
7854
7855 jk
7856 nlmo
7857 "#
7858 ));
7859
7860 cx.update_editor(|editor, window, cx| {
7861 editor.undo_selection(&Default::default(), window, cx);
7862 });
7863
7864 cx.assert_editor_state(indoc!(
7865 r#"abcˇ
7866 defˇghi
7867
7868 jk
7869 nlmo
7870 "#
7871 ));
7872
7873 cx.update_editor(|editor, window, cx| {
7874 editor.redo_selection(&Default::default(), window, cx);
7875 });
7876
7877 cx.assert_editor_state(indoc!(
7878 r#"abc
7879 defˇghi
7880
7881 jk
7882 nlmo
7883 "#
7884 ));
7885
7886 cx.update_editor(|editor, window, cx| {
7887 editor.add_selection_below(&Default::default(), window, cx);
7888 });
7889
7890 cx.assert_editor_state(indoc!(
7891 r#"abc
7892 defˇghi
7893 ˇ
7894 jk
7895 nlmo
7896 "#
7897 ));
7898
7899 cx.update_editor(|editor, window, cx| {
7900 editor.add_selection_below(&Default::default(), window, cx);
7901 });
7902
7903 cx.assert_editor_state(indoc!(
7904 r#"abc
7905 defˇghi
7906 ˇ
7907 jkˇ
7908 nlmo
7909 "#
7910 ));
7911
7912 cx.update_editor(|editor, window, cx| {
7913 editor.add_selection_below(&Default::default(), window, cx);
7914 });
7915
7916 cx.assert_editor_state(indoc!(
7917 r#"abc
7918 defˇghi
7919 ˇ
7920 jkˇ
7921 nlmˇo
7922 "#
7923 ));
7924
7925 cx.update_editor(|editor, window, cx| {
7926 editor.add_selection_below(&Default::default(), window, cx);
7927 });
7928
7929 cx.assert_editor_state(indoc!(
7930 r#"abc
7931 defˇghi
7932 ˇ
7933 jkˇ
7934 nlmˇo
7935 ˇ"#
7936 ));
7937
7938 // change selections
7939 cx.set_state(indoc!(
7940 r#"abc
7941 def«ˇg»hi
7942
7943 jk
7944 nlmo
7945 "#
7946 ));
7947
7948 cx.update_editor(|editor, window, cx| {
7949 editor.add_selection_below(&Default::default(), window, cx);
7950 });
7951
7952 cx.assert_editor_state(indoc!(
7953 r#"abc
7954 def«ˇg»hi
7955
7956 jk
7957 nlm«ˇo»
7958 "#
7959 ));
7960
7961 cx.update_editor(|editor, window, cx| {
7962 editor.add_selection_below(&Default::default(), window, cx);
7963 });
7964
7965 cx.assert_editor_state(indoc!(
7966 r#"abc
7967 def«ˇg»hi
7968
7969 jk
7970 nlm«ˇo»
7971 "#
7972 ));
7973
7974 cx.update_editor(|editor, window, cx| {
7975 editor.add_selection_above(&Default::default(), window, cx);
7976 });
7977
7978 cx.assert_editor_state(indoc!(
7979 r#"abc
7980 def«ˇg»hi
7981
7982 jk
7983 nlmo
7984 "#
7985 ));
7986
7987 cx.update_editor(|editor, window, cx| {
7988 editor.add_selection_above(&Default::default(), window, cx);
7989 });
7990
7991 cx.assert_editor_state(indoc!(
7992 r#"abc
7993 def«ˇg»hi
7994
7995 jk
7996 nlmo
7997 "#
7998 ));
7999
8000 // Change selections again
8001 cx.set_state(indoc!(
8002 r#"a«bc
8003 defgˇ»hi
8004
8005 jk
8006 nlmo
8007 "#
8008 ));
8009
8010 cx.update_editor(|editor, window, cx| {
8011 editor.add_selection_below(&Default::default(), window, cx);
8012 });
8013
8014 cx.assert_editor_state(indoc!(
8015 r#"a«bcˇ»
8016 d«efgˇ»hi
8017
8018 j«kˇ»
8019 nlmo
8020 "#
8021 ));
8022
8023 cx.update_editor(|editor, window, cx| {
8024 editor.add_selection_below(&Default::default(), window, cx);
8025 });
8026 cx.assert_editor_state(indoc!(
8027 r#"a«bcˇ»
8028 d«efgˇ»hi
8029
8030 j«kˇ»
8031 n«lmoˇ»
8032 "#
8033 ));
8034 cx.update_editor(|editor, window, cx| {
8035 editor.add_selection_above(&Default::default(), window, cx);
8036 });
8037
8038 cx.assert_editor_state(indoc!(
8039 r#"a«bcˇ»
8040 d«efgˇ»hi
8041
8042 j«kˇ»
8043 nlmo
8044 "#
8045 ));
8046
8047 // Change selections again
8048 cx.set_state(indoc!(
8049 r#"abc
8050 d«ˇefghi
8051
8052 jk
8053 nlm»o
8054 "#
8055 ));
8056
8057 cx.update_editor(|editor, window, cx| {
8058 editor.add_selection_above(&Default::default(), window, cx);
8059 });
8060
8061 cx.assert_editor_state(indoc!(
8062 r#"a«ˇbc»
8063 d«ˇef»ghi
8064
8065 j«ˇk»
8066 n«ˇlm»o
8067 "#
8068 ));
8069
8070 cx.update_editor(|editor, window, cx| {
8071 editor.add_selection_below(&Default::default(), window, cx);
8072 });
8073
8074 cx.assert_editor_state(indoc!(
8075 r#"abc
8076 d«ˇef»ghi
8077
8078 j«ˇk»
8079 n«ˇlm»o
8080 "#
8081 ));
8082}
8083
8084#[gpui::test]
8085async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8086 init_test(cx, |_| {});
8087 let mut cx = EditorTestContext::new(cx).await;
8088
8089 cx.set_state(indoc!(
8090 r#"line onˇe
8091 liˇne two
8092 line three
8093 line four"#
8094 ));
8095
8096 cx.update_editor(|editor, window, cx| {
8097 editor.add_selection_below(&Default::default(), window, cx);
8098 });
8099
8100 // test multiple cursors expand in the same direction
8101 cx.assert_editor_state(indoc!(
8102 r#"line onˇe
8103 liˇne twˇo
8104 liˇne three
8105 line four"#
8106 ));
8107
8108 cx.update_editor(|editor, window, cx| {
8109 editor.add_selection_below(&Default::default(), window, cx);
8110 });
8111
8112 cx.update_editor(|editor, window, cx| {
8113 editor.add_selection_below(&Default::default(), window, cx);
8114 });
8115
8116 // test multiple cursors expand below overflow
8117 cx.assert_editor_state(indoc!(
8118 r#"line onˇe
8119 liˇne twˇo
8120 liˇne thˇree
8121 liˇne foˇur"#
8122 ));
8123
8124 cx.update_editor(|editor, window, cx| {
8125 editor.add_selection_above(&Default::default(), window, cx);
8126 });
8127
8128 // test multiple cursors retrieves back correctly
8129 cx.assert_editor_state(indoc!(
8130 r#"line onˇe
8131 liˇne twˇo
8132 liˇne thˇree
8133 line four"#
8134 ));
8135
8136 cx.update_editor(|editor, window, cx| {
8137 editor.add_selection_above(&Default::default(), window, cx);
8138 });
8139
8140 cx.update_editor(|editor, window, cx| {
8141 editor.add_selection_above(&Default::default(), window, cx);
8142 });
8143
8144 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8145 cx.assert_editor_state(indoc!(
8146 r#"liˇne onˇe
8147 liˇne two
8148 line three
8149 line four"#
8150 ));
8151
8152 cx.update_editor(|editor, window, cx| {
8153 editor.undo_selection(&Default::default(), window, cx);
8154 });
8155
8156 // test undo
8157 cx.assert_editor_state(indoc!(
8158 r#"line onˇe
8159 liˇne twˇo
8160 line three
8161 line four"#
8162 ));
8163
8164 cx.update_editor(|editor, window, cx| {
8165 editor.redo_selection(&Default::default(), window, cx);
8166 });
8167
8168 // test redo
8169 cx.assert_editor_state(indoc!(
8170 r#"liˇne onˇe
8171 liˇne two
8172 line three
8173 line four"#
8174 ));
8175
8176 cx.set_state(indoc!(
8177 r#"abcd
8178 ef«ghˇ»
8179 ijkl
8180 «mˇ»nop"#
8181 ));
8182
8183 cx.update_editor(|editor, window, cx| {
8184 editor.add_selection_above(&Default::default(), window, cx);
8185 });
8186
8187 // test multiple selections expand in the same direction
8188 cx.assert_editor_state(indoc!(
8189 r#"ab«cdˇ»
8190 ef«ghˇ»
8191 «iˇ»jkl
8192 «mˇ»nop"#
8193 ));
8194
8195 cx.update_editor(|editor, window, cx| {
8196 editor.add_selection_above(&Default::default(), window, cx);
8197 });
8198
8199 // test multiple selection upward overflow
8200 cx.assert_editor_state(indoc!(
8201 r#"ab«cdˇ»
8202 «eˇ»f«ghˇ»
8203 «iˇ»jkl
8204 «mˇ»nop"#
8205 ));
8206
8207 cx.update_editor(|editor, window, cx| {
8208 editor.add_selection_below(&Default::default(), window, cx);
8209 });
8210
8211 // test multiple selection retrieves back correctly
8212 cx.assert_editor_state(indoc!(
8213 r#"abcd
8214 ef«ghˇ»
8215 «iˇ»jkl
8216 «mˇ»nop"#
8217 ));
8218
8219 cx.update_editor(|editor, window, cx| {
8220 editor.add_selection_below(&Default::default(), window, cx);
8221 });
8222
8223 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8224 cx.assert_editor_state(indoc!(
8225 r#"abcd
8226 ef«ghˇ»
8227 ij«klˇ»
8228 «mˇ»nop"#
8229 ));
8230
8231 cx.update_editor(|editor, window, cx| {
8232 editor.undo_selection(&Default::default(), window, cx);
8233 });
8234
8235 // test undo
8236 cx.assert_editor_state(indoc!(
8237 r#"abcd
8238 ef«ghˇ»
8239 «iˇ»jkl
8240 «mˇ»nop"#
8241 ));
8242
8243 cx.update_editor(|editor, window, cx| {
8244 editor.redo_selection(&Default::default(), window, cx);
8245 });
8246
8247 // test redo
8248 cx.assert_editor_state(indoc!(
8249 r#"abcd
8250 ef«ghˇ»
8251 ij«klˇ»
8252 «mˇ»nop"#
8253 ));
8254}
8255
8256#[gpui::test]
8257async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8258 init_test(cx, |_| {});
8259 let mut cx = EditorTestContext::new(cx).await;
8260
8261 cx.set_state(indoc!(
8262 r#"line onˇe
8263 liˇne two
8264 line three
8265 line four"#
8266 ));
8267
8268 cx.update_editor(|editor, window, cx| {
8269 editor.add_selection_below(&Default::default(), window, cx);
8270 editor.add_selection_below(&Default::default(), window, cx);
8271 editor.add_selection_below(&Default::default(), window, cx);
8272 });
8273
8274 // initial state with two multi cursor groups
8275 cx.assert_editor_state(indoc!(
8276 r#"line onˇe
8277 liˇne twˇo
8278 liˇne thˇree
8279 liˇne foˇur"#
8280 ));
8281
8282 // add single cursor in middle - simulate opt click
8283 cx.update_editor(|editor, window, cx| {
8284 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8285 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8286 editor.end_selection(window, cx);
8287 });
8288
8289 cx.assert_editor_state(indoc!(
8290 r#"line onˇe
8291 liˇne twˇo
8292 liˇneˇ thˇree
8293 liˇne foˇur"#
8294 ));
8295
8296 cx.update_editor(|editor, window, cx| {
8297 editor.add_selection_above(&Default::default(), window, cx);
8298 });
8299
8300 // test new added selection expands above and existing selection shrinks
8301 cx.assert_editor_state(indoc!(
8302 r#"line onˇe
8303 liˇneˇ twˇo
8304 liˇneˇ thˇree
8305 line four"#
8306 ));
8307
8308 cx.update_editor(|editor, window, cx| {
8309 editor.add_selection_above(&Default::default(), window, cx);
8310 });
8311
8312 // test new added selection expands above and existing selection shrinks
8313 cx.assert_editor_state(indoc!(
8314 r#"lineˇ onˇe
8315 liˇneˇ twˇo
8316 lineˇ three
8317 line four"#
8318 ));
8319
8320 // intial state with two selection groups
8321 cx.set_state(indoc!(
8322 r#"abcd
8323 ef«ghˇ»
8324 ijkl
8325 «mˇ»nop"#
8326 ));
8327
8328 cx.update_editor(|editor, window, cx| {
8329 editor.add_selection_above(&Default::default(), window, cx);
8330 editor.add_selection_above(&Default::default(), window, cx);
8331 });
8332
8333 cx.assert_editor_state(indoc!(
8334 r#"ab«cdˇ»
8335 «eˇ»f«ghˇ»
8336 «iˇ»jkl
8337 «mˇ»nop"#
8338 ));
8339
8340 // add single selection in middle - simulate opt drag
8341 cx.update_editor(|editor, window, cx| {
8342 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8343 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8344 editor.update_selection(
8345 DisplayPoint::new(DisplayRow(2), 4),
8346 0,
8347 gpui::Point::<f32>::default(),
8348 window,
8349 cx,
8350 );
8351 editor.end_selection(window, cx);
8352 });
8353
8354 cx.assert_editor_state(indoc!(
8355 r#"ab«cdˇ»
8356 «eˇ»f«ghˇ»
8357 «iˇ»jk«lˇ»
8358 «mˇ»nop"#
8359 ));
8360
8361 cx.update_editor(|editor, window, cx| {
8362 editor.add_selection_below(&Default::default(), window, cx);
8363 });
8364
8365 // test new added selection expands below, others shrinks from above
8366 cx.assert_editor_state(indoc!(
8367 r#"abcd
8368 ef«ghˇ»
8369 «iˇ»jk«lˇ»
8370 «mˇ»no«pˇ»"#
8371 ));
8372}
8373
8374#[gpui::test]
8375async fn test_select_next(cx: &mut TestAppContext) {
8376 init_test(cx, |_| {});
8377
8378 let mut cx = EditorTestContext::new(cx).await;
8379 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8380
8381 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8382 .unwrap();
8383 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8384
8385 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8386 .unwrap();
8387 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8388
8389 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8390 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8391
8392 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8393 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8394
8395 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8396 .unwrap();
8397 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8398
8399 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8400 .unwrap();
8401 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8402
8403 // Test selection direction should be preserved
8404 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8405
8406 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8407 .unwrap();
8408 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8409}
8410
8411#[gpui::test]
8412async fn test_select_all_matches(cx: &mut TestAppContext) {
8413 init_test(cx, |_| {});
8414
8415 let mut cx = EditorTestContext::new(cx).await;
8416
8417 // Test caret-only selections
8418 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8419 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8420 .unwrap();
8421 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8422
8423 // Test left-to-right selections
8424 cx.set_state("abc\n«abcˇ»\nabc");
8425 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8426 .unwrap();
8427 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8428
8429 // Test right-to-left selections
8430 cx.set_state("abc\n«ˇabc»\nabc");
8431 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8432 .unwrap();
8433 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8434
8435 // Test selecting whitespace with caret selection
8436 cx.set_state("abc\nˇ abc\nabc");
8437 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8438 .unwrap();
8439 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8440
8441 // Test selecting whitespace with left-to-right selection
8442 cx.set_state("abc\n«ˇ »abc\nabc");
8443 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8444 .unwrap();
8445 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8446
8447 // Test no matches with right-to-left selection
8448 cx.set_state("abc\n« ˇ»abc\nabc");
8449 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8450 .unwrap();
8451 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8452
8453 // Test with a single word and clip_at_line_ends=true (#29823)
8454 cx.set_state("aˇbc");
8455 cx.update_editor(|e, window, cx| {
8456 e.set_clip_at_line_ends(true, cx);
8457 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8458 e.set_clip_at_line_ends(false, cx);
8459 });
8460 cx.assert_editor_state("«abcˇ»");
8461}
8462
8463#[gpui::test]
8464async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8465 init_test(cx, |_| {});
8466
8467 let mut cx = EditorTestContext::new(cx).await;
8468
8469 let large_body_1 = "\nd".repeat(200);
8470 let large_body_2 = "\ne".repeat(200);
8471
8472 cx.set_state(&format!(
8473 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8474 ));
8475 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8476 let scroll_position = editor.scroll_position(cx);
8477 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8478 scroll_position
8479 });
8480
8481 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8482 .unwrap();
8483 cx.assert_editor_state(&format!(
8484 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8485 ));
8486 let scroll_position_after_selection =
8487 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8488 assert_eq!(
8489 initial_scroll_position, scroll_position_after_selection,
8490 "Scroll position should not change after selecting all matches"
8491 );
8492}
8493
8494#[gpui::test]
8495async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8496 init_test(cx, |_| {});
8497
8498 let mut cx = EditorLspTestContext::new_rust(
8499 lsp::ServerCapabilities {
8500 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8501 ..Default::default()
8502 },
8503 cx,
8504 )
8505 .await;
8506
8507 cx.set_state(indoc! {"
8508 line 1
8509 line 2
8510 linˇe 3
8511 line 4
8512 line 5
8513 "});
8514
8515 // Make an edit
8516 cx.update_editor(|editor, window, cx| {
8517 editor.handle_input("X", window, cx);
8518 });
8519
8520 // Move cursor to a different position
8521 cx.update_editor(|editor, window, cx| {
8522 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8523 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8524 });
8525 });
8526
8527 cx.assert_editor_state(indoc! {"
8528 line 1
8529 line 2
8530 linXe 3
8531 line 4
8532 liˇne 5
8533 "});
8534
8535 cx.lsp
8536 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8537 Ok(Some(vec![lsp::TextEdit::new(
8538 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8539 "PREFIX ".to_string(),
8540 )]))
8541 });
8542
8543 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8544 .unwrap()
8545 .await
8546 .unwrap();
8547
8548 cx.assert_editor_state(indoc! {"
8549 PREFIX line 1
8550 line 2
8551 linXe 3
8552 line 4
8553 liˇne 5
8554 "});
8555
8556 // Undo formatting
8557 cx.update_editor(|editor, window, cx| {
8558 editor.undo(&Default::default(), window, cx);
8559 });
8560
8561 // Verify cursor moved back to position after edit
8562 cx.assert_editor_state(indoc! {"
8563 line 1
8564 line 2
8565 linXˇe 3
8566 line 4
8567 line 5
8568 "});
8569}
8570
8571#[gpui::test]
8572async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8573 init_test(cx, |_| {});
8574
8575 let mut cx = EditorTestContext::new(cx).await;
8576
8577 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8578 cx.update_editor(|editor, window, cx| {
8579 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8580 });
8581
8582 cx.set_state(indoc! {"
8583 line 1
8584 line 2
8585 linˇe 3
8586 line 4
8587 line 5
8588 line 6
8589 line 7
8590 line 8
8591 line 9
8592 line 10
8593 "});
8594
8595 let snapshot = cx.buffer_snapshot();
8596 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8597
8598 cx.update(|_, cx| {
8599 provider.update(cx, |provider, _| {
8600 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8601 id: None,
8602 edits: vec![(edit_position..edit_position, "X".into())],
8603 edit_preview: None,
8604 }))
8605 })
8606 });
8607
8608 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8609 cx.update_editor(|editor, window, cx| {
8610 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8611 });
8612
8613 cx.assert_editor_state(indoc! {"
8614 line 1
8615 line 2
8616 lineXˇ 3
8617 line 4
8618 line 5
8619 line 6
8620 line 7
8621 line 8
8622 line 9
8623 line 10
8624 "});
8625
8626 cx.update_editor(|editor, window, cx| {
8627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8628 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8629 });
8630 });
8631
8632 cx.assert_editor_state(indoc! {"
8633 line 1
8634 line 2
8635 lineX 3
8636 line 4
8637 line 5
8638 line 6
8639 line 7
8640 line 8
8641 line 9
8642 liˇne 10
8643 "});
8644
8645 cx.update_editor(|editor, window, cx| {
8646 editor.undo(&Default::default(), window, cx);
8647 });
8648
8649 cx.assert_editor_state(indoc! {"
8650 line 1
8651 line 2
8652 lineˇ 3
8653 line 4
8654 line 5
8655 line 6
8656 line 7
8657 line 8
8658 line 9
8659 line 10
8660 "});
8661}
8662
8663#[gpui::test]
8664async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8665 init_test(cx, |_| {});
8666
8667 let mut cx = EditorTestContext::new(cx).await;
8668 cx.set_state(
8669 r#"let foo = 2;
8670lˇet foo = 2;
8671let fooˇ = 2;
8672let foo = 2;
8673let foo = ˇ2;"#,
8674 );
8675
8676 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8677 .unwrap();
8678 cx.assert_editor_state(
8679 r#"let foo = 2;
8680«letˇ» foo = 2;
8681let «fooˇ» = 2;
8682let foo = 2;
8683let foo = «2ˇ»;"#,
8684 );
8685
8686 // noop for multiple selections with different contents
8687 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8688 .unwrap();
8689 cx.assert_editor_state(
8690 r#"let foo = 2;
8691«letˇ» foo = 2;
8692let «fooˇ» = 2;
8693let foo = 2;
8694let foo = «2ˇ»;"#,
8695 );
8696
8697 // Test last selection direction should be preserved
8698 cx.set_state(
8699 r#"let foo = 2;
8700let foo = 2;
8701let «fooˇ» = 2;
8702let «ˇfoo» = 2;
8703let foo = 2;"#,
8704 );
8705
8706 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8707 .unwrap();
8708 cx.assert_editor_state(
8709 r#"let foo = 2;
8710let foo = 2;
8711let «fooˇ» = 2;
8712let «ˇfoo» = 2;
8713let «ˇfoo» = 2;"#,
8714 );
8715}
8716
8717#[gpui::test]
8718async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8719 init_test(cx, |_| {});
8720
8721 let mut cx =
8722 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8723
8724 cx.assert_editor_state(indoc! {"
8725 ˇbbb
8726 ccc
8727
8728 bbb
8729 ccc
8730 "});
8731 cx.dispatch_action(SelectPrevious::default());
8732 cx.assert_editor_state(indoc! {"
8733 «bbbˇ»
8734 ccc
8735
8736 bbb
8737 ccc
8738 "});
8739 cx.dispatch_action(SelectPrevious::default());
8740 cx.assert_editor_state(indoc! {"
8741 «bbbˇ»
8742 ccc
8743
8744 «bbbˇ»
8745 ccc
8746 "});
8747}
8748
8749#[gpui::test]
8750async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8751 init_test(cx, |_| {});
8752
8753 let mut cx = EditorTestContext::new(cx).await;
8754 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8755
8756 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8757 .unwrap();
8758 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8759
8760 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8761 .unwrap();
8762 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8763
8764 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8765 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8766
8767 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
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(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8775 .unwrap();
8776 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8777}
8778
8779#[gpui::test]
8780async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8781 init_test(cx, |_| {});
8782
8783 let mut cx = EditorTestContext::new(cx).await;
8784 cx.set_state("aˇ");
8785
8786 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8787 .unwrap();
8788 cx.assert_editor_state("«aˇ»");
8789 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8790 .unwrap();
8791 cx.assert_editor_state("«aˇ»");
8792}
8793
8794#[gpui::test]
8795async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8796 init_test(cx, |_| {});
8797
8798 let mut cx = EditorTestContext::new(cx).await;
8799 cx.set_state(
8800 r#"let foo = 2;
8801lˇet foo = 2;
8802let fooˇ = 2;
8803let foo = 2;
8804let foo = ˇ2;"#,
8805 );
8806
8807 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8808 .unwrap();
8809 cx.assert_editor_state(
8810 r#"let foo = 2;
8811«letˇ» foo = 2;
8812let «fooˇ» = 2;
8813let foo = 2;
8814let foo = «2ˇ»;"#,
8815 );
8816
8817 // noop for multiple selections with different contents
8818 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8819 .unwrap();
8820 cx.assert_editor_state(
8821 r#"let foo = 2;
8822«letˇ» foo = 2;
8823let «fooˇ» = 2;
8824let foo = 2;
8825let foo = «2ˇ»;"#,
8826 );
8827}
8828
8829#[gpui::test]
8830async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8831 init_test(cx, |_| {});
8832
8833 let mut cx = EditorTestContext::new(cx).await;
8834 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8835
8836 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8837 .unwrap();
8838 // selection direction is preserved
8839 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8840
8841 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8842 .unwrap();
8843 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8844
8845 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8846 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8847
8848 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8849 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8850
8851 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8852 .unwrap();
8853 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8854
8855 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8856 .unwrap();
8857 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8858}
8859
8860#[gpui::test]
8861async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8862 init_test(cx, |_| {});
8863
8864 let language = Arc::new(Language::new(
8865 LanguageConfig::default(),
8866 Some(tree_sitter_rust::LANGUAGE.into()),
8867 ));
8868
8869 let text = r#"
8870 use mod1::mod2::{mod3, mod4};
8871
8872 fn fn_1(param1: bool, param2: &str) {
8873 let var1 = "text";
8874 }
8875 "#
8876 .unindent();
8877
8878 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8879 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8880 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8881
8882 editor
8883 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8884 .await;
8885
8886 editor.update_in(cx, |editor, window, cx| {
8887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8888 s.select_display_ranges([
8889 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8890 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8891 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8892 ]);
8893 });
8894 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8895 });
8896 editor.update(cx, |editor, cx| {
8897 assert_text_with_selections(
8898 editor,
8899 indoc! {r#"
8900 use mod1::mod2::{mod3, «mod4ˇ»};
8901
8902 fn fn_1«ˇ(param1: bool, param2: &str)» {
8903 let var1 = "«ˇtext»";
8904 }
8905 "#},
8906 cx,
8907 );
8908 });
8909
8910 editor.update_in(cx, |editor, window, cx| {
8911 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8912 });
8913 editor.update(cx, |editor, cx| {
8914 assert_text_with_selections(
8915 editor,
8916 indoc! {r#"
8917 use mod1::mod2::«{mod3, mod4}ˇ»;
8918
8919 «ˇfn fn_1(param1: bool, param2: &str) {
8920 let var1 = "text";
8921 }»
8922 "#},
8923 cx,
8924 );
8925 });
8926
8927 editor.update_in(cx, |editor, window, cx| {
8928 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8929 });
8930 assert_eq!(
8931 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8932 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8933 );
8934
8935 // Trying to expand the selected syntax node one more time has no effect.
8936 editor.update_in(cx, |editor, window, cx| {
8937 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8938 });
8939 assert_eq!(
8940 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8941 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8942 );
8943
8944 editor.update_in(cx, |editor, window, cx| {
8945 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8946 });
8947 editor.update(cx, |editor, cx| {
8948 assert_text_with_selections(
8949 editor,
8950 indoc! {r#"
8951 use mod1::mod2::«{mod3, mod4}ˇ»;
8952
8953 «ˇfn fn_1(param1: bool, param2: &str) {
8954 let var1 = "text";
8955 }»
8956 "#},
8957 cx,
8958 );
8959 });
8960
8961 editor.update_in(cx, |editor, window, cx| {
8962 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8963 });
8964 editor.update(cx, |editor, cx| {
8965 assert_text_with_selections(
8966 editor,
8967 indoc! {r#"
8968 use mod1::mod2::{mod3, «mod4ˇ»};
8969
8970 fn fn_1«ˇ(param1: bool, param2: &str)» {
8971 let var1 = "«ˇtext»";
8972 }
8973 "#},
8974 cx,
8975 );
8976 });
8977
8978 editor.update_in(cx, |editor, window, cx| {
8979 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8980 });
8981 editor.update(cx, |editor, cx| {
8982 assert_text_with_selections(
8983 editor,
8984 indoc! {r#"
8985 use mod1::mod2::{mod3, moˇd4};
8986
8987 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8988 let var1 = "teˇxt";
8989 }
8990 "#},
8991 cx,
8992 );
8993 });
8994
8995 // Trying to shrink the selected syntax node one more time has no effect.
8996 editor.update_in(cx, |editor, window, cx| {
8997 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8998 });
8999 editor.update_in(cx, |editor, _, cx| {
9000 assert_text_with_selections(
9001 editor,
9002 indoc! {r#"
9003 use mod1::mod2::{mod3, moˇd4};
9004
9005 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9006 let var1 = "teˇxt";
9007 }
9008 "#},
9009 cx,
9010 );
9011 });
9012
9013 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9014 // a fold.
9015 editor.update_in(cx, |editor, window, cx| {
9016 editor.fold_creases(
9017 vec![
9018 Crease::simple(
9019 Point::new(0, 21)..Point::new(0, 24),
9020 FoldPlaceholder::test(),
9021 ),
9022 Crease::simple(
9023 Point::new(3, 20)..Point::new(3, 22),
9024 FoldPlaceholder::test(),
9025 ),
9026 ],
9027 true,
9028 window,
9029 cx,
9030 );
9031 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9032 });
9033 editor.update(cx, |editor, cx| {
9034 assert_text_with_selections(
9035 editor,
9036 indoc! {r#"
9037 use mod1::mod2::«{mod3, mod4}ˇ»;
9038
9039 fn fn_1«ˇ(param1: bool, param2: &str)» {
9040 let var1 = "«ˇtext»";
9041 }
9042 "#},
9043 cx,
9044 );
9045 });
9046}
9047
9048#[gpui::test]
9049async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9050 init_test(cx, |_| {});
9051
9052 let language = Arc::new(Language::new(
9053 LanguageConfig::default(),
9054 Some(tree_sitter_rust::LANGUAGE.into()),
9055 ));
9056
9057 let text = "let a = 2;";
9058
9059 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9060 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9061 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9062
9063 editor
9064 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9065 .await;
9066
9067 // Test case 1: Cursor at end of word
9068 editor.update_in(cx, |editor, window, cx| {
9069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9070 s.select_display_ranges([
9071 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9072 ]);
9073 });
9074 });
9075 editor.update(cx, |editor, cx| {
9076 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9077 });
9078 editor.update_in(cx, |editor, window, cx| {
9079 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9080 });
9081 editor.update(cx, |editor, cx| {
9082 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9083 });
9084 editor.update_in(cx, |editor, window, cx| {
9085 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9086 });
9087 editor.update(cx, |editor, cx| {
9088 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9089 });
9090
9091 // Test case 2: Cursor at end of statement
9092 editor.update_in(cx, |editor, window, cx| {
9093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9094 s.select_display_ranges([
9095 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9096 ]);
9097 });
9098 });
9099 editor.update(cx, |editor, cx| {
9100 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9101 });
9102 editor.update_in(cx, |editor, window, cx| {
9103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9104 });
9105 editor.update(cx, |editor, cx| {
9106 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9107 });
9108}
9109
9110#[gpui::test]
9111async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9112 init_test(cx, |_| {});
9113
9114 let language = Arc::new(Language::new(
9115 LanguageConfig {
9116 name: "JavaScript".into(),
9117 ..Default::default()
9118 },
9119 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9120 ));
9121
9122 let text = r#"
9123 let a = {
9124 key: "value",
9125 };
9126 "#
9127 .unindent();
9128
9129 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9130 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9131 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9132
9133 editor
9134 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9135 .await;
9136
9137 // Test case 1: Cursor after '{'
9138 editor.update_in(cx, |editor, window, cx| {
9139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9140 s.select_display_ranges([
9141 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9142 ]);
9143 });
9144 });
9145 editor.update(cx, |editor, cx| {
9146 assert_text_with_selections(
9147 editor,
9148 indoc! {r#"
9149 let a = {ˇ
9150 key: "value",
9151 };
9152 "#},
9153 cx,
9154 );
9155 });
9156 editor.update_in(cx, |editor, window, cx| {
9157 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9158 });
9159 editor.update(cx, |editor, cx| {
9160 assert_text_with_selections(
9161 editor,
9162 indoc! {r#"
9163 let a = «ˇ{
9164 key: "value",
9165 }»;
9166 "#},
9167 cx,
9168 );
9169 });
9170
9171 // Test case 2: Cursor after ':'
9172 editor.update_in(cx, |editor, window, cx| {
9173 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9174 s.select_display_ranges([
9175 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9176 ]);
9177 });
9178 });
9179 editor.update(cx, |editor, cx| {
9180 assert_text_with_selections(
9181 editor,
9182 indoc! {r#"
9183 let a = {
9184 key:ˇ "value",
9185 };
9186 "#},
9187 cx,
9188 );
9189 });
9190 editor.update_in(cx, |editor, window, cx| {
9191 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9192 });
9193 editor.update(cx, |editor, cx| {
9194 assert_text_with_selections(
9195 editor,
9196 indoc! {r#"
9197 let a = {
9198 «ˇkey: "value"»,
9199 };
9200 "#},
9201 cx,
9202 );
9203 });
9204 editor.update_in(cx, |editor, window, cx| {
9205 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9206 });
9207 editor.update(cx, |editor, cx| {
9208 assert_text_with_selections(
9209 editor,
9210 indoc! {r#"
9211 let a = «ˇ{
9212 key: "value",
9213 }»;
9214 "#},
9215 cx,
9216 );
9217 });
9218
9219 // Test case 3: Cursor after ','
9220 editor.update_in(cx, |editor, window, cx| {
9221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9222 s.select_display_ranges([
9223 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9224 ]);
9225 });
9226 });
9227 editor.update(cx, |editor, cx| {
9228 assert_text_with_selections(
9229 editor,
9230 indoc! {r#"
9231 let a = {
9232 key: "value",ˇ
9233 };
9234 "#},
9235 cx,
9236 );
9237 });
9238 editor.update_in(cx, |editor, window, cx| {
9239 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9240 });
9241 editor.update(cx, |editor, cx| {
9242 assert_text_with_selections(
9243 editor,
9244 indoc! {r#"
9245 let a = «ˇ{
9246 key: "value",
9247 }»;
9248 "#},
9249 cx,
9250 );
9251 });
9252
9253 // Test case 4: Cursor after ';'
9254 editor.update_in(cx, |editor, window, cx| {
9255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9256 s.select_display_ranges([
9257 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9258 ]);
9259 });
9260 });
9261 editor.update(cx, |editor, cx| {
9262 assert_text_with_selections(
9263 editor,
9264 indoc! {r#"
9265 let a = {
9266 key: "value",
9267 };ˇ
9268 "#},
9269 cx,
9270 );
9271 });
9272 editor.update_in(cx, |editor, window, cx| {
9273 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9274 });
9275 editor.update(cx, |editor, cx| {
9276 assert_text_with_selections(
9277 editor,
9278 indoc! {r#"
9279 «ˇlet a = {
9280 key: "value",
9281 };
9282 »"#},
9283 cx,
9284 );
9285 });
9286}
9287
9288#[gpui::test]
9289async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9290 init_test(cx, |_| {});
9291
9292 let language = Arc::new(Language::new(
9293 LanguageConfig::default(),
9294 Some(tree_sitter_rust::LANGUAGE.into()),
9295 ));
9296
9297 let text = r#"
9298 use mod1::mod2::{mod3, mod4};
9299
9300 fn fn_1(param1: bool, param2: &str) {
9301 let var1 = "hello world";
9302 }
9303 "#
9304 .unindent();
9305
9306 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9307 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9308 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9309
9310 editor
9311 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9312 .await;
9313
9314 // Test 1: Cursor on a letter of a string word
9315 editor.update_in(cx, |editor, window, cx| {
9316 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9317 s.select_display_ranges([
9318 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9319 ]);
9320 });
9321 });
9322 editor.update_in(cx, |editor, window, cx| {
9323 assert_text_with_selections(
9324 editor,
9325 indoc! {r#"
9326 use mod1::mod2::{mod3, mod4};
9327
9328 fn fn_1(param1: bool, param2: &str) {
9329 let var1 = "hˇello world";
9330 }
9331 "#},
9332 cx,
9333 );
9334 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9335 assert_text_with_selections(
9336 editor,
9337 indoc! {r#"
9338 use mod1::mod2::{mod3, mod4};
9339
9340 fn fn_1(param1: bool, param2: &str) {
9341 let var1 = "«ˇhello» world";
9342 }
9343 "#},
9344 cx,
9345 );
9346 });
9347
9348 // Test 2: Partial selection within a word
9349 editor.update_in(cx, |editor, window, cx| {
9350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9351 s.select_display_ranges([
9352 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9353 ]);
9354 });
9355 });
9356 editor.update_in(cx, |editor, window, cx| {
9357 assert_text_with_selections(
9358 editor,
9359 indoc! {r#"
9360 use mod1::mod2::{mod3, mod4};
9361
9362 fn fn_1(param1: bool, param2: &str) {
9363 let var1 = "h«elˇ»lo world";
9364 }
9365 "#},
9366 cx,
9367 );
9368 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9369 assert_text_with_selections(
9370 editor,
9371 indoc! {r#"
9372 use mod1::mod2::{mod3, mod4};
9373
9374 fn fn_1(param1: bool, param2: &str) {
9375 let var1 = "«ˇhello» world";
9376 }
9377 "#},
9378 cx,
9379 );
9380 });
9381
9382 // Test 3: Complete word already selected
9383 editor.update_in(cx, |editor, window, cx| {
9384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9385 s.select_display_ranges([
9386 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9387 ]);
9388 });
9389 });
9390 editor.update_in(cx, |editor, window, cx| {
9391 assert_text_with_selections(
9392 editor,
9393 indoc! {r#"
9394 use mod1::mod2::{mod3, mod4};
9395
9396 fn fn_1(param1: bool, param2: &str) {
9397 let var1 = "«helloˇ» world";
9398 }
9399 "#},
9400 cx,
9401 );
9402 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9403 assert_text_with_selections(
9404 editor,
9405 indoc! {r#"
9406 use mod1::mod2::{mod3, mod4};
9407
9408 fn fn_1(param1: bool, param2: &str) {
9409 let var1 = "«hello worldˇ»";
9410 }
9411 "#},
9412 cx,
9413 );
9414 });
9415
9416 // Test 4: Selection spanning across words
9417 editor.update_in(cx, |editor, window, cx| {
9418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9419 s.select_display_ranges([
9420 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9421 ]);
9422 });
9423 });
9424 editor.update_in(cx, |editor, window, cx| {
9425 assert_text_with_selections(
9426 editor,
9427 indoc! {r#"
9428 use mod1::mod2::{mod3, mod4};
9429
9430 fn fn_1(param1: bool, param2: &str) {
9431 let var1 = "hel«lo woˇ»rld";
9432 }
9433 "#},
9434 cx,
9435 );
9436 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9437 assert_text_with_selections(
9438 editor,
9439 indoc! {r#"
9440 use mod1::mod2::{mod3, mod4};
9441
9442 fn fn_1(param1: bool, param2: &str) {
9443 let var1 = "«ˇhello world»";
9444 }
9445 "#},
9446 cx,
9447 );
9448 });
9449
9450 // Test 5: Expansion beyond string
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9453 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9454 assert_text_with_selections(
9455 editor,
9456 indoc! {r#"
9457 use mod1::mod2::{mod3, mod4};
9458
9459 fn fn_1(param1: bool, param2: &str) {
9460 «ˇlet var1 = "hello world";»
9461 }
9462 "#},
9463 cx,
9464 );
9465 });
9466}
9467
9468#[gpui::test]
9469async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9470 init_test(cx, |_| {});
9471
9472 let mut cx = EditorTestContext::new(cx).await;
9473
9474 let language = Arc::new(Language::new(
9475 LanguageConfig::default(),
9476 Some(tree_sitter_rust::LANGUAGE.into()),
9477 ));
9478
9479 cx.update_buffer(|buffer, cx| {
9480 buffer.set_language(Some(language), cx);
9481 });
9482
9483 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9484 cx.update_editor(|editor, window, cx| {
9485 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9486 });
9487
9488 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9489
9490 cx.set_state(indoc! { r#"fn a() {
9491 // what
9492 // a
9493 // ˇlong
9494 // method
9495 // I
9496 // sure
9497 // hope
9498 // it
9499 // works
9500 }"# });
9501
9502 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9503 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9504 cx.update(|_, cx| {
9505 multi_buffer.update(cx, |multi_buffer, cx| {
9506 multi_buffer.set_excerpts_for_path(
9507 PathKey::for_buffer(&buffer, cx),
9508 buffer,
9509 [Point::new(1, 0)..Point::new(1, 0)],
9510 3,
9511 cx,
9512 );
9513 });
9514 });
9515
9516 let editor2 = cx.new_window_entity(|window, cx| {
9517 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9518 });
9519
9520 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9521 cx.update_editor(|editor, window, cx| {
9522 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9523 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9524 })
9525 });
9526
9527 cx.assert_editor_state(indoc! { "
9528 fn a() {
9529 // what
9530 // a
9531 ˇ // long
9532 // method"});
9533
9534 cx.update_editor(|editor, window, cx| {
9535 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9536 });
9537
9538 // Although we could potentially make the action work when the syntax node
9539 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9540 // did. Maybe we could also expand the excerpt to contain the range?
9541 cx.assert_editor_state(indoc! { "
9542 fn a() {
9543 // what
9544 // a
9545 ˇ // long
9546 // method"});
9547}
9548
9549#[gpui::test]
9550async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9551 init_test(cx, |_| {});
9552
9553 let base_text = r#"
9554 impl A {
9555 // this is an uncommitted comment
9556
9557 fn b() {
9558 c();
9559 }
9560
9561 // this is another uncommitted comment
9562
9563 fn d() {
9564 // e
9565 // f
9566 }
9567 }
9568
9569 fn g() {
9570 // h
9571 }
9572 "#
9573 .unindent();
9574
9575 let text = r#"
9576 ˇimpl A {
9577
9578 fn b() {
9579 c();
9580 }
9581
9582 fn d() {
9583 // e
9584 // f
9585 }
9586 }
9587
9588 fn g() {
9589 // h
9590 }
9591 "#
9592 .unindent();
9593
9594 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9595 cx.set_state(&text);
9596 cx.set_head_text(&base_text);
9597 cx.update_editor(|editor, window, cx| {
9598 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9599 });
9600
9601 cx.assert_state_with_diff(
9602 "
9603 ˇimpl A {
9604 - // this is an uncommitted comment
9605
9606 fn b() {
9607 c();
9608 }
9609
9610 - // this is another uncommitted comment
9611 -
9612 fn d() {
9613 // e
9614 // f
9615 }
9616 }
9617
9618 fn g() {
9619 // h
9620 }
9621 "
9622 .unindent(),
9623 );
9624
9625 let expected_display_text = "
9626 impl A {
9627 // this is an uncommitted comment
9628
9629 fn b() {
9630 ⋯
9631 }
9632
9633 // this is another uncommitted comment
9634
9635 fn d() {
9636 ⋯
9637 }
9638 }
9639
9640 fn g() {
9641 ⋯
9642 }
9643 "
9644 .unindent();
9645
9646 cx.update_editor(|editor, window, cx| {
9647 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9648 assert_eq!(editor.display_text(cx), expected_display_text);
9649 });
9650}
9651
9652#[gpui::test]
9653async fn test_autoindent(cx: &mut TestAppContext) {
9654 init_test(cx, |_| {});
9655
9656 let language = Arc::new(
9657 Language::new(
9658 LanguageConfig {
9659 brackets: BracketPairConfig {
9660 pairs: vec![
9661 BracketPair {
9662 start: "{".to_string(),
9663 end: "}".to_string(),
9664 close: false,
9665 surround: false,
9666 newline: true,
9667 },
9668 BracketPair {
9669 start: "(".to_string(),
9670 end: ")".to_string(),
9671 close: false,
9672 surround: false,
9673 newline: true,
9674 },
9675 ],
9676 ..Default::default()
9677 },
9678 ..Default::default()
9679 },
9680 Some(tree_sitter_rust::LANGUAGE.into()),
9681 )
9682 .with_indents_query(
9683 r#"
9684 (_ "(" ")" @end) @indent
9685 (_ "{" "}" @end) @indent
9686 "#,
9687 )
9688 .unwrap(),
9689 );
9690
9691 let text = "fn a() {}";
9692
9693 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9694 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9695 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9696 editor
9697 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9698 .await;
9699
9700 editor.update_in(cx, |editor, window, cx| {
9701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9702 s.select_ranges([5..5, 8..8, 9..9])
9703 });
9704 editor.newline(&Newline, window, cx);
9705 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9706 assert_eq!(
9707 editor.selections.ranges(&editor.display_snapshot(cx)),
9708 &[
9709 Point::new(1, 4)..Point::new(1, 4),
9710 Point::new(3, 4)..Point::new(3, 4),
9711 Point::new(5, 0)..Point::new(5, 0)
9712 ]
9713 );
9714 });
9715}
9716
9717#[gpui::test]
9718async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9719 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9720
9721 let language = Arc::new(
9722 Language::new(
9723 LanguageConfig {
9724 brackets: BracketPairConfig {
9725 pairs: vec![
9726 BracketPair {
9727 start: "{".to_string(),
9728 end: "}".to_string(),
9729 close: false,
9730 surround: false,
9731 newline: true,
9732 },
9733 BracketPair {
9734 start: "(".to_string(),
9735 end: ")".to_string(),
9736 close: false,
9737 surround: false,
9738 newline: true,
9739 },
9740 ],
9741 ..Default::default()
9742 },
9743 ..Default::default()
9744 },
9745 Some(tree_sitter_rust::LANGUAGE.into()),
9746 )
9747 .with_indents_query(
9748 r#"
9749 (_ "(" ")" @end) @indent
9750 (_ "{" "}" @end) @indent
9751 "#,
9752 )
9753 .unwrap(),
9754 );
9755
9756 let text = "fn a() {}";
9757
9758 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9759 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9760 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9761 editor
9762 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9763 .await;
9764
9765 editor.update_in(cx, |editor, window, cx| {
9766 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9767 s.select_ranges([5..5, 8..8, 9..9])
9768 });
9769 editor.newline(&Newline, window, cx);
9770 assert_eq!(
9771 editor.text(cx),
9772 indoc!(
9773 "
9774 fn a(
9775
9776 ) {
9777
9778 }
9779 "
9780 )
9781 );
9782 assert_eq!(
9783 editor.selections.ranges(&editor.display_snapshot(cx)),
9784 &[
9785 Point::new(1, 0)..Point::new(1, 0),
9786 Point::new(3, 0)..Point::new(3, 0),
9787 Point::new(5, 0)..Point::new(5, 0)
9788 ]
9789 );
9790 });
9791}
9792
9793#[gpui::test]
9794async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9795 init_test(cx, |settings| {
9796 settings.defaults.auto_indent = Some(true);
9797 settings.languages.0.insert(
9798 "python".into(),
9799 LanguageSettingsContent {
9800 auto_indent: Some(false),
9801 ..Default::default()
9802 },
9803 );
9804 });
9805
9806 let mut cx = EditorTestContext::new(cx).await;
9807
9808 let injected_language = Arc::new(
9809 Language::new(
9810 LanguageConfig {
9811 brackets: BracketPairConfig {
9812 pairs: vec![
9813 BracketPair {
9814 start: "{".to_string(),
9815 end: "}".to_string(),
9816 close: false,
9817 surround: false,
9818 newline: true,
9819 },
9820 BracketPair {
9821 start: "(".to_string(),
9822 end: ")".to_string(),
9823 close: true,
9824 surround: false,
9825 newline: true,
9826 },
9827 ],
9828 ..Default::default()
9829 },
9830 name: "python".into(),
9831 ..Default::default()
9832 },
9833 Some(tree_sitter_python::LANGUAGE.into()),
9834 )
9835 .with_indents_query(
9836 r#"
9837 (_ "(" ")" @end) @indent
9838 (_ "{" "}" @end) @indent
9839 "#,
9840 )
9841 .unwrap(),
9842 );
9843
9844 let language = Arc::new(
9845 Language::new(
9846 LanguageConfig {
9847 brackets: BracketPairConfig {
9848 pairs: vec![
9849 BracketPair {
9850 start: "{".to_string(),
9851 end: "}".to_string(),
9852 close: false,
9853 surround: false,
9854 newline: true,
9855 },
9856 BracketPair {
9857 start: "(".to_string(),
9858 end: ")".to_string(),
9859 close: true,
9860 surround: false,
9861 newline: true,
9862 },
9863 ],
9864 ..Default::default()
9865 },
9866 name: LanguageName::new("rust"),
9867 ..Default::default()
9868 },
9869 Some(tree_sitter_rust::LANGUAGE.into()),
9870 )
9871 .with_indents_query(
9872 r#"
9873 (_ "(" ")" @end) @indent
9874 (_ "{" "}" @end) @indent
9875 "#,
9876 )
9877 .unwrap()
9878 .with_injection_query(
9879 r#"
9880 (macro_invocation
9881 macro: (identifier) @_macro_name
9882 (token_tree) @injection.content
9883 (#set! injection.language "python"))
9884 "#,
9885 )
9886 .unwrap(),
9887 );
9888
9889 cx.language_registry().add(injected_language);
9890 cx.language_registry().add(language.clone());
9891
9892 cx.update_buffer(|buffer, cx| {
9893 buffer.set_language(Some(language), cx);
9894 });
9895
9896 cx.set_state(r#"struct A {ˇ}"#);
9897
9898 cx.update_editor(|editor, window, cx| {
9899 editor.newline(&Default::default(), window, cx);
9900 });
9901
9902 cx.assert_editor_state(indoc!(
9903 "struct A {
9904 ˇ
9905 }"
9906 ));
9907
9908 cx.set_state(r#"select_biased!(ˇ)"#);
9909
9910 cx.update_editor(|editor, window, cx| {
9911 editor.newline(&Default::default(), window, cx);
9912 editor.handle_input("def ", window, cx);
9913 editor.handle_input("(", window, cx);
9914 editor.newline(&Default::default(), window, cx);
9915 editor.handle_input("a", window, cx);
9916 });
9917
9918 cx.assert_editor_state(indoc!(
9919 "select_biased!(
9920 def (
9921 aˇ
9922 )
9923 )"
9924 ));
9925}
9926
9927#[gpui::test]
9928async fn test_autoindent_selections(cx: &mut TestAppContext) {
9929 init_test(cx, |_| {});
9930
9931 {
9932 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9933 cx.set_state(indoc! {"
9934 impl A {
9935
9936 fn b() {}
9937
9938 «fn c() {
9939
9940 }ˇ»
9941 }
9942 "});
9943
9944 cx.update_editor(|editor, window, cx| {
9945 editor.autoindent(&Default::default(), window, cx);
9946 });
9947
9948 cx.assert_editor_state(indoc! {"
9949 impl A {
9950
9951 fn b() {}
9952
9953 «fn c() {
9954
9955 }ˇ»
9956 }
9957 "});
9958 }
9959
9960 {
9961 let mut cx = EditorTestContext::new_multibuffer(
9962 cx,
9963 [indoc! { "
9964 impl A {
9965 «
9966 // a
9967 fn b(){}
9968 »
9969 «
9970 }
9971 fn c(){}
9972 »
9973 "}],
9974 );
9975
9976 let buffer = cx.update_editor(|editor, _, cx| {
9977 let buffer = editor.buffer().update(cx, |buffer, _| {
9978 buffer.all_buffers().iter().next().unwrap().clone()
9979 });
9980 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9981 buffer
9982 });
9983
9984 cx.run_until_parked();
9985 cx.update_editor(|editor, window, cx| {
9986 editor.select_all(&Default::default(), window, cx);
9987 editor.autoindent(&Default::default(), window, cx)
9988 });
9989 cx.run_until_parked();
9990
9991 cx.update(|_, cx| {
9992 assert_eq!(
9993 buffer.read(cx).text(),
9994 indoc! { "
9995 impl A {
9996
9997 // a
9998 fn b(){}
9999
10000
10001 }
10002 fn c(){}
10003
10004 " }
10005 )
10006 });
10007 }
10008}
10009
10010#[gpui::test]
10011async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10012 init_test(cx, |_| {});
10013
10014 let mut cx = EditorTestContext::new(cx).await;
10015
10016 let language = Arc::new(Language::new(
10017 LanguageConfig {
10018 brackets: BracketPairConfig {
10019 pairs: vec![
10020 BracketPair {
10021 start: "{".to_string(),
10022 end: "}".to_string(),
10023 close: true,
10024 surround: true,
10025 newline: true,
10026 },
10027 BracketPair {
10028 start: "(".to_string(),
10029 end: ")".to_string(),
10030 close: true,
10031 surround: true,
10032 newline: true,
10033 },
10034 BracketPair {
10035 start: "/*".to_string(),
10036 end: " */".to_string(),
10037 close: true,
10038 surround: true,
10039 newline: true,
10040 },
10041 BracketPair {
10042 start: "[".to_string(),
10043 end: "]".to_string(),
10044 close: false,
10045 surround: false,
10046 newline: true,
10047 },
10048 BracketPair {
10049 start: "\"".to_string(),
10050 end: "\"".to_string(),
10051 close: true,
10052 surround: true,
10053 newline: false,
10054 },
10055 BracketPair {
10056 start: "<".to_string(),
10057 end: ">".to_string(),
10058 close: false,
10059 surround: true,
10060 newline: true,
10061 },
10062 ],
10063 ..Default::default()
10064 },
10065 autoclose_before: "})]".to_string(),
10066 ..Default::default()
10067 },
10068 Some(tree_sitter_rust::LANGUAGE.into()),
10069 ));
10070
10071 cx.language_registry().add(language.clone());
10072 cx.update_buffer(|buffer, cx| {
10073 buffer.set_language(Some(language), cx);
10074 });
10075
10076 cx.set_state(
10077 &r#"
10078 🏀ˇ
10079 εˇ
10080 ❤️ˇ
10081 "#
10082 .unindent(),
10083 );
10084
10085 // autoclose multiple nested brackets at multiple cursors
10086 cx.update_editor(|editor, window, cx| {
10087 editor.handle_input("{", window, cx);
10088 editor.handle_input("{", window, cx);
10089 editor.handle_input("{", window, cx);
10090 });
10091 cx.assert_editor_state(
10092 &"
10093 🏀{{{ˇ}}}
10094 ε{{{ˇ}}}
10095 ❤️{{{ˇ}}}
10096 "
10097 .unindent(),
10098 );
10099
10100 // insert a different closing bracket
10101 cx.update_editor(|editor, window, cx| {
10102 editor.handle_input(")", window, cx);
10103 });
10104 cx.assert_editor_state(
10105 &"
10106 🏀{{{)ˇ}}}
10107 ε{{{)ˇ}}}
10108 ❤️{{{)ˇ}}}
10109 "
10110 .unindent(),
10111 );
10112
10113 // skip over the auto-closed brackets when typing a closing bracket
10114 cx.update_editor(|editor, window, cx| {
10115 editor.move_right(&MoveRight, window, cx);
10116 editor.handle_input("}", window, cx);
10117 editor.handle_input("}", window, cx);
10118 editor.handle_input("}", window, cx);
10119 });
10120 cx.assert_editor_state(
10121 &"
10122 🏀{{{)}}}}ˇ
10123 ε{{{)}}}}ˇ
10124 ❤️{{{)}}}}ˇ
10125 "
10126 .unindent(),
10127 );
10128
10129 // autoclose multi-character pairs
10130 cx.set_state(
10131 &"
10132 ˇ
10133 ˇ
10134 "
10135 .unindent(),
10136 );
10137 cx.update_editor(|editor, window, cx| {
10138 editor.handle_input("/", window, cx);
10139 editor.handle_input("*", window, cx);
10140 });
10141 cx.assert_editor_state(
10142 &"
10143 /*ˇ */
10144 /*ˇ */
10145 "
10146 .unindent(),
10147 );
10148
10149 // one cursor autocloses a multi-character pair, one cursor
10150 // does not autoclose.
10151 cx.set_state(
10152 &"
10153 /ˇ
10154 ˇ
10155 "
10156 .unindent(),
10157 );
10158 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10159 cx.assert_editor_state(
10160 &"
10161 /*ˇ */
10162 *ˇ
10163 "
10164 .unindent(),
10165 );
10166
10167 // Don't autoclose if the next character isn't whitespace and isn't
10168 // listed in the language's "autoclose_before" section.
10169 cx.set_state("ˇa b");
10170 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10171 cx.assert_editor_state("{ˇa b");
10172
10173 // Don't autoclose if `close` is false for the bracket pair
10174 cx.set_state("ˇ");
10175 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10176 cx.assert_editor_state("[ˇ");
10177
10178 // Surround with brackets if text is selected
10179 cx.set_state("«aˇ» b");
10180 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10181 cx.assert_editor_state("{«aˇ»} b");
10182
10183 // Autoclose when not immediately after a word character
10184 cx.set_state("a ˇ");
10185 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10186 cx.assert_editor_state("a \"ˇ\"");
10187
10188 // Autoclose pair where the start and end characters are the same
10189 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10190 cx.assert_editor_state("a \"\"ˇ");
10191
10192 // Don't autoclose when immediately after a word character
10193 cx.set_state("aˇ");
10194 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10195 cx.assert_editor_state("a\"ˇ");
10196
10197 // Do autoclose when after a non-word character
10198 cx.set_state("{ˇ");
10199 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10200 cx.assert_editor_state("{\"ˇ\"");
10201
10202 // Non identical pairs autoclose regardless of preceding character
10203 cx.set_state("aˇ");
10204 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10205 cx.assert_editor_state("a{ˇ}");
10206
10207 // Don't autoclose pair if autoclose is disabled
10208 cx.set_state("ˇ");
10209 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10210 cx.assert_editor_state("<ˇ");
10211
10212 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10213 cx.set_state("«aˇ» b");
10214 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10215 cx.assert_editor_state("<«aˇ»> b");
10216}
10217
10218#[gpui::test]
10219async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10220 init_test(cx, |settings| {
10221 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10222 });
10223
10224 let mut cx = EditorTestContext::new(cx).await;
10225
10226 let language = Arc::new(Language::new(
10227 LanguageConfig {
10228 brackets: BracketPairConfig {
10229 pairs: vec![
10230 BracketPair {
10231 start: "{".to_string(),
10232 end: "}".to_string(),
10233 close: true,
10234 surround: true,
10235 newline: true,
10236 },
10237 BracketPair {
10238 start: "(".to_string(),
10239 end: ")".to_string(),
10240 close: true,
10241 surround: true,
10242 newline: true,
10243 },
10244 BracketPair {
10245 start: "[".to_string(),
10246 end: "]".to_string(),
10247 close: false,
10248 surround: false,
10249 newline: true,
10250 },
10251 ],
10252 ..Default::default()
10253 },
10254 autoclose_before: "})]".to_string(),
10255 ..Default::default()
10256 },
10257 Some(tree_sitter_rust::LANGUAGE.into()),
10258 ));
10259
10260 cx.language_registry().add(language.clone());
10261 cx.update_buffer(|buffer, cx| {
10262 buffer.set_language(Some(language), cx);
10263 });
10264
10265 cx.set_state(
10266 &"
10267 ˇ
10268 ˇ
10269 ˇ
10270 "
10271 .unindent(),
10272 );
10273
10274 // ensure only matching closing brackets are skipped over
10275 cx.update_editor(|editor, window, cx| {
10276 editor.handle_input("}", window, cx);
10277 editor.move_left(&MoveLeft, window, cx);
10278 editor.handle_input(")", window, cx);
10279 editor.move_left(&MoveLeft, window, cx);
10280 });
10281 cx.assert_editor_state(
10282 &"
10283 ˇ)}
10284 ˇ)}
10285 ˇ)}
10286 "
10287 .unindent(),
10288 );
10289
10290 // skip-over closing brackets at multiple cursors
10291 cx.update_editor(|editor, window, cx| {
10292 editor.handle_input(")", window, cx);
10293 editor.handle_input("}", window, cx);
10294 });
10295 cx.assert_editor_state(
10296 &"
10297 )}ˇ
10298 )}ˇ
10299 )}ˇ
10300 "
10301 .unindent(),
10302 );
10303
10304 // ignore non-close brackets
10305 cx.update_editor(|editor, window, cx| {
10306 editor.handle_input("]", window, cx);
10307 editor.move_left(&MoveLeft, window, cx);
10308 editor.handle_input("]", window, cx);
10309 });
10310 cx.assert_editor_state(
10311 &"
10312 )}]ˇ]
10313 )}]ˇ]
10314 )}]ˇ]
10315 "
10316 .unindent(),
10317 );
10318}
10319
10320#[gpui::test]
10321async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10322 init_test(cx, |_| {});
10323
10324 let mut cx = EditorTestContext::new(cx).await;
10325
10326 let html_language = Arc::new(
10327 Language::new(
10328 LanguageConfig {
10329 name: "HTML".into(),
10330 brackets: BracketPairConfig {
10331 pairs: vec![
10332 BracketPair {
10333 start: "<".into(),
10334 end: ">".into(),
10335 close: true,
10336 ..Default::default()
10337 },
10338 BracketPair {
10339 start: "{".into(),
10340 end: "}".into(),
10341 close: true,
10342 ..Default::default()
10343 },
10344 BracketPair {
10345 start: "(".into(),
10346 end: ")".into(),
10347 close: true,
10348 ..Default::default()
10349 },
10350 ],
10351 ..Default::default()
10352 },
10353 autoclose_before: "})]>".into(),
10354 ..Default::default()
10355 },
10356 Some(tree_sitter_html::LANGUAGE.into()),
10357 )
10358 .with_injection_query(
10359 r#"
10360 (script_element
10361 (raw_text) @injection.content
10362 (#set! injection.language "javascript"))
10363 "#,
10364 )
10365 .unwrap(),
10366 );
10367
10368 let javascript_language = Arc::new(Language::new(
10369 LanguageConfig {
10370 name: "JavaScript".into(),
10371 brackets: BracketPairConfig {
10372 pairs: vec![
10373 BracketPair {
10374 start: "/*".into(),
10375 end: " */".into(),
10376 close: true,
10377 ..Default::default()
10378 },
10379 BracketPair {
10380 start: "{".into(),
10381 end: "}".into(),
10382 close: true,
10383 ..Default::default()
10384 },
10385 BracketPair {
10386 start: "(".into(),
10387 end: ")".into(),
10388 close: true,
10389 ..Default::default()
10390 },
10391 ],
10392 ..Default::default()
10393 },
10394 autoclose_before: "})]>".into(),
10395 ..Default::default()
10396 },
10397 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10398 ));
10399
10400 cx.language_registry().add(html_language.clone());
10401 cx.language_registry().add(javascript_language);
10402 cx.executor().run_until_parked();
10403
10404 cx.update_buffer(|buffer, cx| {
10405 buffer.set_language(Some(html_language), cx);
10406 });
10407
10408 cx.set_state(
10409 &r#"
10410 <body>ˇ
10411 <script>
10412 var x = 1;ˇ
10413 </script>
10414 </body>ˇ
10415 "#
10416 .unindent(),
10417 );
10418
10419 // Precondition: different languages are active at different locations.
10420 cx.update_editor(|editor, window, cx| {
10421 let snapshot = editor.snapshot(window, cx);
10422 let cursors = editor
10423 .selections
10424 .ranges::<usize>(&editor.display_snapshot(cx));
10425 let languages = cursors
10426 .iter()
10427 .map(|c| snapshot.language_at(c.start).unwrap().name())
10428 .collect::<Vec<_>>();
10429 assert_eq!(
10430 languages,
10431 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10432 );
10433 });
10434
10435 // Angle brackets autoclose in HTML, but not JavaScript.
10436 cx.update_editor(|editor, window, cx| {
10437 editor.handle_input("<", window, cx);
10438 editor.handle_input("a", window, cx);
10439 });
10440 cx.assert_editor_state(
10441 &r#"
10442 <body><aˇ>
10443 <script>
10444 var x = 1;<aˇ
10445 </script>
10446 </body><aˇ>
10447 "#
10448 .unindent(),
10449 );
10450
10451 // Curly braces and parens autoclose in both HTML and JavaScript.
10452 cx.update_editor(|editor, window, cx| {
10453 editor.handle_input(" b=", window, cx);
10454 editor.handle_input("{", window, cx);
10455 editor.handle_input("c", window, cx);
10456 editor.handle_input("(", window, cx);
10457 });
10458 cx.assert_editor_state(
10459 &r#"
10460 <body><a b={c(ˇ)}>
10461 <script>
10462 var x = 1;<a b={c(ˇ)}
10463 </script>
10464 </body><a b={c(ˇ)}>
10465 "#
10466 .unindent(),
10467 );
10468
10469 // Brackets that were already autoclosed are skipped.
10470 cx.update_editor(|editor, window, cx| {
10471 editor.handle_input(")", window, cx);
10472 editor.handle_input("d", window, cx);
10473 editor.handle_input("}", window, cx);
10474 });
10475 cx.assert_editor_state(
10476 &r#"
10477 <body><a b={c()d}ˇ>
10478 <script>
10479 var x = 1;<a b={c()d}ˇ
10480 </script>
10481 </body><a b={c()d}ˇ>
10482 "#
10483 .unindent(),
10484 );
10485 cx.update_editor(|editor, window, cx| {
10486 editor.handle_input(">", window, cx);
10487 });
10488 cx.assert_editor_state(
10489 &r#"
10490 <body><a b={c()d}>ˇ
10491 <script>
10492 var x = 1;<a b={c()d}>ˇ
10493 </script>
10494 </body><a b={c()d}>ˇ
10495 "#
10496 .unindent(),
10497 );
10498
10499 // Reset
10500 cx.set_state(
10501 &r#"
10502 <body>ˇ
10503 <script>
10504 var x = 1;ˇ
10505 </script>
10506 </body>ˇ
10507 "#
10508 .unindent(),
10509 );
10510
10511 cx.update_editor(|editor, window, cx| {
10512 editor.handle_input("<", window, cx);
10513 });
10514 cx.assert_editor_state(
10515 &r#"
10516 <body><ˇ>
10517 <script>
10518 var x = 1;<ˇ
10519 </script>
10520 </body><ˇ>
10521 "#
10522 .unindent(),
10523 );
10524
10525 // When backspacing, the closing angle brackets are removed.
10526 cx.update_editor(|editor, window, cx| {
10527 editor.backspace(&Backspace, window, cx);
10528 });
10529 cx.assert_editor_state(
10530 &r#"
10531 <body>ˇ
10532 <script>
10533 var x = 1;ˇ
10534 </script>
10535 </body>ˇ
10536 "#
10537 .unindent(),
10538 );
10539
10540 // Block comments autoclose in JavaScript, but not HTML.
10541 cx.update_editor(|editor, window, cx| {
10542 editor.handle_input("/", window, cx);
10543 editor.handle_input("*", window, cx);
10544 });
10545 cx.assert_editor_state(
10546 &r#"
10547 <body>/*ˇ
10548 <script>
10549 var x = 1;/*ˇ */
10550 </script>
10551 </body>/*ˇ
10552 "#
10553 .unindent(),
10554 );
10555}
10556
10557#[gpui::test]
10558async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10559 init_test(cx, |_| {});
10560
10561 let mut cx = EditorTestContext::new(cx).await;
10562
10563 let rust_language = Arc::new(
10564 Language::new(
10565 LanguageConfig {
10566 name: "Rust".into(),
10567 brackets: serde_json::from_value(json!([
10568 { "start": "{", "end": "}", "close": true, "newline": true },
10569 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10570 ]))
10571 .unwrap(),
10572 autoclose_before: "})]>".into(),
10573 ..Default::default()
10574 },
10575 Some(tree_sitter_rust::LANGUAGE.into()),
10576 )
10577 .with_override_query("(string_literal) @string")
10578 .unwrap(),
10579 );
10580
10581 cx.language_registry().add(rust_language.clone());
10582 cx.update_buffer(|buffer, cx| {
10583 buffer.set_language(Some(rust_language), cx);
10584 });
10585
10586 cx.set_state(
10587 &r#"
10588 let x = ˇ
10589 "#
10590 .unindent(),
10591 );
10592
10593 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10594 cx.update_editor(|editor, window, cx| {
10595 editor.handle_input("\"", window, cx);
10596 });
10597 cx.assert_editor_state(
10598 &r#"
10599 let x = "ˇ"
10600 "#
10601 .unindent(),
10602 );
10603
10604 // Inserting another quotation mark. The cursor moves across the existing
10605 // automatically-inserted quotation mark.
10606 cx.update_editor(|editor, window, cx| {
10607 editor.handle_input("\"", window, cx);
10608 });
10609 cx.assert_editor_state(
10610 &r#"
10611 let x = ""ˇ
10612 "#
10613 .unindent(),
10614 );
10615
10616 // Reset
10617 cx.set_state(
10618 &r#"
10619 let x = ˇ
10620 "#
10621 .unindent(),
10622 );
10623
10624 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10625 cx.update_editor(|editor, window, cx| {
10626 editor.handle_input("\"", window, cx);
10627 editor.handle_input(" ", window, cx);
10628 editor.move_left(&Default::default(), window, cx);
10629 editor.handle_input("\\", window, cx);
10630 editor.handle_input("\"", window, cx);
10631 });
10632 cx.assert_editor_state(
10633 &r#"
10634 let x = "\"ˇ "
10635 "#
10636 .unindent(),
10637 );
10638
10639 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10640 // mark. Nothing is inserted.
10641 cx.update_editor(|editor, window, cx| {
10642 editor.move_right(&Default::default(), window, cx);
10643 editor.handle_input("\"", window, cx);
10644 });
10645 cx.assert_editor_state(
10646 &r#"
10647 let x = "\" "ˇ
10648 "#
10649 .unindent(),
10650 );
10651}
10652
10653#[gpui::test]
10654async fn test_surround_with_pair(cx: &mut TestAppContext) {
10655 init_test(cx, |_| {});
10656
10657 let language = Arc::new(Language::new(
10658 LanguageConfig {
10659 brackets: BracketPairConfig {
10660 pairs: vec![
10661 BracketPair {
10662 start: "{".to_string(),
10663 end: "}".to_string(),
10664 close: true,
10665 surround: true,
10666 newline: true,
10667 },
10668 BracketPair {
10669 start: "/* ".to_string(),
10670 end: "*/".to_string(),
10671 close: true,
10672 surround: true,
10673 ..Default::default()
10674 },
10675 ],
10676 ..Default::default()
10677 },
10678 ..Default::default()
10679 },
10680 Some(tree_sitter_rust::LANGUAGE.into()),
10681 ));
10682
10683 let text = r#"
10684 a
10685 b
10686 c
10687 "#
10688 .unindent();
10689
10690 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10691 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10692 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10693 editor
10694 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10695 .await;
10696
10697 editor.update_in(cx, |editor, window, cx| {
10698 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10699 s.select_display_ranges([
10700 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10701 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10702 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10703 ])
10704 });
10705
10706 editor.handle_input("{", window, cx);
10707 editor.handle_input("{", window, cx);
10708 editor.handle_input("{", window, cx);
10709 assert_eq!(
10710 editor.text(cx),
10711 "
10712 {{{a}}}
10713 {{{b}}}
10714 {{{c}}}
10715 "
10716 .unindent()
10717 );
10718 assert_eq!(
10719 editor.selections.display_ranges(cx),
10720 [
10721 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10722 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10723 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10724 ]
10725 );
10726
10727 editor.undo(&Undo, window, cx);
10728 editor.undo(&Undo, window, cx);
10729 editor.undo(&Undo, window, cx);
10730 assert_eq!(
10731 editor.text(cx),
10732 "
10733 a
10734 b
10735 c
10736 "
10737 .unindent()
10738 );
10739 assert_eq!(
10740 editor.selections.display_ranges(cx),
10741 [
10742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10743 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10744 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10745 ]
10746 );
10747
10748 // Ensure inserting the first character of a multi-byte bracket pair
10749 // doesn't surround the selections with the bracket.
10750 editor.handle_input("/", window, cx);
10751 assert_eq!(
10752 editor.text(cx),
10753 "
10754 /
10755 /
10756 /
10757 "
10758 .unindent()
10759 );
10760 assert_eq!(
10761 editor.selections.display_ranges(cx),
10762 [
10763 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10764 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10765 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10766 ]
10767 );
10768
10769 editor.undo(&Undo, window, cx);
10770 assert_eq!(
10771 editor.text(cx),
10772 "
10773 a
10774 b
10775 c
10776 "
10777 .unindent()
10778 );
10779 assert_eq!(
10780 editor.selections.display_ranges(cx),
10781 [
10782 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10783 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10784 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10785 ]
10786 );
10787
10788 // Ensure inserting the last character of a multi-byte bracket pair
10789 // doesn't surround the selections with the bracket.
10790 editor.handle_input("*", window, cx);
10791 assert_eq!(
10792 editor.text(cx),
10793 "
10794 *
10795 *
10796 *
10797 "
10798 .unindent()
10799 );
10800 assert_eq!(
10801 editor.selections.display_ranges(cx),
10802 [
10803 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10804 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10805 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10806 ]
10807 );
10808 });
10809}
10810
10811#[gpui::test]
10812async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10813 init_test(cx, |_| {});
10814
10815 let language = Arc::new(Language::new(
10816 LanguageConfig {
10817 brackets: BracketPairConfig {
10818 pairs: vec![BracketPair {
10819 start: "{".to_string(),
10820 end: "}".to_string(),
10821 close: true,
10822 surround: true,
10823 newline: true,
10824 }],
10825 ..Default::default()
10826 },
10827 autoclose_before: "}".to_string(),
10828 ..Default::default()
10829 },
10830 Some(tree_sitter_rust::LANGUAGE.into()),
10831 ));
10832
10833 let text = r#"
10834 a
10835 b
10836 c
10837 "#
10838 .unindent();
10839
10840 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10841 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10842 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10843 editor
10844 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10845 .await;
10846
10847 editor.update_in(cx, |editor, window, cx| {
10848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10849 s.select_ranges([
10850 Point::new(0, 1)..Point::new(0, 1),
10851 Point::new(1, 1)..Point::new(1, 1),
10852 Point::new(2, 1)..Point::new(2, 1),
10853 ])
10854 });
10855
10856 editor.handle_input("{", window, cx);
10857 editor.handle_input("{", window, cx);
10858 editor.handle_input("_", window, cx);
10859 assert_eq!(
10860 editor.text(cx),
10861 "
10862 a{{_}}
10863 b{{_}}
10864 c{{_}}
10865 "
10866 .unindent()
10867 );
10868 assert_eq!(
10869 editor
10870 .selections
10871 .ranges::<Point>(&editor.display_snapshot(cx)),
10872 [
10873 Point::new(0, 4)..Point::new(0, 4),
10874 Point::new(1, 4)..Point::new(1, 4),
10875 Point::new(2, 4)..Point::new(2, 4)
10876 ]
10877 );
10878
10879 editor.backspace(&Default::default(), window, cx);
10880 editor.backspace(&Default::default(), window, cx);
10881 assert_eq!(
10882 editor.text(cx),
10883 "
10884 a{}
10885 b{}
10886 c{}
10887 "
10888 .unindent()
10889 );
10890 assert_eq!(
10891 editor
10892 .selections
10893 .ranges::<Point>(&editor.display_snapshot(cx)),
10894 [
10895 Point::new(0, 2)..Point::new(0, 2),
10896 Point::new(1, 2)..Point::new(1, 2),
10897 Point::new(2, 2)..Point::new(2, 2)
10898 ]
10899 );
10900
10901 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10902 assert_eq!(
10903 editor.text(cx),
10904 "
10905 a
10906 b
10907 c
10908 "
10909 .unindent()
10910 );
10911 assert_eq!(
10912 editor
10913 .selections
10914 .ranges::<Point>(&editor.display_snapshot(cx)),
10915 [
10916 Point::new(0, 1)..Point::new(0, 1),
10917 Point::new(1, 1)..Point::new(1, 1),
10918 Point::new(2, 1)..Point::new(2, 1)
10919 ]
10920 );
10921 });
10922}
10923
10924#[gpui::test]
10925async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10926 init_test(cx, |settings| {
10927 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10928 });
10929
10930 let mut cx = EditorTestContext::new(cx).await;
10931
10932 let language = Arc::new(Language::new(
10933 LanguageConfig {
10934 brackets: BracketPairConfig {
10935 pairs: vec![
10936 BracketPair {
10937 start: "{".to_string(),
10938 end: "}".to_string(),
10939 close: true,
10940 surround: true,
10941 newline: true,
10942 },
10943 BracketPair {
10944 start: "(".to_string(),
10945 end: ")".to_string(),
10946 close: true,
10947 surround: true,
10948 newline: true,
10949 },
10950 BracketPair {
10951 start: "[".to_string(),
10952 end: "]".to_string(),
10953 close: false,
10954 surround: true,
10955 newline: true,
10956 },
10957 ],
10958 ..Default::default()
10959 },
10960 autoclose_before: "})]".to_string(),
10961 ..Default::default()
10962 },
10963 Some(tree_sitter_rust::LANGUAGE.into()),
10964 ));
10965
10966 cx.language_registry().add(language.clone());
10967 cx.update_buffer(|buffer, cx| {
10968 buffer.set_language(Some(language), cx);
10969 });
10970
10971 cx.set_state(
10972 &"
10973 {(ˇ)}
10974 [[ˇ]]
10975 {(ˇ)}
10976 "
10977 .unindent(),
10978 );
10979
10980 cx.update_editor(|editor, window, cx| {
10981 editor.backspace(&Default::default(), window, cx);
10982 editor.backspace(&Default::default(), window, cx);
10983 });
10984
10985 cx.assert_editor_state(
10986 &"
10987 ˇ
10988 ˇ]]
10989 ˇ
10990 "
10991 .unindent(),
10992 );
10993
10994 cx.update_editor(|editor, window, cx| {
10995 editor.handle_input("{", window, cx);
10996 editor.handle_input("{", window, cx);
10997 editor.move_right(&MoveRight, window, cx);
10998 editor.move_right(&MoveRight, window, cx);
10999 editor.move_left(&MoveLeft, window, cx);
11000 editor.move_left(&MoveLeft, window, cx);
11001 editor.backspace(&Default::default(), window, cx);
11002 });
11003
11004 cx.assert_editor_state(
11005 &"
11006 {ˇ}
11007 {ˇ}]]
11008 {ˇ}
11009 "
11010 .unindent(),
11011 );
11012
11013 cx.update_editor(|editor, window, cx| {
11014 editor.backspace(&Default::default(), window, cx);
11015 });
11016
11017 cx.assert_editor_state(
11018 &"
11019 ˇ
11020 ˇ]]
11021 ˇ
11022 "
11023 .unindent(),
11024 );
11025}
11026
11027#[gpui::test]
11028async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11029 init_test(cx, |_| {});
11030
11031 let language = Arc::new(Language::new(
11032 LanguageConfig::default(),
11033 Some(tree_sitter_rust::LANGUAGE.into()),
11034 ));
11035
11036 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11037 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11038 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11039 editor
11040 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11041 .await;
11042
11043 editor.update_in(cx, |editor, window, cx| {
11044 editor.set_auto_replace_emoji_shortcode(true);
11045
11046 editor.handle_input("Hello ", window, cx);
11047 editor.handle_input(":wave", window, cx);
11048 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11049
11050 editor.handle_input(":", window, cx);
11051 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11052
11053 editor.handle_input(" :smile", window, cx);
11054 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11055
11056 editor.handle_input(":", window, cx);
11057 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11058
11059 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11060 editor.handle_input(":wave", window, cx);
11061 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11062
11063 editor.handle_input(":", window, cx);
11064 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11065
11066 editor.handle_input(":1", window, cx);
11067 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11068
11069 editor.handle_input(":", window, cx);
11070 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11071
11072 // Ensure shortcode does not get replaced when it is part of a word
11073 editor.handle_input(" Test:wave", window, cx);
11074 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11075
11076 editor.handle_input(":", window, cx);
11077 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11078
11079 editor.set_auto_replace_emoji_shortcode(false);
11080
11081 // Ensure shortcode does not get replaced when auto replace is off
11082 editor.handle_input(" :wave", window, cx);
11083 assert_eq!(
11084 editor.text(cx),
11085 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11086 );
11087
11088 editor.handle_input(":", window, cx);
11089 assert_eq!(
11090 editor.text(cx),
11091 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11092 );
11093 });
11094}
11095
11096#[gpui::test]
11097async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11098 init_test(cx, |_| {});
11099
11100 let (text, insertion_ranges) = marked_text_ranges(
11101 indoc! {"
11102 ˇ
11103 "},
11104 false,
11105 );
11106
11107 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11108 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11109
11110 _ = editor.update_in(cx, |editor, window, cx| {
11111 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11112
11113 editor
11114 .insert_snippet(&insertion_ranges, snippet, window, cx)
11115 .unwrap();
11116
11117 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11118 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11119 assert_eq!(editor.text(cx), expected_text);
11120 assert_eq!(
11121 editor
11122 .selections
11123 .ranges::<usize>(&editor.display_snapshot(cx)),
11124 selection_ranges
11125 );
11126 }
11127
11128 assert(
11129 editor,
11130 cx,
11131 indoc! {"
11132 type «» =•
11133 "},
11134 );
11135
11136 assert!(editor.context_menu_visible(), "There should be a matches");
11137 });
11138}
11139
11140#[gpui::test]
11141async fn test_snippets(cx: &mut TestAppContext) {
11142 init_test(cx, |_| {});
11143
11144 let mut cx = EditorTestContext::new(cx).await;
11145
11146 cx.set_state(indoc! {"
11147 a.ˇ b
11148 a.ˇ b
11149 a.ˇ b
11150 "});
11151
11152 cx.update_editor(|editor, window, cx| {
11153 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11154 let insertion_ranges = editor
11155 .selections
11156 .all(&editor.display_snapshot(cx))
11157 .iter()
11158 .map(|s| s.range())
11159 .collect::<Vec<_>>();
11160 editor
11161 .insert_snippet(&insertion_ranges, snippet, window, cx)
11162 .unwrap();
11163 });
11164
11165 cx.assert_editor_state(indoc! {"
11166 a.f(«oneˇ», two, «threeˇ») b
11167 a.f(«oneˇ», two, «threeˇ») b
11168 a.f(«oneˇ», two, «threeˇ») b
11169 "});
11170
11171 // Can't move earlier than the first tab stop
11172 cx.update_editor(|editor, window, cx| {
11173 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11174 });
11175 cx.assert_editor_state(indoc! {"
11176 a.f(«oneˇ», two, «threeˇ») b
11177 a.f(«oneˇ», two, «threeˇ») b
11178 a.f(«oneˇ», two, «threeˇ») b
11179 "});
11180
11181 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11182 cx.assert_editor_state(indoc! {"
11183 a.f(one, «twoˇ», three) b
11184 a.f(one, «twoˇ», three) b
11185 a.f(one, «twoˇ», three) b
11186 "});
11187
11188 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11189 cx.assert_editor_state(indoc! {"
11190 a.f(«oneˇ», two, «threeˇ») b
11191 a.f(«oneˇ», two, «threeˇ») b
11192 a.f(«oneˇ», two, «threeˇ») b
11193 "});
11194
11195 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11196 cx.assert_editor_state(indoc! {"
11197 a.f(one, «twoˇ», three) b
11198 a.f(one, «twoˇ», three) b
11199 a.f(one, «twoˇ», three) b
11200 "});
11201 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11202 cx.assert_editor_state(indoc! {"
11203 a.f(one, two, three)ˇ b
11204 a.f(one, two, three)ˇ b
11205 a.f(one, two, three)ˇ b
11206 "});
11207
11208 // As soon as the last tab stop is reached, snippet state is gone
11209 cx.update_editor(|editor, window, cx| {
11210 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11211 });
11212 cx.assert_editor_state(indoc! {"
11213 a.f(one, two, three)ˇ b
11214 a.f(one, two, three)ˇ b
11215 a.f(one, two, three)ˇ b
11216 "});
11217}
11218
11219#[gpui::test]
11220async fn test_snippet_indentation(cx: &mut TestAppContext) {
11221 init_test(cx, |_| {});
11222
11223 let mut cx = EditorTestContext::new(cx).await;
11224
11225 cx.update_editor(|editor, window, cx| {
11226 let snippet = Snippet::parse(indoc! {"
11227 /*
11228 * Multiline comment with leading indentation
11229 *
11230 * $1
11231 */
11232 $0"})
11233 .unwrap();
11234 let insertion_ranges = editor
11235 .selections
11236 .all(&editor.display_snapshot(cx))
11237 .iter()
11238 .map(|s| s.range())
11239 .collect::<Vec<_>>();
11240 editor
11241 .insert_snippet(&insertion_ranges, snippet, window, cx)
11242 .unwrap();
11243 });
11244
11245 cx.assert_editor_state(indoc! {"
11246 /*
11247 * Multiline comment with leading indentation
11248 *
11249 * ˇ
11250 */
11251 "});
11252
11253 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11254 cx.assert_editor_state(indoc! {"
11255 /*
11256 * Multiline comment with leading indentation
11257 *
11258 *•
11259 */
11260 ˇ"});
11261}
11262
11263#[gpui::test]
11264async fn test_document_format_during_save(cx: &mut TestAppContext) {
11265 init_test(cx, |_| {});
11266
11267 let fs = FakeFs::new(cx.executor());
11268 fs.insert_file(path!("/file.rs"), Default::default()).await;
11269
11270 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11271
11272 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11273 language_registry.add(rust_lang());
11274 let mut fake_servers = language_registry.register_fake_lsp(
11275 "Rust",
11276 FakeLspAdapter {
11277 capabilities: lsp::ServerCapabilities {
11278 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11279 ..Default::default()
11280 },
11281 ..Default::default()
11282 },
11283 );
11284
11285 let buffer = project
11286 .update(cx, |project, cx| {
11287 project.open_local_buffer(path!("/file.rs"), cx)
11288 })
11289 .await
11290 .unwrap();
11291
11292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11293 let (editor, cx) = cx.add_window_view(|window, cx| {
11294 build_editor_with_project(project.clone(), buffer, window, cx)
11295 });
11296 editor.update_in(cx, |editor, window, cx| {
11297 editor.set_text("one\ntwo\nthree\n", window, cx)
11298 });
11299 assert!(cx.read(|cx| editor.is_dirty(cx)));
11300
11301 cx.executor().start_waiting();
11302 let fake_server = fake_servers.next().await.unwrap();
11303
11304 {
11305 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11306 move |params, _| async move {
11307 assert_eq!(
11308 params.text_document.uri,
11309 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11310 );
11311 assert_eq!(params.options.tab_size, 4);
11312 Ok(Some(vec![lsp::TextEdit::new(
11313 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11314 ", ".to_string(),
11315 )]))
11316 },
11317 );
11318 let save = editor
11319 .update_in(cx, |editor, window, cx| {
11320 editor.save(
11321 SaveOptions {
11322 format: true,
11323 autosave: false,
11324 },
11325 project.clone(),
11326 window,
11327 cx,
11328 )
11329 })
11330 .unwrap();
11331 cx.executor().start_waiting();
11332 save.await;
11333
11334 assert_eq!(
11335 editor.update(cx, |editor, cx| editor.text(cx)),
11336 "one, two\nthree\n"
11337 );
11338 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11339 }
11340
11341 {
11342 editor.update_in(cx, |editor, window, cx| {
11343 editor.set_text("one\ntwo\nthree\n", window, cx)
11344 });
11345 assert!(cx.read(|cx| editor.is_dirty(cx)));
11346
11347 // Ensure we can still save even if formatting hangs.
11348 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11349 move |params, _| async move {
11350 assert_eq!(
11351 params.text_document.uri,
11352 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11353 );
11354 futures::future::pending::<()>().await;
11355 unreachable!()
11356 },
11357 );
11358 let save = editor
11359 .update_in(cx, |editor, window, cx| {
11360 editor.save(
11361 SaveOptions {
11362 format: true,
11363 autosave: false,
11364 },
11365 project.clone(),
11366 window,
11367 cx,
11368 )
11369 })
11370 .unwrap();
11371 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11372 cx.executor().start_waiting();
11373 save.await;
11374 assert_eq!(
11375 editor.update(cx, |editor, cx| editor.text(cx)),
11376 "one\ntwo\nthree\n"
11377 );
11378 }
11379
11380 // Set rust language override and assert overridden tabsize is sent to language server
11381 update_test_language_settings(cx, |settings| {
11382 settings.languages.0.insert(
11383 "Rust".into(),
11384 LanguageSettingsContent {
11385 tab_size: NonZeroU32::new(8),
11386 ..Default::default()
11387 },
11388 );
11389 });
11390
11391 {
11392 editor.update_in(cx, |editor, window, cx| {
11393 editor.set_text("somehting_new\n", window, cx)
11394 });
11395 assert!(cx.read(|cx| editor.is_dirty(cx)));
11396 let _formatting_request_signal = fake_server
11397 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11398 assert_eq!(
11399 params.text_document.uri,
11400 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11401 );
11402 assert_eq!(params.options.tab_size, 8);
11403 Ok(Some(vec![]))
11404 });
11405 let save = editor
11406 .update_in(cx, |editor, window, cx| {
11407 editor.save(
11408 SaveOptions {
11409 format: true,
11410 autosave: false,
11411 },
11412 project.clone(),
11413 window,
11414 cx,
11415 )
11416 })
11417 .unwrap();
11418 cx.executor().start_waiting();
11419 save.await;
11420 }
11421}
11422
11423#[gpui::test]
11424async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11425 init_test(cx, |settings| {
11426 settings.defaults.ensure_final_newline_on_save = Some(false);
11427 });
11428
11429 let fs = FakeFs::new(cx.executor());
11430 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11431
11432 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11433
11434 let buffer = project
11435 .update(cx, |project, cx| {
11436 project.open_local_buffer(path!("/file.txt"), cx)
11437 })
11438 .await
11439 .unwrap();
11440
11441 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11442 let (editor, cx) = cx.add_window_view(|window, cx| {
11443 build_editor_with_project(project.clone(), buffer, window, cx)
11444 });
11445 editor.update_in(cx, |editor, window, cx| {
11446 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11447 s.select_ranges([0..0])
11448 });
11449 });
11450 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11451
11452 editor.update_in(cx, |editor, window, cx| {
11453 editor.handle_input("\n", window, cx)
11454 });
11455 cx.run_until_parked();
11456 save(&editor, &project, cx).await;
11457 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11458
11459 editor.update_in(cx, |editor, window, cx| {
11460 editor.undo(&Default::default(), window, cx);
11461 });
11462 save(&editor, &project, cx).await;
11463 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11464
11465 editor.update_in(cx, |editor, window, cx| {
11466 editor.redo(&Default::default(), window, cx);
11467 });
11468 cx.run_until_parked();
11469 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11470
11471 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11472 let save = editor
11473 .update_in(cx, |editor, window, cx| {
11474 editor.save(
11475 SaveOptions {
11476 format: true,
11477 autosave: false,
11478 },
11479 project.clone(),
11480 window,
11481 cx,
11482 )
11483 })
11484 .unwrap();
11485 cx.executor().start_waiting();
11486 save.await;
11487 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11488 }
11489}
11490
11491#[gpui::test]
11492async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11493 init_test(cx, |_| {});
11494
11495 let cols = 4;
11496 let rows = 10;
11497 let sample_text_1 = sample_text(rows, cols, 'a');
11498 assert_eq!(
11499 sample_text_1,
11500 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11501 );
11502 let sample_text_2 = sample_text(rows, cols, 'l');
11503 assert_eq!(
11504 sample_text_2,
11505 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11506 );
11507 let sample_text_3 = sample_text(rows, cols, 'v');
11508 assert_eq!(
11509 sample_text_3,
11510 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11511 );
11512
11513 let fs = FakeFs::new(cx.executor());
11514 fs.insert_tree(
11515 path!("/a"),
11516 json!({
11517 "main.rs": sample_text_1,
11518 "other.rs": sample_text_2,
11519 "lib.rs": sample_text_3,
11520 }),
11521 )
11522 .await;
11523
11524 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11525 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11526 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11527
11528 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11529 language_registry.add(rust_lang());
11530 let mut fake_servers = language_registry.register_fake_lsp(
11531 "Rust",
11532 FakeLspAdapter {
11533 capabilities: lsp::ServerCapabilities {
11534 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11535 ..Default::default()
11536 },
11537 ..Default::default()
11538 },
11539 );
11540
11541 let worktree = project.update(cx, |project, cx| {
11542 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11543 assert_eq!(worktrees.len(), 1);
11544 worktrees.pop().unwrap()
11545 });
11546 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11547
11548 let buffer_1 = project
11549 .update(cx, |project, cx| {
11550 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11551 })
11552 .await
11553 .unwrap();
11554 let buffer_2 = project
11555 .update(cx, |project, cx| {
11556 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11557 })
11558 .await
11559 .unwrap();
11560 let buffer_3 = project
11561 .update(cx, |project, cx| {
11562 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11563 })
11564 .await
11565 .unwrap();
11566
11567 let multi_buffer = cx.new(|cx| {
11568 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11569 multi_buffer.push_excerpts(
11570 buffer_1.clone(),
11571 [
11572 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11573 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11574 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11575 ],
11576 cx,
11577 );
11578 multi_buffer.push_excerpts(
11579 buffer_2.clone(),
11580 [
11581 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11582 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11583 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11584 ],
11585 cx,
11586 );
11587 multi_buffer.push_excerpts(
11588 buffer_3.clone(),
11589 [
11590 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11591 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11592 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11593 ],
11594 cx,
11595 );
11596 multi_buffer
11597 });
11598 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11599 Editor::new(
11600 EditorMode::full(),
11601 multi_buffer,
11602 Some(project.clone()),
11603 window,
11604 cx,
11605 )
11606 });
11607
11608 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11609 editor.change_selections(
11610 SelectionEffects::scroll(Autoscroll::Next),
11611 window,
11612 cx,
11613 |s| s.select_ranges(Some(1..2)),
11614 );
11615 editor.insert("|one|two|three|", window, cx);
11616 });
11617 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11618 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11619 editor.change_selections(
11620 SelectionEffects::scroll(Autoscroll::Next),
11621 window,
11622 cx,
11623 |s| s.select_ranges(Some(60..70)),
11624 );
11625 editor.insert("|four|five|six|", window, cx);
11626 });
11627 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11628
11629 // First two buffers should be edited, but not the third one.
11630 assert_eq!(
11631 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11632 "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}",
11633 );
11634 buffer_1.update(cx, |buffer, _| {
11635 assert!(buffer.is_dirty());
11636 assert_eq!(
11637 buffer.text(),
11638 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11639 )
11640 });
11641 buffer_2.update(cx, |buffer, _| {
11642 assert!(buffer.is_dirty());
11643 assert_eq!(
11644 buffer.text(),
11645 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11646 )
11647 });
11648 buffer_3.update(cx, |buffer, _| {
11649 assert!(!buffer.is_dirty());
11650 assert_eq!(buffer.text(), sample_text_3,)
11651 });
11652 cx.executor().run_until_parked();
11653
11654 cx.executor().start_waiting();
11655 let save = multi_buffer_editor
11656 .update_in(cx, |editor, window, cx| {
11657 editor.save(
11658 SaveOptions {
11659 format: true,
11660 autosave: false,
11661 },
11662 project.clone(),
11663 window,
11664 cx,
11665 )
11666 })
11667 .unwrap();
11668
11669 let fake_server = fake_servers.next().await.unwrap();
11670 fake_server
11671 .server
11672 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11673 Ok(Some(vec![lsp::TextEdit::new(
11674 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11675 format!("[{} formatted]", params.text_document.uri),
11676 )]))
11677 })
11678 .detach();
11679 save.await;
11680
11681 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11682 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11683 assert_eq!(
11684 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11685 uri!(
11686 "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}"
11687 ),
11688 );
11689 buffer_1.update(cx, |buffer, _| {
11690 assert!(!buffer.is_dirty());
11691 assert_eq!(
11692 buffer.text(),
11693 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11694 )
11695 });
11696 buffer_2.update(cx, |buffer, _| {
11697 assert!(!buffer.is_dirty());
11698 assert_eq!(
11699 buffer.text(),
11700 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11701 )
11702 });
11703 buffer_3.update(cx, |buffer, _| {
11704 assert!(!buffer.is_dirty());
11705 assert_eq!(buffer.text(), sample_text_3,)
11706 });
11707}
11708
11709#[gpui::test]
11710async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11711 init_test(cx, |_| {});
11712
11713 let fs = FakeFs::new(cx.executor());
11714 fs.insert_tree(
11715 path!("/dir"),
11716 json!({
11717 "file1.rs": "fn main() { println!(\"hello\"); }",
11718 "file2.rs": "fn test() { println!(\"test\"); }",
11719 "file3.rs": "fn other() { println!(\"other\"); }\n",
11720 }),
11721 )
11722 .await;
11723
11724 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11725 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11726 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11727
11728 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11729 language_registry.add(rust_lang());
11730
11731 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11732 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11733
11734 // Open three buffers
11735 let buffer_1 = project
11736 .update(cx, |project, cx| {
11737 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11738 })
11739 .await
11740 .unwrap();
11741 let buffer_2 = project
11742 .update(cx, |project, cx| {
11743 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11744 })
11745 .await
11746 .unwrap();
11747 let buffer_3 = project
11748 .update(cx, |project, cx| {
11749 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11750 })
11751 .await
11752 .unwrap();
11753
11754 // Create a multi-buffer with all three buffers
11755 let multi_buffer = cx.new(|cx| {
11756 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11757 multi_buffer.push_excerpts(
11758 buffer_1.clone(),
11759 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11760 cx,
11761 );
11762 multi_buffer.push_excerpts(
11763 buffer_2.clone(),
11764 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11765 cx,
11766 );
11767 multi_buffer.push_excerpts(
11768 buffer_3.clone(),
11769 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11770 cx,
11771 );
11772 multi_buffer
11773 });
11774
11775 let editor = cx.new_window_entity(|window, cx| {
11776 Editor::new(
11777 EditorMode::full(),
11778 multi_buffer,
11779 Some(project.clone()),
11780 window,
11781 cx,
11782 )
11783 });
11784
11785 // Edit only the first buffer
11786 editor.update_in(cx, |editor, window, cx| {
11787 editor.change_selections(
11788 SelectionEffects::scroll(Autoscroll::Next),
11789 window,
11790 cx,
11791 |s| s.select_ranges(Some(10..10)),
11792 );
11793 editor.insert("// edited", window, cx);
11794 });
11795
11796 // Verify that only buffer 1 is dirty
11797 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11798 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11799 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11800
11801 // Get write counts after file creation (files were created with initial content)
11802 // We expect each file to have been written once during creation
11803 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11804 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11805 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11806
11807 // Perform autosave
11808 let save_task = editor.update_in(cx, |editor, window, cx| {
11809 editor.save(
11810 SaveOptions {
11811 format: true,
11812 autosave: true,
11813 },
11814 project.clone(),
11815 window,
11816 cx,
11817 )
11818 });
11819 save_task.await.unwrap();
11820
11821 // Only the dirty buffer should have been saved
11822 assert_eq!(
11823 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11824 1,
11825 "Buffer 1 was dirty, so it should have been written once during autosave"
11826 );
11827 assert_eq!(
11828 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11829 0,
11830 "Buffer 2 was clean, so it should not have been written during autosave"
11831 );
11832 assert_eq!(
11833 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11834 0,
11835 "Buffer 3 was clean, so it should not have been written during autosave"
11836 );
11837
11838 // Verify buffer states after autosave
11839 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11840 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11841 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11842
11843 // Now perform a manual save (format = true)
11844 let save_task = editor.update_in(cx, |editor, window, cx| {
11845 editor.save(
11846 SaveOptions {
11847 format: true,
11848 autosave: false,
11849 },
11850 project.clone(),
11851 window,
11852 cx,
11853 )
11854 });
11855 save_task.await.unwrap();
11856
11857 // During manual save, clean buffers don't get written to disk
11858 // They just get did_save called for language server notifications
11859 assert_eq!(
11860 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11861 1,
11862 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11863 );
11864 assert_eq!(
11865 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11866 0,
11867 "Buffer 2 should not have been written at all"
11868 );
11869 assert_eq!(
11870 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11871 0,
11872 "Buffer 3 should not have been written at all"
11873 );
11874}
11875
11876async fn setup_range_format_test(
11877 cx: &mut TestAppContext,
11878) -> (
11879 Entity<Project>,
11880 Entity<Editor>,
11881 &mut gpui::VisualTestContext,
11882 lsp::FakeLanguageServer,
11883) {
11884 init_test(cx, |_| {});
11885
11886 let fs = FakeFs::new(cx.executor());
11887 fs.insert_file(path!("/file.rs"), Default::default()).await;
11888
11889 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11890
11891 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11892 language_registry.add(rust_lang());
11893 let mut fake_servers = language_registry.register_fake_lsp(
11894 "Rust",
11895 FakeLspAdapter {
11896 capabilities: lsp::ServerCapabilities {
11897 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11898 ..lsp::ServerCapabilities::default()
11899 },
11900 ..FakeLspAdapter::default()
11901 },
11902 );
11903
11904 let buffer = project
11905 .update(cx, |project, cx| {
11906 project.open_local_buffer(path!("/file.rs"), cx)
11907 })
11908 .await
11909 .unwrap();
11910
11911 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11912 let (editor, cx) = cx.add_window_view(|window, cx| {
11913 build_editor_with_project(project.clone(), buffer, window, cx)
11914 });
11915
11916 cx.executor().start_waiting();
11917 let fake_server = fake_servers.next().await.unwrap();
11918
11919 (project, editor, cx, fake_server)
11920}
11921
11922#[gpui::test]
11923async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11924 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11925
11926 editor.update_in(cx, |editor, window, cx| {
11927 editor.set_text("one\ntwo\nthree\n", window, cx)
11928 });
11929 assert!(cx.read(|cx| editor.is_dirty(cx)));
11930
11931 let save = editor
11932 .update_in(cx, |editor, window, cx| {
11933 editor.save(
11934 SaveOptions {
11935 format: true,
11936 autosave: false,
11937 },
11938 project.clone(),
11939 window,
11940 cx,
11941 )
11942 })
11943 .unwrap();
11944 fake_server
11945 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11946 assert_eq!(
11947 params.text_document.uri,
11948 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11949 );
11950 assert_eq!(params.options.tab_size, 4);
11951 Ok(Some(vec![lsp::TextEdit::new(
11952 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11953 ", ".to_string(),
11954 )]))
11955 })
11956 .next()
11957 .await;
11958 cx.executor().start_waiting();
11959 save.await;
11960 assert_eq!(
11961 editor.update(cx, |editor, cx| editor.text(cx)),
11962 "one, two\nthree\n"
11963 );
11964 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11965}
11966
11967#[gpui::test]
11968async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11969 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11970
11971 editor.update_in(cx, |editor, window, cx| {
11972 editor.set_text("one\ntwo\nthree\n", window, cx)
11973 });
11974 assert!(cx.read(|cx| editor.is_dirty(cx)));
11975
11976 // Test that save still works when formatting hangs
11977 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11978 move |params, _| async move {
11979 assert_eq!(
11980 params.text_document.uri,
11981 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11982 );
11983 futures::future::pending::<()>().await;
11984 unreachable!()
11985 },
11986 );
11987 let save = editor
11988 .update_in(cx, |editor, window, cx| {
11989 editor.save(
11990 SaveOptions {
11991 format: true,
11992 autosave: false,
11993 },
11994 project.clone(),
11995 window,
11996 cx,
11997 )
11998 })
11999 .unwrap();
12000 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12001 cx.executor().start_waiting();
12002 save.await;
12003 assert_eq!(
12004 editor.update(cx, |editor, cx| editor.text(cx)),
12005 "one\ntwo\nthree\n"
12006 );
12007 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12008}
12009
12010#[gpui::test]
12011async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12012 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12013
12014 // Buffer starts clean, no formatting should be requested
12015 let save = editor
12016 .update_in(cx, |editor, window, cx| {
12017 editor.save(
12018 SaveOptions {
12019 format: false,
12020 autosave: false,
12021 },
12022 project.clone(),
12023 window,
12024 cx,
12025 )
12026 })
12027 .unwrap();
12028 let _pending_format_request = fake_server
12029 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12030 panic!("Should not be invoked");
12031 })
12032 .next();
12033 cx.executor().start_waiting();
12034 save.await;
12035 cx.run_until_parked();
12036}
12037
12038#[gpui::test]
12039async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12040 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12041
12042 // Set Rust language override and assert overridden tabsize is sent to language server
12043 update_test_language_settings(cx, |settings| {
12044 settings.languages.0.insert(
12045 "Rust".into(),
12046 LanguageSettingsContent {
12047 tab_size: NonZeroU32::new(8),
12048 ..Default::default()
12049 },
12050 );
12051 });
12052
12053 editor.update_in(cx, |editor, window, cx| {
12054 editor.set_text("something_new\n", window, cx)
12055 });
12056 assert!(cx.read(|cx| editor.is_dirty(cx)));
12057 let save = editor
12058 .update_in(cx, |editor, window, cx| {
12059 editor.save(
12060 SaveOptions {
12061 format: true,
12062 autosave: false,
12063 },
12064 project.clone(),
12065 window,
12066 cx,
12067 )
12068 })
12069 .unwrap();
12070 fake_server
12071 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12072 assert_eq!(
12073 params.text_document.uri,
12074 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12075 );
12076 assert_eq!(params.options.tab_size, 8);
12077 Ok(Some(Vec::new()))
12078 })
12079 .next()
12080 .await;
12081 save.await;
12082}
12083
12084#[gpui::test]
12085async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12086 init_test(cx, |settings| {
12087 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12088 settings::LanguageServerFormatterSpecifier::Current,
12089 )))
12090 });
12091
12092 let fs = FakeFs::new(cx.executor());
12093 fs.insert_file(path!("/file.rs"), Default::default()).await;
12094
12095 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12096
12097 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12098 language_registry.add(Arc::new(Language::new(
12099 LanguageConfig {
12100 name: "Rust".into(),
12101 matcher: LanguageMatcher {
12102 path_suffixes: vec!["rs".to_string()],
12103 ..Default::default()
12104 },
12105 ..LanguageConfig::default()
12106 },
12107 Some(tree_sitter_rust::LANGUAGE.into()),
12108 )));
12109 update_test_language_settings(cx, |settings| {
12110 // Enable Prettier formatting for the same buffer, and ensure
12111 // LSP is called instead of Prettier.
12112 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12113 });
12114 let mut fake_servers = language_registry.register_fake_lsp(
12115 "Rust",
12116 FakeLspAdapter {
12117 capabilities: lsp::ServerCapabilities {
12118 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12119 ..Default::default()
12120 },
12121 ..Default::default()
12122 },
12123 );
12124
12125 let buffer = project
12126 .update(cx, |project, cx| {
12127 project.open_local_buffer(path!("/file.rs"), cx)
12128 })
12129 .await
12130 .unwrap();
12131
12132 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12133 let (editor, cx) = cx.add_window_view(|window, cx| {
12134 build_editor_with_project(project.clone(), buffer, window, cx)
12135 });
12136 editor.update_in(cx, |editor, window, cx| {
12137 editor.set_text("one\ntwo\nthree\n", window, cx)
12138 });
12139
12140 cx.executor().start_waiting();
12141 let fake_server = fake_servers.next().await.unwrap();
12142
12143 let format = editor
12144 .update_in(cx, |editor, window, cx| {
12145 editor.perform_format(
12146 project.clone(),
12147 FormatTrigger::Manual,
12148 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12149 window,
12150 cx,
12151 )
12152 })
12153 .unwrap();
12154 fake_server
12155 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12156 assert_eq!(
12157 params.text_document.uri,
12158 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12159 );
12160 assert_eq!(params.options.tab_size, 4);
12161 Ok(Some(vec![lsp::TextEdit::new(
12162 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12163 ", ".to_string(),
12164 )]))
12165 })
12166 .next()
12167 .await;
12168 cx.executor().start_waiting();
12169 format.await;
12170 assert_eq!(
12171 editor.update(cx, |editor, cx| editor.text(cx)),
12172 "one, two\nthree\n"
12173 );
12174
12175 editor.update_in(cx, |editor, window, cx| {
12176 editor.set_text("one\ntwo\nthree\n", window, cx)
12177 });
12178 // Ensure we don't lock if formatting hangs.
12179 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12180 move |params, _| async move {
12181 assert_eq!(
12182 params.text_document.uri,
12183 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12184 );
12185 futures::future::pending::<()>().await;
12186 unreachable!()
12187 },
12188 );
12189 let format = editor
12190 .update_in(cx, |editor, window, cx| {
12191 editor.perform_format(
12192 project,
12193 FormatTrigger::Manual,
12194 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12195 window,
12196 cx,
12197 )
12198 })
12199 .unwrap();
12200 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12201 cx.executor().start_waiting();
12202 format.await;
12203 assert_eq!(
12204 editor.update(cx, |editor, cx| editor.text(cx)),
12205 "one\ntwo\nthree\n"
12206 );
12207}
12208
12209#[gpui::test]
12210async fn test_multiple_formatters(cx: &mut TestAppContext) {
12211 init_test(cx, |settings| {
12212 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12213 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12214 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12215 Formatter::CodeAction("code-action-1".into()),
12216 Formatter::CodeAction("code-action-2".into()),
12217 ]))
12218 });
12219
12220 let fs = FakeFs::new(cx.executor());
12221 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12222 .await;
12223
12224 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12225 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12226 language_registry.add(rust_lang());
12227
12228 let mut fake_servers = language_registry.register_fake_lsp(
12229 "Rust",
12230 FakeLspAdapter {
12231 capabilities: lsp::ServerCapabilities {
12232 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12233 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12234 commands: vec!["the-command-for-code-action-1".into()],
12235 ..Default::default()
12236 }),
12237 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12238 ..Default::default()
12239 },
12240 ..Default::default()
12241 },
12242 );
12243
12244 let buffer = project
12245 .update(cx, |project, cx| {
12246 project.open_local_buffer(path!("/file.rs"), cx)
12247 })
12248 .await
12249 .unwrap();
12250
12251 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12252 let (editor, cx) = cx.add_window_view(|window, cx| {
12253 build_editor_with_project(project.clone(), buffer, window, cx)
12254 });
12255
12256 cx.executor().start_waiting();
12257
12258 let fake_server = fake_servers.next().await.unwrap();
12259 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12260 move |_params, _| async move {
12261 Ok(Some(vec![lsp::TextEdit::new(
12262 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12263 "applied-formatting\n".to_string(),
12264 )]))
12265 },
12266 );
12267 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12268 move |params, _| async move {
12269 let requested_code_actions = params.context.only.expect("Expected code action request");
12270 assert_eq!(requested_code_actions.len(), 1);
12271
12272 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12273 let code_action = match requested_code_actions[0].as_str() {
12274 "code-action-1" => lsp::CodeAction {
12275 kind: Some("code-action-1".into()),
12276 edit: Some(lsp::WorkspaceEdit::new(
12277 [(
12278 uri,
12279 vec![lsp::TextEdit::new(
12280 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12281 "applied-code-action-1-edit\n".to_string(),
12282 )],
12283 )]
12284 .into_iter()
12285 .collect(),
12286 )),
12287 command: Some(lsp::Command {
12288 command: "the-command-for-code-action-1".into(),
12289 ..Default::default()
12290 }),
12291 ..Default::default()
12292 },
12293 "code-action-2" => lsp::CodeAction {
12294 kind: Some("code-action-2".into()),
12295 edit: Some(lsp::WorkspaceEdit::new(
12296 [(
12297 uri,
12298 vec![lsp::TextEdit::new(
12299 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12300 "applied-code-action-2-edit\n".to_string(),
12301 )],
12302 )]
12303 .into_iter()
12304 .collect(),
12305 )),
12306 ..Default::default()
12307 },
12308 req => panic!("Unexpected code action request: {:?}", req),
12309 };
12310 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12311 code_action,
12312 )]))
12313 },
12314 );
12315
12316 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12317 move |params, _| async move { Ok(params) }
12318 });
12319
12320 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12321 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12322 let fake = fake_server.clone();
12323 let lock = command_lock.clone();
12324 move |params, _| {
12325 assert_eq!(params.command, "the-command-for-code-action-1");
12326 let fake = fake.clone();
12327 let lock = lock.clone();
12328 async move {
12329 lock.lock().await;
12330 fake.server
12331 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12332 label: None,
12333 edit: lsp::WorkspaceEdit {
12334 changes: Some(
12335 [(
12336 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12337 vec![lsp::TextEdit {
12338 range: lsp::Range::new(
12339 lsp::Position::new(0, 0),
12340 lsp::Position::new(0, 0),
12341 ),
12342 new_text: "applied-code-action-1-command\n".into(),
12343 }],
12344 )]
12345 .into_iter()
12346 .collect(),
12347 ),
12348 ..Default::default()
12349 },
12350 })
12351 .await
12352 .into_response()
12353 .unwrap();
12354 Ok(Some(json!(null)))
12355 }
12356 }
12357 });
12358
12359 cx.executor().start_waiting();
12360 editor
12361 .update_in(cx, |editor, window, cx| {
12362 editor.perform_format(
12363 project.clone(),
12364 FormatTrigger::Manual,
12365 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12366 window,
12367 cx,
12368 )
12369 })
12370 .unwrap()
12371 .await;
12372 editor.update(cx, |editor, cx| {
12373 assert_eq!(
12374 editor.text(cx),
12375 r#"
12376 applied-code-action-2-edit
12377 applied-code-action-1-command
12378 applied-code-action-1-edit
12379 applied-formatting
12380 one
12381 two
12382 three
12383 "#
12384 .unindent()
12385 );
12386 });
12387
12388 editor.update_in(cx, |editor, window, cx| {
12389 editor.undo(&Default::default(), window, cx);
12390 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12391 });
12392
12393 // Perform a manual edit while waiting for an LSP command
12394 // that's being run as part of a formatting code action.
12395 let lock_guard = command_lock.lock().await;
12396 let format = editor
12397 .update_in(cx, |editor, window, cx| {
12398 editor.perform_format(
12399 project.clone(),
12400 FormatTrigger::Manual,
12401 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12402 window,
12403 cx,
12404 )
12405 })
12406 .unwrap();
12407 cx.run_until_parked();
12408 editor.update(cx, |editor, cx| {
12409 assert_eq!(
12410 editor.text(cx),
12411 r#"
12412 applied-code-action-1-edit
12413 applied-formatting
12414 one
12415 two
12416 three
12417 "#
12418 .unindent()
12419 );
12420
12421 editor.buffer.update(cx, |buffer, cx| {
12422 let ix = buffer.len(cx);
12423 buffer.edit([(ix..ix, "edited\n")], None, cx);
12424 });
12425 });
12426
12427 // Allow the LSP command to proceed. Because the buffer was edited,
12428 // the second code action will not be run.
12429 drop(lock_guard);
12430 format.await;
12431 editor.update_in(cx, |editor, window, cx| {
12432 assert_eq!(
12433 editor.text(cx),
12434 r#"
12435 applied-code-action-1-command
12436 applied-code-action-1-edit
12437 applied-formatting
12438 one
12439 two
12440 three
12441 edited
12442 "#
12443 .unindent()
12444 );
12445
12446 // The manual edit is undone first, because it is the last thing the user did
12447 // (even though the command completed afterwards).
12448 editor.undo(&Default::default(), window, cx);
12449 assert_eq!(
12450 editor.text(cx),
12451 r#"
12452 applied-code-action-1-command
12453 applied-code-action-1-edit
12454 applied-formatting
12455 one
12456 two
12457 three
12458 "#
12459 .unindent()
12460 );
12461
12462 // All the formatting (including the command, which completed after the manual edit)
12463 // is undone together.
12464 editor.undo(&Default::default(), window, cx);
12465 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12466 });
12467}
12468
12469#[gpui::test]
12470async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12471 init_test(cx, |settings| {
12472 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12473 settings::LanguageServerFormatterSpecifier::Current,
12474 )]))
12475 });
12476
12477 let fs = FakeFs::new(cx.executor());
12478 fs.insert_file(path!("/file.ts"), Default::default()).await;
12479
12480 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12481
12482 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12483 language_registry.add(Arc::new(Language::new(
12484 LanguageConfig {
12485 name: "TypeScript".into(),
12486 matcher: LanguageMatcher {
12487 path_suffixes: vec!["ts".to_string()],
12488 ..Default::default()
12489 },
12490 ..LanguageConfig::default()
12491 },
12492 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12493 )));
12494 update_test_language_settings(cx, |settings| {
12495 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12496 });
12497 let mut fake_servers = language_registry.register_fake_lsp(
12498 "TypeScript",
12499 FakeLspAdapter {
12500 capabilities: lsp::ServerCapabilities {
12501 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12502 ..Default::default()
12503 },
12504 ..Default::default()
12505 },
12506 );
12507
12508 let buffer = project
12509 .update(cx, |project, cx| {
12510 project.open_local_buffer(path!("/file.ts"), cx)
12511 })
12512 .await
12513 .unwrap();
12514
12515 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12516 let (editor, cx) = cx.add_window_view(|window, cx| {
12517 build_editor_with_project(project.clone(), buffer, window, cx)
12518 });
12519 editor.update_in(cx, |editor, window, cx| {
12520 editor.set_text(
12521 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12522 window,
12523 cx,
12524 )
12525 });
12526
12527 cx.executor().start_waiting();
12528 let fake_server = fake_servers.next().await.unwrap();
12529
12530 let format = editor
12531 .update_in(cx, |editor, window, cx| {
12532 editor.perform_code_action_kind(
12533 project.clone(),
12534 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12535 window,
12536 cx,
12537 )
12538 })
12539 .unwrap();
12540 fake_server
12541 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12542 assert_eq!(
12543 params.text_document.uri,
12544 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12545 );
12546 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12547 lsp::CodeAction {
12548 title: "Organize Imports".to_string(),
12549 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12550 edit: Some(lsp::WorkspaceEdit {
12551 changes: Some(
12552 [(
12553 params.text_document.uri.clone(),
12554 vec![lsp::TextEdit::new(
12555 lsp::Range::new(
12556 lsp::Position::new(1, 0),
12557 lsp::Position::new(2, 0),
12558 ),
12559 "".to_string(),
12560 )],
12561 )]
12562 .into_iter()
12563 .collect(),
12564 ),
12565 ..Default::default()
12566 }),
12567 ..Default::default()
12568 },
12569 )]))
12570 })
12571 .next()
12572 .await;
12573 cx.executor().start_waiting();
12574 format.await;
12575 assert_eq!(
12576 editor.update(cx, |editor, cx| editor.text(cx)),
12577 "import { a } from 'module';\n\nconst x = a;\n"
12578 );
12579
12580 editor.update_in(cx, |editor, window, cx| {
12581 editor.set_text(
12582 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12583 window,
12584 cx,
12585 )
12586 });
12587 // Ensure we don't lock if code action hangs.
12588 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12589 move |params, _| async move {
12590 assert_eq!(
12591 params.text_document.uri,
12592 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12593 );
12594 futures::future::pending::<()>().await;
12595 unreachable!()
12596 },
12597 );
12598 let format = editor
12599 .update_in(cx, |editor, window, cx| {
12600 editor.perform_code_action_kind(
12601 project,
12602 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12603 window,
12604 cx,
12605 )
12606 })
12607 .unwrap();
12608 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12609 cx.executor().start_waiting();
12610 format.await;
12611 assert_eq!(
12612 editor.update(cx, |editor, cx| editor.text(cx)),
12613 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12614 );
12615}
12616
12617#[gpui::test]
12618async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12619 init_test(cx, |_| {});
12620
12621 let mut cx = EditorLspTestContext::new_rust(
12622 lsp::ServerCapabilities {
12623 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12624 ..Default::default()
12625 },
12626 cx,
12627 )
12628 .await;
12629
12630 cx.set_state(indoc! {"
12631 one.twoˇ
12632 "});
12633
12634 // The format request takes a long time. When it completes, it inserts
12635 // a newline and an indent before the `.`
12636 cx.lsp
12637 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12638 let executor = cx.background_executor().clone();
12639 async move {
12640 executor.timer(Duration::from_millis(100)).await;
12641 Ok(Some(vec![lsp::TextEdit {
12642 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12643 new_text: "\n ".into(),
12644 }]))
12645 }
12646 });
12647
12648 // Submit a format request.
12649 let format_1 = cx
12650 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12651 .unwrap();
12652 cx.executor().run_until_parked();
12653
12654 // Submit a second format request.
12655 let format_2 = cx
12656 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12657 .unwrap();
12658 cx.executor().run_until_parked();
12659
12660 // Wait for both format requests to complete
12661 cx.executor().advance_clock(Duration::from_millis(200));
12662 cx.executor().start_waiting();
12663 format_1.await.unwrap();
12664 cx.executor().start_waiting();
12665 format_2.await.unwrap();
12666
12667 // The formatting edits only happens once.
12668 cx.assert_editor_state(indoc! {"
12669 one
12670 .twoˇ
12671 "});
12672}
12673
12674#[gpui::test]
12675async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12676 init_test(cx, |settings| {
12677 settings.defaults.formatter = Some(FormatterList::default())
12678 });
12679
12680 let mut cx = EditorLspTestContext::new_rust(
12681 lsp::ServerCapabilities {
12682 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12683 ..Default::default()
12684 },
12685 cx,
12686 )
12687 .await;
12688
12689 // Record which buffer changes have been sent to the language server
12690 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12691 cx.lsp
12692 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12693 let buffer_changes = buffer_changes.clone();
12694 move |params, _| {
12695 buffer_changes.lock().extend(
12696 params
12697 .content_changes
12698 .into_iter()
12699 .map(|e| (e.range.unwrap(), e.text)),
12700 );
12701 }
12702 });
12703 // Handle formatting requests to the language server.
12704 cx.lsp
12705 .set_request_handler::<lsp::request::Formatting, _, _>({
12706 let buffer_changes = buffer_changes.clone();
12707 move |_, _| {
12708 let buffer_changes = buffer_changes.clone();
12709 // Insert blank lines between each line of the buffer.
12710 async move {
12711 // When formatting is requested, trailing whitespace has already been stripped,
12712 // and the trailing newline has already been added.
12713 assert_eq!(
12714 &buffer_changes.lock()[1..],
12715 &[
12716 (
12717 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12718 "".into()
12719 ),
12720 (
12721 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12722 "".into()
12723 ),
12724 (
12725 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12726 "\n".into()
12727 ),
12728 ]
12729 );
12730
12731 Ok(Some(vec![
12732 lsp::TextEdit {
12733 range: lsp::Range::new(
12734 lsp::Position::new(1, 0),
12735 lsp::Position::new(1, 0),
12736 ),
12737 new_text: "\n".into(),
12738 },
12739 lsp::TextEdit {
12740 range: lsp::Range::new(
12741 lsp::Position::new(2, 0),
12742 lsp::Position::new(2, 0),
12743 ),
12744 new_text: "\n".into(),
12745 },
12746 ]))
12747 }
12748 }
12749 });
12750
12751 // Set up a buffer white some trailing whitespace and no trailing newline.
12752 cx.set_state(
12753 &[
12754 "one ", //
12755 "twoˇ", //
12756 "three ", //
12757 "four", //
12758 ]
12759 .join("\n"),
12760 );
12761 cx.run_until_parked();
12762
12763 // Submit a format request.
12764 let format = cx
12765 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12766 .unwrap();
12767
12768 cx.run_until_parked();
12769 // After formatting the buffer, the trailing whitespace is stripped,
12770 // a newline is appended, and the edits provided by the language server
12771 // have been applied.
12772 format.await.unwrap();
12773
12774 cx.assert_editor_state(
12775 &[
12776 "one", //
12777 "", //
12778 "twoˇ", //
12779 "", //
12780 "three", //
12781 "four", //
12782 "", //
12783 ]
12784 .join("\n"),
12785 );
12786
12787 // Undoing the formatting undoes the trailing whitespace removal, the
12788 // trailing newline, and the LSP edits.
12789 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12790 cx.assert_editor_state(
12791 &[
12792 "one ", //
12793 "twoˇ", //
12794 "three ", //
12795 "four", //
12796 ]
12797 .join("\n"),
12798 );
12799}
12800
12801#[gpui::test]
12802async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12803 cx: &mut TestAppContext,
12804) {
12805 init_test(cx, |_| {});
12806
12807 cx.update(|cx| {
12808 cx.update_global::<SettingsStore, _>(|settings, cx| {
12809 settings.update_user_settings(cx, |settings| {
12810 settings.editor.auto_signature_help = Some(true);
12811 });
12812 });
12813 });
12814
12815 let mut cx = EditorLspTestContext::new_rust(
12816 lsp::ServerCapabilities {
12817 signature_help_provider: Some(lsp::SignatureHelpOptions {
12818 ..Default::default()
12819 }),
12820 ..Default::default()
12821 },
12822 cx,
12823 )
12824 .await;
12825
12826 let language = Language::new(
12827 LanguageConfig {
12828 name: "Rust".into(),
12829 brackets: BracketPairConfig {
12830 pairs: vec![
12831 BracketPair {
12832 start: "{".to_string(),
12833 end: "}".to_string(),
12834 close: true,
12835 surround: true,
12836 newline: true,
12837 },
12838 BracketPair {
12839 start: "(".to_string(),
12840 end: ")".to_string(),
12841 close: true,
12842 surround: true,
12843 newline: true,
12844 },
12845 BracketPair {
12846 start: "/*".to_string(),
12847 end: " */".to_string(),
12848 close: true,
12849 surround: true,
12850 newline: true,
12851 },
12852 BracketPair {
12853 start: "[".to_string(),
12854 end: "]".to_string(),
12855 close: false,
12856 surround: false,
12857 newline: true,
12858 },
12859 BracketPair {
12860 start: "\"".to_string(),
12861 end: "\"".to_string(),
12862 close: true,
12863 surround: true,
12864 newline: false,
12865 },
12866 BracketPair {
12867 start: "<".to_string(),
12868 end: ">".to_string(),
12869 close: false,
12870 surround: true,
12871 newline: true,
12872 },
12873 ],
12874 ..Default::default()
12875 },
12876 autoclose_before: "})]".to_string(),
12877 ..Default::default()
12878 },
12879 Some(tree_sitter_rust::LANGUAGE.into()),
12880 );
12881 let language = Arc::new(language);
12882
12883 cx.language_registry().add(language.clone());
12884 cx.update_buffer(|buffer, cx| {
12885 buffer.set_language(Some(language), cx);
12886 });
12887
12888 cx.set_state(
12889 &r#"
12890 fn main() {
12891 sampleˇ
12892 }
12893 "#
12894 .unindent(),
12895 );
12896
12897 cx.update_editor(|editor, window, cx| {
12898 editor.handle_input("(", window, cx);
12899 });
12900 cx.assert_editor_state(
12901 &"
12902 fn main() {
12903 sample(ˇ)
12904 }
12905 "
12906 .unindent(),
12907 );
12908
12909 let mocked_response = lsp::SignatureHelp {
12910 signatures: vec![lsp::SignatureInformation {
12911 label: "fn sample(param1: u8, param2: u8)".to_string(),
12912 documentation: None,
12913 parameters: Some(vec![
12914 lsp::ParameterInformation {
12915 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12916 documentation: None,
12917 },
12918 lsp::ParameterInformation {
12919 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12920 documentation: None,
12921 },
12922 ]),
12923 active_parameter: None,
12924 }],
12925 active_signature: Some(0),
12926 active_parameter: Some(0),
12927 };
12928 handle_signature_help_request(&mut cx, mocked_response).await;
12929
12930 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12931 .await;
12932
12933 cx.editor(|editor, _, _| {
12934 let signature_help_state = editor.signature_help_state.popover().cloned();
12935 let signature = signature_help_state.unwrap();
12936 assert_eq!(
12937 signature.signatures[signature.current_signature].label,
12938 "fn sample(param1: u8, param2: u8)"
12939 );
12940 });
12941}
12942
12943#[gpui::test]
12944async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12945 init_test(cx, |_| {});
12946
12947 cx.update(|cx| {
12948 cx.update_global::<SettingsStore, _>(|settings, cx| {
12949 settings.update_user_settings(cx, |settings| {
12950 settings.editor.auto_signature_help = Some(false);
12951 settings.editor.show_signature_help_after_edits = Some(false);
12952 });
12953 });
12954 });
12955
12956 let mut cx = EditorLspTestContext::new_rust(
12957 lsp::ServerCapabilities {
12958 signature_help_provider: Some(lsp::SignatureHelpOptions {
12959 ..Default::default()
12960 }),
12961 ..Default::default()
12962 },
12963 cx,
12964 )
12965 .await;
12966
12967 let language = Language::new(
12968 LanguageConfig {
12969 name: "Rust".into(),
12970 brackets: BracketPairConfig {
12971 pairs: vec![
12972 BracketPair {
12973 start: "{".to_string(),
12974 end: "}".to_string(),
12975 close: true,
12976 surround: true,
12977 newline: true,
12978 },
12979 BracketPair {
12980 start: "(".to_string(),
12981 end: ")".to_string(),
12982 close: true,
12983 surround: true,
12984 newline: true,
12985 },
12986 BracketPair {
12987 start: "/*".to_string(),
12988 end: " */".to_string(),
12989 close: true,
12990 surround: true,
12991 newline: true,
12992 },
12993 BracketPair {
12994 start: "[".to_string(),
12995 end: "]".to_string(),
12996 close: false,
12997 surround: false,
12998 newline: true,
12999 },
13000 BracketPair {
13001 start: "\"".to_string(),
13002 end: "\"".to_string(),
13003 close: true,
13004 surround: true,
13005 newline: false,
13006 },
13007 BracketPair {
13008 start: "<".to_string(),
13009 end: ">".to_string(),
13010 close: false,
13011 surround: true,
13012 newline: true,
13013 },
13014 ],
13015 ..Default::default()
13016 },
13017 autoclose_before: "})]".to_string(),
13018 ..Default::default()
13019 },
13020 Some(tree_sitter_rust::LANGUAGE.into()),
13021 );
13022 let language = Arc::new(language);
13023
13024 cx.language_registry().add(language.clone());
13025 cx.update_buffer(|buffer, cx| {
13026 buffer.set_language(Some(language), cx);
13027 });
13028
13029 // Ensure that signature_help is not called when no signature help is enabled.
13030 cx.set_state(
13031 &r#"
13032 fn main() {
13033 sampleˇ
13034 }
13035 "#
13036 .unindent(),
13037 );
13038 cx.update_editor(|editor, window, cx| {
13039 editor.handle_input("(", window, cx);
13040 });
13041 cx.assert_editor_state(
13042 &"
13043 fn main() {
13044 sample(ˇ)
13045 }
13046 "
13047 .unindent(),
13048 );
13049 cx.editor(|editor, _, _| {
13050 assert!(editor.signature_help_state.task().is_none());
13051 });
13052
13053 let mocked_response = lsp::SignatureHelp {
13054 signatures: vec![lsp::SignatureInformation {
13055 label: "fn sample(param1: u8, param2: u8)".to_string(),
13056 documentation: None,
13057 parameters: Some(vec![
13058 lsp::ParameterInformation {
13059 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13060 documentation: None,
13061 },
13062 lsp::ParameterInformation {
13063 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13064 documentation: None,
13065 },
13066 ]),
13067 active_parameter: None,
13068 }],
13069 active_signature: Some(0),
13070 active_parameter: Some(0),
13071 };
13072
13073 // Ensure that signature_help is called when enabled afte edits
13074 cx.update(|_, cx| {
13075 cx.update_global::<SettingsStore, _>(|settings, cx| {
13076 settings.update_user_settings(cx, |settings| {
13077 settings.editor.auto_signature_help = Some(false);
13078 settings.editor.show_signature_help_after_edits = Some(true);
13079 });
13080 });
13081 });
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 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13102 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13103 .await;
13104 cx.update_editor(|editor, _, _| {
13105 let signature_help_state = editor.signature_help_state.popover().cloned();
13106 assert!(signature_help_state.is_some());
13107 let signature = signature_help_state.unwrap();
13108 assert_eq!(
13109 signature.signatures[signature.current_signature].label,
13110 "fn sample(param1: u8, param2: u8)"
13111 );
13112 editor.signature_help_state = SignatureHelpState::default();
13113 });
13114
13115 // Ensure that signature_help is called when auto signature help override is enabled
13116 cx.update(|_, cx| {
13117 cx.update_global::<SettingsStore, _>(|settings, cx| {
13118 settings.update_user_settings(cx, |settings| {
13119 settings.editor.auto_signature_help = Some(true);
13120 settings.editor.show_signature_help_after_edits = Some(false);
13121 });
13122 });
13123 });
13124 cx.set_state(
13125 &r#"
13126 fn main() {
13127 sampleˇ
13128 }
13129 "#
13130 .unindent(),
13131 );
13132 cx.update_editor(|editor, window, cx| {
13133 editor.handle_input("(", window, cx);
13134 });
13135 cx.assert_editor_state(
13136 &"
13137 fn main() {
13138 sample(ˇ)
13139 }
13140 "
13141 .unindent(),
13142 );
13143 handle_signature_help_request(&mut cx, mocked_response).await;
13144 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13145 .await;
13146 cx.editor(|editor, _, _| {
13147 let signature_help_state = editor.signature_help_state.popover().cloned();
13148 assert!(signature_help_state.is_some());
13149 let signature = signature_help_state.unwrap();
13150 assert_eq!(
13151 signature.signatures[signature.current_signature].label,
13152 "fn sample(param1: u8, param2: u8)"
13153 );
13154 });
13155}
13156
13157#[gpui::test]
13158async fn test_signature_help(cx: &mut TestAppContext) {
13159 init_test(cx, |_| {});
13160 cx.update(|cx| {
13161 cx.update_global::<SettingsStore, _>(|settings, cx| {
13162 settings.update_user_settings(cx, |settings| {
13163 settings.editor.auto_signature_help = Some(true);
13164 });
13165 });
13166 });
13167
13168 let mut cx = EditorLspTestContext::new_rust(
13169 lsp::ServerCapabilities {
13170 signature_help_provider: Some(lsp::SignatureHelpOptions {
13171 ..Default::default()
13172 }),
13173 ..Default::default()
13174 },
13175 cx,
13176 )
13177 .await;
13178
13179 // A test that directly calls `show_signature_help`
13180 cx.update_editor(|editor, window, cx| {
13181 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13182 });
13183
13184 let mocked_response = lsp::SignatureHelp {
13185 signatures: vec![lsp::SignatureInformation {
13186 label: "fn sample(param1: u8, param2: u8)".to_string(),
13187 documentation: None,
13188 parameters: Some(vec![
13189 lsp::ParameterInformation {
13190 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13191 documentation: None,
13192 },
13193 lsp::ParameterInformation {
13194 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13195 documentation: None,
13196 },
13197 ]),
13198 active_parameter: None,
13199 }],
13200 active_signature: Some(0),
13201 active_parameter: Some(0),
13202 };
13203 handle_signature_help_request(&mut cx, mocked_response).await;
13204
13205 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13206 .await;
13207
13208 cx.editor(|editor, _, _| {
13209 let signature_help_state = editor.signature_help_state.popover().cloned();
13210 assert!(signature_help_state.is_some());
13211 let signature = signature_help_state.unwrap();
13212 assert_eq!(
13213 signature.signatures[signature.current_signature].label,
13214 "fn sample(param1: u8, param2: u8)"
13215 );
13216 });
13217
13218 // When exiting outside from inside the brackets, `signature_help` is closed.
13219 cx.set_state(indoc! {"
13220 fn main() {
13221 sample(ˇ);
13222 }
13223
13224 fn sample(param1: u8, param2: u8) {}
13225 "});
13226
13227 cx.update_editor(|editor, window, cx| {
13228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13229 s.select_ranges([0..0])
13230 });
13231 });
13232
13233 let mocked_response = lsp::SignatureHelp {
13234 signatures: Vec::new(),
13235 active_signature: None,
13236 active_parameter: None,
13237 };
13238 handle_signature_help_request(&mut cx, mocked_response).await;
13239
13240 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13241 .await;
13242
13243 cx.editor(|editor, _, _| {
13244 assert!(!editor.signature_help_state.is_shown());
13245 });
13246
13247 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13248 cx.set_state(indoc! {"
13249 fn main() {
13250 sample(ˇ);
13251 }
13252
13253 fn sample(param1: u8, param2: u8) {}
13254 "});
13255
13256 let mocked_response = lsp::SignatureHelp {
13257 signatures: vec![lsp::SignatureInformation {
13258 label: "fn sample(param1: u8, param2: u8)".to_string(),
13259 documentation: None,
13260 parameters: Some(vec![
13261 lsp::ParameterInformation {
13262 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13263 documentation: None,
13264 },
13265 lsp::ParameterInformation {
13266 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13267 documentation: None,
13268 },
13269 ]),
13270 active_parameter: None,
13271 }],
13272 active_signature: Some(0),
13273 active_parameter: Some(0),
13274 };
13275 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13276 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13277 .await;
13278 cx.editor(|editor, _, _| {
13279 assert!(editor.signature_help_state.is_shown());
13280 });
13281
13282 // Restore the popover with more parameter input
13283 cx.set_state(indoc! {"
13284 fn main() {
13285 sample(param1, param2ˇ);
13286 }
13287
13288 fn sample(param1: u8, param2: u8) {}
13289 "});
13290
13291 let mocked_response = lsp::SignatureHelp {
13292 signatures: vec![lsp::SignatureInformation {
13293 label: "fn sample(param1: u8, param2: u8)".to_string(),
13294 documentation: None,
13295 parameters: Some(vec![
13296 lsp::ParameterInformation {
13297 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13298 documentation: None,
13299 },
13300 lsp::ParameterInformation {
13301 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13302 documentation: None,
13303 },
13304 ]),
13305 active_parameter: None,
13306 }],
13307 active_signature: Some(0),
13308 active_parameter: Some(1),
13309 };
13310 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13311 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13312 .await;
13313
13314 // When selecting a range, the popover is gone.
13315 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13316 cx.update_editor(|editor, window, cx| {
13317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13318 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13319 })
13320 });
13321 cx.assert_editor_state(indoc! {"
13322 fn main() {
13323 sample(param1, «ˇparam2»);
13324 }
13325
13326 fn sample(param1: u8, param2: u8) {}
13327 "});
13328 cx.editor(|editor, _, _| {
13329 assert!(!editor.signature_help_state.is_shown());
13330 });
13331
13332 // When unselecting again, the popover is back if within the brackets.
13333 cx.update_editor(|editor, window, cx| {
13334 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13335 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13336 })
13337 });
13338 cx.assert_editor_state(indoc! {"
13339 fn main() {
13340 sample(param1, ˇparam2);
13341 }
13342
13343 fn sample(param1: u8, param2: u8) {}
13344 "});
13345 handle_signature_help_request(&mut cx, mocked_response).await;
13346 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13347 .await;
13348 cx.editor(|editor, _, _| {
13349 assert!(editor.signature_help_state.is_shown());
13350 });
13351
13352 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13353 cx.update_editor(|editor, window, cx| {
13354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13355 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13356 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13357 })
13358 });
13359 cx.assert_editor_state(indoc! {"
13360 fn main() {
13361 sample(param1, ˇparam2);
13362 }
13363
13364 fn sample(param1: u8, param2: u8) {}
13365 "});
13366
13367 let mocked_response = lsp::SignatureHelp {
13368 signatures: vec![lsp::SignatureInformation {
13369 label: "fn sample(param1: u8, param2: u8)".to_string(),
13370 documentation: None,
13371 parameters: Some(vec![
13372 lsp::ParameterInformation {
13373 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13374 documentation: None,
13375 },
13376 lsp::ParameterInformation {
13377 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13378 documentation: None,
13379 },
13380 ]),
13381 active_parameter: None,
13382 }],
13383 active_signature: Some(0),
13384 active_parameter: Some(1),
13385 };
13386 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13387 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13388 .await;
13389 cx.update_editor(|editor, _, cx| {
13390 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13391 });
13392 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13393 .await;
13394 cx.update_editor(|editor, window, cx| {
13395 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13396 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13397 })
13398 });
13399 cx.assert_editor_state(indoc! {"
13400 fn main() {
13401 sample(param1, «ˇparam2»);
13402 }
13403
13404 fn sample(param1: u8, param2: u8) {}
13405 "});
13406 cx.update_editor(|editor, window, cx| {
13407 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
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 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13419 .await;
13420}
13421
13422#[gpui::test]
13423async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13424 init_test(cx, |_| {});
13425
13426 let mut cx = EditorLspTestContext::new_rust(
13427 lsp::ServerCapabilities {
13428 signature_help_provider: Some(lsp::SignatureHelpOptions {
13429 ..Default::default()
13430 }),
13431 ..Default::default()
13432 },
13433 cx,
13434 )
13435 .await;
13436
13437 cx.set_state(indoc! {"
13438 fn main() {
13439 overloadedˇ
13440 }
13441 "});
13442
13443 cx.update_editor(|editor, window, cx| {
13444 editor.handle_input("(", window, cx);
13445 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13446 });
13447
13448 // Mock response with 3 signatures
13449 let mocked_response = lsp::SignatureHelp {
13450 signatures: vec![
13451 lsp::SignatureInformation {
13452 label: "fn overloaded(x: i32)".to_string(),
13453 documentation: None,
13454 parameters: Some(vec![lsp::ParameterInformation {
13455 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13456 documentation: None,
13457 }]),
13458 active_parameter: None,
13459 },
13460 lsp::SignatureInformation {
13461 label: "fn overloaded(x: i32, y: i32)".to_string(),
13462 documentation: None,
13463 parameters: Some(vec![
13464 lsp::ParameterInformation {
13465 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13466 documentation: None,
13467 },
13468 lsp::ParameterInformation {
13469 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13470 documentation: None,
13471 },
13472 ]),
13473 active_parameter: None,
13474 },
13475 lsp::SignatureInformation {
13476 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13477 documentation: None,
13478 parameters: Some(vec![
13479 lsp::ParameterInformation {
13480 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13481 documentation: None,
13482 },
13483 lsp::ParameterInformation {
13484 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13485 documentation: None,
13486 },
13487 lsp::ParameterInformation {
13488 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13489 documentation: None,
13490 },
13491 ]),
13492 active_parameter: None,
13493 },
13494 ],
13495 active_signature: Some(1),
13496 active_parameter: Some(0),
13497 };
13498 handle_signature_help_request(&mut cx, mocked_response).await;
13499
13500 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13501 .await;
13502
13503 // Verify we have multiple signatures and the right one is selected
13504 cx.editor(|editor, _, _| {
13505 let popover = editor.signature_help_state.popover().cloned().unwrap();
13506 assert_eq!(popover.signatures.len(), 3);
13507 // active_signature was 1, so that should be the current
13508 assert_eq!(popover.current_signature, 1);
13509 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13510 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13511 assert_eq!(
13512 popover.signatures[2].label,
13513 "fn overloaded(x: i32, y: i32, z: i32)"
13514 );
13515 });
13516
13517 // Test navigation functionality
13518 cx.update_editor(|editor, window, cx| {
13519 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13520 });
13521
13522 cx.editor(|editor, _, _| {
13523 let popover = editor.signature_help_state.popover().cloned().unwrap();
13524 assert_eq!(popover.current_signature, 2);
13525 });
13526
13527 // Test wrap around
13528 cx.update_editor(|editor, window, cx| {
13529 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13530 });
13531
13532 cx.editor(|editor, _, _| {
13533 let popover = editor.signature_help_state.popover().cloned().unwrap();
13534 assert_eq!(popover.current_signature, 0);
13535 });
13536
13537 // Test previous navigation
13538 cx.update_editor(|editor, window, cx| {
13539 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13540 });
13541
13542 cx.editor(|editor, _, _| {
13543 let popover = editor.signature_help_state.popover().cloned().unwrap();
13544 assert_eq!(popover.current_signature, 2);
13545 });
13546}
13547
13548#[gpui::test]
13549async fn test_completion_mode(cx: &mut TestAppContext) {
13550 init_test(cx, |_| {});
13551 let mut cx = EditorLspTestContext::new_rust(
13552 lsp::ServerCapabilities {
13553 completion_provider: Some(lsp::CompletionOptions {
13554 resolve_provider: Some(true),
13555 ..Default::default()
13556 }),
13557 ..Default::default()
13558 },
13559 cx,
13560 )
13561 .await;
13562
13563 struct Run {
13564 run_description: &'static str,
13565 initial_state: String,
13566 buffer_marked_text: String,
13567 completion_label: &'static str,
13568 completion_text: &'static str,
13569 expected_with_insert_mode: String,
13570 expected_with_replace_mode: String,
13571 expected_with_replace_subsequence_mode: String,
13572 expected_with_replace_suffix_mode: String,
13573 }
13574
13575 let runs = [
13576 Run {
13577 run_description: "Start of word matches completion text",
13578 initial_state: "before ediˇ after".into(),
13579 buffer_marked_text: "before <edi|> after".into(),
13580 completion_label: "editor",
13581 completion_text: "editor",
13582 expected_with_insert_mode: "before editorˇ after".into(),
13583 expected_with_replace_mode: "before editorˇ after".into(),
13584 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13585 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13586 },
13587 Run {
13588 run_description: "Accept same text at the middle of the word",
13589 initial_state: "before ediˇtor after".into(),
13590 buffer_marked_text: "before <edi|tor> after".into(),
13591 completion_label: "editor",
13592 completion_text: "editor",
13593 expected_with_insert_mode: "before editorˇtor after".into(),
13594 expected_with_replace_mode: "before editorˇ after".into(),
13595 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13596 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13597 },
13598 Run {
13599 run_description: "End of word matches completion text -- cursor at end",
13600 initial_state: "before torˇ after".into(),
13601 buffer_marked_text: "before <tor|> after".into(),
13602 completion_label: "editor",
13603 completion_text: "editor",
13604 expected_with_insert_mode: "before editorˇ after".into(),
13605 expected_with_replace_mode: "before editorˇ after".into(),
13606 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13607 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13608 },
13609 Run {
13610 run_description: "End of word matches completion text -- cursor at start",
13611 initial_state: "before ˇtor after".into(),
13612 buffer_marked_text: "before <|tor> after".into(),
13613 completion_label: "editor",
13614 completion_text: "editor",
13615 expected_with_insert_mode: "before editorˇtor after".into(),
13616 expected_with_replace_mode: "before editorˇ after".into(),
13617 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13618 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13619 },
13620 Run {
13621 run_description: "Prepend text containing whitespace",
13622 initial_state: "pˇfield: bool".into(),
13623 buffer_marked_text: "<p|field>: bool".into(),
13624 completion_label: "pub ",
13625 completion_text: "pub ",
13626 expected_with_insert_mode: "pub ˇfield: bool".into(),
13627 expected_with_replace_mode: "pub ˇ: bool".into(),
13628 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13629 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13630 },
13631 Run {
13632 run_description: "Add element to start of list",
13633 initial_state: "[element_ˇelement_2]".into(),
13634 buffer_marked_text: "[<element_|element_2>]".into(),
13635 completion_label: "element_1",
13636 completion_text: "element_1",
13637 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13638 expected_with_replace_mode: "[element_1ˇ]".into(),
13639 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13640 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13641 },
13642 Run {
13643 run_description: "Add element to start of list -- first and second elements are equal",
13644 initial_state: "[elˇelement]".into(),
13645 buffer_marked_text: "[<el|element>]".into(),
13646 completion_label: "element",
13647 completion_text: "element",
13648 expected_with_insert_mode: "[elementˇelement]".into(),
13649 expected_with_replace_mode: "[elementˇ]".into(),
13650 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13651 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13652 },
13653 Run {
13654 run_description: "Ends with matching suffix",
13655 initial_state: "SubˇError".into(),
13656 buffer_marked_text: "<Sub|Error>".into(),
13657 completion_label: "SubscriptionError",
13658 completion_text: "SubscriptionError",
13659 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13660 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13661 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13662 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13663 },
13664 Run {
13665 run_description: "Suffix is a subsequence -- contiguous",
13666 initial_state: "SubˇErr".into(),
13667 buffer_marked_text: "<Sub|Err>".into(),
13668 completion_label: "SubscriptionError",
13669 completion_text: "SubscriptionError",
13670 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13671 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13672 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13673 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13674 },
13675 Run {
13676 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13677 initial_state: "Suˇscrirr".into(),
13678 buffer_marked_text: "<Su|scrirr>".into(),
13679 completion_label: "SubscriptionError",
13680 completion_text: "SubscriptionError",
13681 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13682 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13683 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13684 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13685 },
13686 Run {
13687 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13688 initial_state: "foo(indˇix)".into(),
13689 buffer_marked_text: "foo(<ind|ix>)".into(),
13690 completion_label: "node_index",
13691 completion_text: "node_index",
13692 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13693 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13694 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13695 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13696 },
13697 Run {
13698 run_description: "Replace range ends before cursor - should extend to cursor",
13699 initial_state: "before editˇo after".into(),
13700 buffer_marked_text: "before <{ed}>it|o after".into(),
13701 completion_label: "editor",
13702 completion_text: "editor",
13703 expected_with_insert_mode: "before editorˇo after".into(),
13704 expected_with_replace_mode: "before editorˇo after".into(),
13705 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13706 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13707 },
13708 Run {
13709 run_description: "Uses label for suffix matching",
13710 initial_state: "before ediˇtor after".into(),
13711 buffer_marked_text: "before <edi|tor> after".into(),
13712 completion_label: "editor",
13713 completion_text: "editor()",
13714 expected_with_insert_mode: "before editor()ˇtor after".into(),
13715 expected_with_replace_mode: "before editor()ˇ after".into(),
13716 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13717 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13718 },
13719 Run {
13720 run_description: "Case insensitive subsequence and suffix matching",
13721 initial_state: "before EDiˇtoR after".into(),
13722 buffer_marked_text: "before <EDi|toR> after".into(),
13723 completion_label: "editor",
13724 completion_text: "editor",
13725 expected_with_insert_mode: "before editorˇtoR after".into(),
13726 expected_with_replace_mode: "before editorˇ after".into(),
13727 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13728 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13729 },
13730 ];
13731
13732 for run in runs {
13733 let run_variations = [
13734 (LspInsertMode::Insert, run.expected_with_insert_mode),
13735 (LspInsertMode::Replace, run.expected_with_replace_mode),
13736 (
13737 LspInsertMode::ReplaceSubsequence,
13738 run.expected_with_replace_subsequence_mode,
13739 ),
13740 (
13741 LspInsertMode::ReplaceSuffix,
13742 run.expected_with_replace_suffix_mode,
13743 ),
13744 ];
13745
13746 for (lsp_insert_mode, expected_text) in run_variations {
13747 eprintln!(
13748 "run = {:?}, mode = {lsp_insert_mode:.?}",
13749 run.run_description,
13750 );
13751
13752 update_test_language_settings(&mut cx, |settings| {
13753 settings.defaults.completions = Some(CompletionSettingsContent {
13754 lsp_insert_mode: Some(lsp_insert_mode),
13755 words: Some(WordsCompletionMode::Disabled),
13756 words_min_length: Some(0),
13757 ..Default::default()
13758 });
13759 });
13760
13761 cx.set_state(&run.initial_state);
13762 cx.update_editor(|editor, window, cx| {
13763 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13764 });
13765
13766 let counter = Arc::new(AtomicUsize::new(0));
13767 handle_completion_request_with_insert_and_replace(
13768 &mut cx,
13769 &run.buffer_marked_text,
13770 vec![(run.completion_label, run.completion_text)],
13771 counter.clone(),
13772 )
13773 .await;
13774 cx.condition(|editor, _| editor.context_menu_visible())
13775 .await;
13776 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13777
13778 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13779 editor
13780 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13781 .unwrap()
13782 });
13783 cx.assert_editor_state(&expected_text);
13784 handle_resolve_completion_request(&mut cx, None).await;
13785 apply_additional_edits.await.unwrap();
13786 }
13787 }
13788}
13789
13790#[gpui::test]
13791async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13792 init_test(cx, |_| {});
13793 let mut cx = EditorLspTestContext::new_rust(
13794 lsp::ServerCapabilities {
13795 completion_provider: Some(lsp::CompletionOptions {
13796 resolve_provider: Some(true),
13797 ..Default::default()
13798 }),
13799 ..Default::default()
13800 },
13801 cx,
13802 )
13803 .await;
13804
13805 let initial_state = "SubˇError";
13806 let buffer_marked_text = "<Sub|Error>";
13807 let completion_text = "SubscriptionError";
13808 let expected_with_insert_mode = "SubscriptionErrorˇError";
13809 let expected_with_replace_mode = "SubscriptionErrorˇ";
13810
13811 update_test_language_settings(&mut cx, |settings| {
13812 settings.defaults.completions = Some(CompletionSettingsContent {
13813 words: Some(WordsCompletionMode::Disabled),
13814 words_min_length: Some(0),
13815 // set the opposite here to ensure that the action is overriding the default behavior
13816 lsp_insert_mode: Some(LspInsertMode::Insert),
13817 ..Default::default()
13818 });
13819 });
13820
13821 cx.set_state(initial_state);
13822 cx.update_editor(|editor, window, cx| {
13823 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13824 });
13825
13826 let counter = Arc::new(AtomicUsize::new(0));
13827 handle_completion_request_with_insert_and_replace(
13828 &mut cx,
13829 buffer_marked_text,
13830 vec![(completion_text, completion_text)],
13831 counter.clone(),
13832 )
13833 .await;
13834 cx.condition(|editor, _| editor.context_menu_visible())
13835 .await;
13836 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13837
13838 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13839 editor
13840 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13841 .unwrap()
13842 });
13843 cx.assert_editor_state(expected_with_replace_mode);
13844 handle_resolve_completion_request(&mut cx, None).await;
13845 apply_additional_edits.await.unwrap();
13846
13847 update_test_language_settings(&mut cx, |settings| {
13848 settings.defaults.completions = Some(CompletionSettingsContent {
13849 words: Some(WordsCompletionMode::Disabled),
13850 words_min_length: Some(0),
13851 // set the opposite here to ensure that the action is overriding the default behavior
13852 lsp_insert_mode: Some(LspInsertMode::Replace),
13853 ..Default::default()
13854 });
13855 });
13856
13857 cx.set_state(initial_state);
13858 cx.update_editor(|editor, window, cx| {
13859 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13860 });
13861 handle_completion_request_with_insert_and_replace(
13862 &mut cx,
13863 buffer_marked_text,
13864 vec![(completion_text, completion_text)],
13865 counter.clone(),
13866 )
13867 .await;
13868 cx.condition(|editor, _| editor.context_menu_visible())
13869 .await;
13870 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13871
13872 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13873 editor
13874 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13875 .unwrap()
13876 });
13877 cx.assert_editor_state(expected_with_insert_mode);
13878 handle_resolve_completion_request(&mut cx, None).await;
13879 apply_additional_edits.await.unwrap();
13880}
13881
13882#[gpui::test]
13883async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13884 init_test(cx, |_| {});
13885 let mut cx = EditorLspTestContext::new_rust(
13886 lsp::ServerCapabilities {
13887 completion_provider: Some(lsp::CompletionOptions {
13888 resolve_provider: Some(true),
13889 ..Default::default()
13890 }),
13891 ..Default::default()
13892 },
13893 cx,
13894 )
13895 .await;
13896
13897 // scenario: surrounding text matches completion text
13898 let completion_text = "to_offset";
13899 let initial_state = indoc! {"
13900 1. buf.to_offˇsuffix
13901 2. buf.to_offˇsuf
13902 3. buf.to_offˇfix
13903 4. buf.to_offˇ
13904 5. into_offˇensive
13905 6. ˇsuffix
13906 7. let ˇ //
13907 8. aaˇzz
13908 9. buf.to_off«zzzzzˇ»suffix
13909 10. buf.«ˇzzzzz»suffix
13910 11. to_off«ˇzzzzz»
13911
13912 buf.to_offˇsuffix // newest cursor
13913 "};
13914 let completion_marked_buffer = indoc! {"
13915 1. buf.to_offsuffix
13916 2. buf.to_offsuf
13917 3. buf.to_offfix
13918 4. buf.to_off
13919 5. into_offensive
13920 6. suffix
13921 7. let //
13922 8. aazz
13923 9. buf.to_offzzzzzsuffix
13924 10. buf.zzzzzsuffix
13925 11. to_offzzzzz
13926
13927 buf.<to_off|suffix> // newest cursor
13928 "};
13929 let expected = indoc! {"
13930 1. buf.to_offsetˇ
13931 2. buf.to_offsetˇsuf
13932 3. buf.to_offsetˇfix
13933 4. buf.to_offsetˇ
13934 5. into_offsetˇensive
13935 6. to_offsetˇsuffix
13936 7. let to_offsetˇ //
13937 8. aato_offsetˇzz
13938 9. buf.to_offsetˇ
13939 10. buf.to_offsetˇsuffix
13940 11. to_offsetˇ
13941
13942 buf.to_offsetˇ // newest cursor
13943 "};
13944 cx.set_state(initial_state);
13945 cx.update_editor(|editor, window, cx| {
13946 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13947 });
13948 handle_completion_request_with_insert_and_replace(
13949 &mut cx,
13950 completion_marked_buffer,
13951 vec![(completion_text, completion_text)],
13952 Arc::new(AtomicUsize::new(0)),
13953 )
13954 .await;
13955 cx.condition(|editor, _| editor.context_menu_visible())
13956 .await;
13957 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13958 editor
13959 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13960 .unwrap()
13961 });
13962 cx.assert_editor_state(expected);
13963 handle_resolve_completion_request(&mut cx, None).await;
13964 apply_additional_edits.await.unwrap();
13965
13966 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13967 let completion_text = "foo_and_bar";
13968 let initial_state = indoc! {"
13969 1. ooanbˇ
13970 2. zooanbˇ
13971 3. ooanbˇz
13972 4. zooanbˇz
13973 5. ooanˇ
13974 6. oanbˇ
13975
13976 ooanbˇ
13977 "};
13978 let completion_marked_buffer = indoc! {"
13979 1. ooanb
13980 2. zooanb
13981 3. ooanbz
13982 4. zooanbz
13983 5. ooan
13984 6. oanb
13985
13986 <ooanb|>
13987 "};
13988 let expected = indoc! {"
13989 1. foo_and_barˇ
13990 2. zfoo_and_barˇ
13991 3. foo_and_barˇz
13992 4. zfoo_and_barˇz
13993 5. ooanfoo_and_barˇ
13994 6. oanbfoo_and_barˇ
13995
13996 foo_and_barˇ
13997 "};
13998 cx.set_state(initial_state);
13999 cx.update_editor(|editor, window, cx| {
14000 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14001 });
14002 handle_completion_request_with_insert_and_replace(
14003 &mut cx,
14004 completion_marked_buffer,
14005 vec![(completion_text, completion_text)],
14006 Arc::new(AtomicUsize::new(0)),
14007 )
14008 .await;
14009 cx.condition(|editor, _| editor.context_menu_visible())
14010 .await;
14011 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14012 editor
14013 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14014 .unwrap()
14015 });
14016 cx.assert_editor_state(expected);
14017 handle_resolve_completion_request(&mut cx, None).await;
14018 apply_additional_edits.await.unwrap();
14019
14020 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14021 // (expects the same as if it was inserted at the end)
14022 let completion_text = "foo_and_bar";
14023 let initial_state = indoc! {"
14024 1. ooˇanb
14025 2. zooˇanb
14026 3. ooˇanbz
14027 4. zooˇanbz
14028
14029 ooˇanb
14030 "};
14031 let completion_marked_buffer = indoc! {"
14032 1. ooanb
14033 2. zooanb
14034 3. ooanbz
14035 4. zooanbz
14036
14037 <oo|anb>
14038 "};
14039 let expected = indoc! {"
14040 1. foo_and_barˇ
14041 2. zfoo_and_barˇ
14042 3. foo_and_barˇz
14043 4. zfoo_and_barˇz
14044
14045 foo_and_barˇ
14046 "};
14047 cx.set_state(initial_state);
14048 cx.update_editor(|editor, window, cx| {
14049 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14050 });
14051 handle_completion_request_with_insert_and_replace(
14052 &mut cx,
14053 completion_marked_buffer,
14054 vec![(completion_text, completion_text)],
14055 Arc::new(AtomicUsize::new(0)),
14056 )
14057 .await;
14058 cx.condition(|editor, _| editor.context_menu_visible())
14059 .await;
14060 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14061 editor
14062 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14063 .unwrap()
14064 });
14065 cx.assert_editor_state(expected);
14066 handle_resolve_completion_request(&mut cx, None).await;
14067 apply_additional_edits.await.unwrap();
14068}
14069
14070// This used to crash
14071#[gpui::test]
14072async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14073 init_test(cx, |_| {});
14074
14075 let buffer_text = indoc! {"
14076 fn main() {
14077 10.satu;
14078
14079 //
14080 // separate cursors so they open in different excerpts (manually reproducible)
14081 //
14082
14083 10.satu20;
14084 }
14085 "};
14086 let multibuffer_text_with_selections = indoc! {"
14087 fn main() {
14088 10.satuˇ;
14089
14090 //
14091
14092 //
14093
14094 10.satuˇ20;
14095 }
14096 "};
14097 let expected_multibuffer = indoc! {"
14098 fn main() {
14099 10.saturating_sub()ˇ;
14100
14101 //
14102
14103 //
14104
14105 10.saturating_sub()ˇ;
14106 }
14107 "};
14108
14109 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14110 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14111
14112 let fs = FakeFs::new(cx.executor());
14113 fs.insert_tree(
14114 path!("/a"),
14115 json!({
14116 "main.rs": buffer_text,
14117 }),
14118 )
14119 .await;
14120
14121 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14122 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14123 language_registry.add(rust_lang());
14124 let mut fake_servers = language_registry.register_fake_lsp(
14125 "Rust",
14126 FakeLspAdapter {
14127 capabilities: lsp::ServerCapabilities {
14128 completion_provider: Some(lsp::CompletionOptions {
14129 resolve_provider: None,
14130 ..lsp::CompletionOptions::default()
14131 }),
14132 ..lsp::ServerCapabilities::default()
14133 },
14134 ..FakeLspAdapter::default()
14135 },
14136 );
14137 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14138 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14139 let buffer = project
14140 .update(cx, |project, cx| {
14141 project.open_local_buffer(path!("/a/main.rs"), cx)
14142 })
14143 .await
14144 .unwrap();
14145
14146 let multi_buffer = cx.new(|cx| {
14147 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14148 multi_buffer.push_excerpts(
14149 buffer.clone(),
14150 [ExcerptRange::new(0..first_excerpt_end)],
14151 cx,
14152 );
14153 multi_buffer.push_excerpts(
14154 buffer.clone(),
14155 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14156 cx,
14157 );
14158 multi_buffer
14159 });
14160
14161 let editor = workspace
14162 .update(cx, |_, window, cx| {
14163 cx.new(|cx| {
14164 Editor::new(
14165 EditorMode::Full {
14166 scale_ui_elements_with_buffer_font_size: false,
14167 show_active_line_background: false,
14168 sizing_behavior: SizingBehavior::Default,
14169 },
14170 multi_buffer.clone(),
14171 Some(project.clone()),
14172 window,
14173 cx,
14174 )
14175 })
14176 })
14177 .unwrap();
14178
14179 let pane = workspace
14180 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14181 .unwrap();
14182 pane.update_in(cx, |pane, window, cx| {
14183 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14184 });
14185
14186 let fake_server = fake_servers.next().await.unwrap();
14187
14188 editor.update_in(cx, |editor, window, cx| {
14189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14190 s.select_ranges([
14191 Point::new(1, 11)..Point::new(1, 11),
14192 Point::new(7, 11)..Point::new(7, 11),
14193 ])
14194 });
14195
14196 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14197 });
14198
14199 editor.update_in(cx, |editor, window, cx| {
14200 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14201 });
14202
14203 fake_server
14204 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14205 let completion_item = lsp::CompletionItem {
14206 label: "saturating_sub()".into(),
14207 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14208 lsp::InsertReplaceEdit {
14209 new_text: "saturating_sub()".to_owned(),
14210 insert: lsp::Range::new(
14211 lsp::Position::new(7, 7),
14212 lsp::Position::new(7, 11),
14213 ),
14214 replace: lsp::Range::new(
14215 lsp::Position::new(7, 7),
14216 lsp::Position::new(7, 13),
14217 ),
14218 },
14219 )),
14220 ..lsp::CompletionItem::default()
14221 };
14222
14223 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14224 })
14225 .next()
14226 .await
14227 .unwrap();
14228
14229 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14230 .await;
14231
14232 editor
14233 .update_in(cx, |editor, window, cx| {
14234 editor
14235 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14236 .unwrap()
14237 })
14238 .await
14239 .unwrap();
14240
14241 editor.update(cx, |editor, cx| {
14242 assert_text_with_selections(editor, expected_multibuffer, cx);
14243 })
14244}
14245
14246#[gpui::test]
14247async fn test_completion(cx: &mut TestAppContext) {
14248 init_test(cx, |_| {});
14249
14250 let mut cx = EditorLspTestContext::new_rust(
14251 lsp::ServerCapabilities {
14252 completion_provider: Some(lsp::CompletionOptions {
14253 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14254 resolve_provider: Some(true),
14255 ..Default::default()
14256 }),
14257 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14258 ..Default::default()
14259 },
14260 cx,
14261 )
14262 .await;
14263 let counter = Arc::new(AtomicUsize::new(0));
14264
14265 cx.set_state(indoc! {"
14266 oneˇ
14267 two
14268 three
14269 "});
14270 cx.simulate_keystroke(".");
14271 handle_completion_request(
14272 indoc! {"
14273 one.|<>
14274 two
14275 three
14276 "},
14277 vec!["first_completion", "second_completion"],
14278 true,
14279 counter.clone(),
14280 &mut cx,
14281 )
14282 .await;
14283 cx.condition(|editor, _| editor.context_menu_visible())
14284 .await;
14285 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14286
14287 let _handler = handle_signature_help_request(
14288 &mut cx,
14289 lsp::SignatureHelp {
14290 signatures: vec![lsp::SignatureInformation {
14291 label: "test signature".to_string(),
14292 documentation: None,
14293 parameters: Some(vec![lsp::ParameterInformation {
14294 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14295 documentation: None,
14296 }]),
14297 active_parameter: None,
14298 }],
14299 active_signature: None,
14300 active_parameter: None,
14301 },
14302 );
14303 cx.update_editor(|editor, window, cx| {
14304 assert!(
14305 !editor.signature_help_state.is_shown(),
14306 "No signature help was called for"
14307 );
14308 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14309 });
14310 cx.run_until_parked();
14311 cx.update_editor(|editor, _, _| {
14312 assert!(
14313 !editor.signature_help_state.is_shown(),
14314 "No signature help should be shown when completions menu is open"
14315 );
14316 });
14317
14318 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14319 editor.context_menu_next(&Default::default(), window, cx);
14320 editor
14321 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14322 .unwrap()
14323 });
14324 cx.assert_editor_state(indoc! {"
14325 one.second_completionˇ
14326 two
14327 three
14328 "});
14329
14330 handle_resolve_completion_request(
14331 &mut cx,
14332 Some(vec![
14333 (
14334 //This overlaps with the primary completion edit which is
14335 //misbehavior from the LSP spec, test that we filter it out
14336 indoc! {"
14337 one.second_ˇcompletion
14338 two
14339 threeˇ
14340 "},
14341 "overlapping additional edit",
14342 ),
14343 (
14344 indoc! {"
14345 one.second_completion
14346 two
14347 threeˇ
14348 "},
14349 "\nadditional edit",
14350 ),
14351 ]),
14352 )
14353 .await;
14354 apply_additional_edits.await.unwrap();
14355 cx.assert_editor_state(indoc! {"
14356 one.second_completionˇ
14357 two
14358 three
14359 additional edit
14360 "});
14361
14362 cx.set_state(indoc! {"
14363 one.second_completion
14364 twoˇ
14365 threeˇ
14366 additional edit
14367 "});
14368 cx.simulate_keystroke(" ");
14369 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14370 cx.simulate_keystroke("s");
14371 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14372
14373 cx.assert_editor_state(indoc! {"
14374 one.second_completion
14375 two sˇ
14376 three sˇ
14377 additional edit
14378 "});
14379 handle_completion_request(
14380 indoc! {"
14381 one.second_completion
14382 two s
14383 three <s|>
14384 additional edit
14385 "},
14386 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14387 true,
14388 counter.clone(),
14389 &mut cx,
14390 )
14391 .await;
14392 cx.condition(|editor, _| editor.context_menu_visible())
14393 .await;
14394 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14395
14396 cx.simulate_keystroke("i");
14397
14398 handle_completion_request(
14399 indoc! {"
14400 one.second_completion
14401 two si
14402 three <si|>
14403 additional edit
14404 "},
14405 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14406 true,
14407 counter.clone(),
14408 &mut cx,
14409 )
14410 .await;
14411 cx.condition(|editor, _| editor.context_menu_visible())
14412 .await;
14413 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14414
14415 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14416 editor
14417 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14418 .unwrap()
14419 });
14420 cx.assert_editor_state(indoc! {"
14421 one.second_completion
14422 two sixth_completionˇ
14423 three sixth_completionˇ
14424 additional edit
14425 "});
14426
14427 apply_additional_edits.await.unwrap();
14428
14429 update_test_language_settings(&mut cx, |settings| {
14430 settings.defaults.show_completions_on_input = Some(false);
14431 });
14432 cx.set_state("editorˇ");
14433 cx.simulate_keystroke(".");
14434 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14435 cx.simulate_keystrokes("c l o");
14436 cx.assert_editor_state("editor.cloˇ");
14437 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14438 cx.update_editor(|editor, window, cx| {
14439 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14440 });
14441 handle_completion_request(
14442 "editor.<clo|>",
14443 vec!["close", "clobber"],
14444 true,
14445 counter.clone(),
14446 &mut cx,
14447 )
14448 .await;
14449 cx.condition(|editor, _| editor.context_menu_visible())
14450 .await;
14451 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14452
14453 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14454 editor
14455 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14456 .unwrap()
14457 });
14458 cx.assert_editor_state("editor.clobberˇ");
14459 handle_resolve_completion_request(&mut cx, None).await;
14460 apply_additional_edits.await.unwrap();
14461}
14462
14463#[gpui::test]
14464async fn test_completion_reuse(cx: &mut TestAppContext) {
14465 init_test(cx, |_| {});
14466
14467 let mut cx = EditorLspTestContext::new_rust(
14468 lsp::ServerCapabilities {
14469 completion_provider: Some(lsp::CompletionOptions {
14470 trigger_characters: Some(vec![".".to_string()]),
14471 ..Default::default()
14472 }),
14473 ..Default::default()
14474 },
14475 cx,
14476 )
14477 .await;
14478
14479 let counter = Arc::new(AtomicUsize::new(0));
14480 cx.set_state("objˇ");
14481 cx.simulate_keystroke(".");
14482
14483 // Initial completion request returns complete results
14484 let is_incomplete = false;
14485 handle_completion_request(
14486 "obj.|<>",
14487 vec!["a", "ab", "abc"],
14488 is_incomplete,
14489 counter.clone(),
14490 &mut cx,
14491 )
14492 .await;
14493 cx.run_until_parked();
14494 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14495 cx.assert_editor_state("obj.ˇ");
14496 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14497
14498 // Type "a" - filters existing completions
14499 cx.simulate_keystroke("a");
14500 cx.run_until_parked();
14501 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14502 cx.assert_editor_state("obj.aˇ");
14503 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14504
14505 // Type "b" - filters existing completions
14506 cx.simulate_keystroke("b");
14507 cx.run_until_parked();
14508 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14509 cx.assert_editor_state("obj.abˇ");
14510 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14511
14512 // Type "c" - filters existing completions
14513 cx.simulate_keystroke("c");
14514 cx.run_until_parked();
14515 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14516 cx.assert_editor_state("obj.abcˇ");
14517 check_displayed_completions(vec!["abc"], &mut cx);
14518
14519 // Backspace to delete "c" - filters existing completions
14520 cx.update_editor(|editor, window, cx| {
14521 editor.backspace(&Backspace, window, cx);
14522 });
14523 cx.run_until_parked();
14524 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14525 cx.assert_editor_state("obj.abˇ");
14526 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14527
14528 // Moving cursor to the left dismisses menu.
14529 cx.update_editor(|editor, window, cx| {
14530 editor.move_left(&MoveLeft, window, cx);
14531 });
14532 cx.run_until_parked();
14533 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14534 cx.assert_editor_state("obj.aˇb");
14535 cx.update_editor(|editor, _, _| {
14536 assert_eq!(editor.context_menu_visible(), false);
14537 });
14538
14539 // Type "b" - new request
14540 cx.simulate_keystroke("b");
14541 let is_incomplete = false;
14542 handle_completion_request(
14543 "obj.<ab|>a",
14544 vec!["ab", "abc"],
14545 is_incomplete,
14546 counter.clone(),
14547 &mut cx,
14548 )
14549 .await;
14550 cx.run_until_parked();
14551 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14552 cx.assert_editor_state("obj.abˇb");
14553 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14554
14555 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14556 cx.update_editor(|editor, window, cx| {
14557 editor.backspace(&Backspace, window, cx);
14558 });
14559 let is_incomplete = false;
14560 handle_completion_request(
14561 "obj.<a|>b",
14562 vec!["a", "ab", "abc"],
14563 is_incomplete,
14564 counter.clone(),
14565 &mut cx,
14566 )
14567 .await;
14568 cx.run_until_parked();
14569 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14570 cx.assert_editor_state("obj.aˇb");
14571 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14572
14573 // Backspace to delete "a" - dismisses menu.
14574 cx.update_editor(|editor, window, cx| {
14575 editor.backspace(&Backspace, window, cx);
14576 });
14577 cx.run_until_parked();
14578 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14579 cx.assert_editor_state("obj.ˇb");
14580 cx.update_editor(|editor, _, _| {
14581 assert_eq!(editor.context_menu_visible(), false);
14582 });
14583}
14584
14585#[gpui::test]
14586async fn test_word_completion(cx: &mut TestAppContext) {
14587 let lsp_fetch_timeout_ms = 10;
14588 init_test(cx, |language_settings| {
14589 language_settings.defaults.completions = Some(CompletionSettingsContent {
14590 words_min_length: Some(0),
14591 lsp_fetch_timeout_ms: Some(10),
14592 lsp_insert_mode: Some(LspInsertMode::Insert),
14593 ..Default::default()
14594 });
14595 });
14596
14597 let mut cx = EditorLspTestContext::new_rust(
14598 lsp::ServerCapabilities {
14599 completion_provider: Some(lsp::CompletionOptions {
14600 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14601 ..lsp::CompletionOptions::default()
14602 }),
14603 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14604 ..lsp::ServerCapabilities::default()
14605 },
14606 cx,
14607 )
14608 .await;
14609
14610 let throttle_completions = Arc::new(AtomicBool::new(false));
14611
14612 let lsp_throttle_completions = throttle_completions.clone();
14613 let _completion_requests_handler =
14614 cx.lsp
14615 .server
14616 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14617 let lsp_throttle_completions = lsp_throttle_completions.clone();
14618 let cx = cx.clone();
14619 async move {
14620 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14621 cx.background_executor()
14622 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14623 .await;
14624 }
14625 Ok(Some(lsp::CompletionResponse::Array(vec![
14626 lsp::CompletionItem {
14627 label: "first".into(),
14628 ..lsp::CompletionItem::default()
14629 },
14630 lsp::CompletionItem {
14631 label: "last".into(),
14632 ..lsp::CompletionItem::default()
14633 },
14634 ])))
14635 }
14636 });
14637
14638 cx.set_state(indoc! {"
14639 oneˇ
14640 two
14641 three
14642 "});
14643 cx.simulate_keystroke(".");
14644 cx.executor().run_until_parked();
14645 cx.condition(|editor, _| editor.context_menu_visible())
14646 .await;
14647 cx.update_editor(|editor, window, cx| {
14648 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14649 {
14650 assert_eq!(
14651 completion_menu_entries(menu),
14652 &["first", "last"],
14653 "When LSP server is fast to reply, no fallback word completions are used"
14654 );
14655 } else {
14656 panic!("expected completion menu to be open");
14657 }
14658 editor.cancel(&Cancel, window, cx);
14659 });
14660 cx.executor().run_until_parked();
14661 cx.condition(|editor, _| !editor.context_menu_visible())
14662 .await;
14663
14664 throttle_completions.store(true, atomic::Ordering::Release);
14665 cx.simulate_keystroke(".");
14666 cx.executor()
14667 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14668 cx.executor().run_until_parked();
14669 cx.condition(|editor, _| editor.context_menu_visible())
14670 .await;
14671 cx.update_editor(|editor, _, _| {
14672 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14673 {
14674 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14675 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14676 } else {
14677 panic!("expected completion menu to be open");
14678 }
14679 });
14680}
14681
14682#[gpui::test]
14683async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14684 init_test(cx, |language_settings| {
14685 language_settings.defaults.completions = Some(CompletionSettingsContent {
14686 words: Some(WordsCompletionMode::Enabled),
14687 words_min_length: Some(0),
14688 lsp_insert_mode: Some(LspInsertMode::Insert),
14689 ..Default::default()
14690 });
14691 });
14692
14693 let mut cx = EditorLspTestContext::new_rust(
14694 lsp::ServerCapabilities {
14695 completion_provider: Some(lsp::CompletionOptions {
14696 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14697 ..lsp::CompletionOptions::default()
14698 }),
14699 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14700 ..lsp::ServerCapabilities::default()
14701 },
14702 cx,
14703 )
14704 .await;
14705
14706 let _completion_requests_handler =
14707 cx.lsp
14708 .server
14709 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14710 Ok(Some(lsp::CompletionResponse::Array(vec![
14711 lsp::CompletionItem {
14712 label: "first".into(),
14713 ..lsp::CompletionItem::default()
14714 },
14715 lsp::CompletionItem {
14716 label: "last".into(),
14717 ..lsp::CompletionItem::default()
14718 },
14719 ])))
14720 });
14721
14722 cx.set_state(indoc! {"ˇ
14723 first
14724 last
14725 second
14726 "});
14727 cx.simulate_keystroke(".");
14728 cx.executor().run_until_parked();
14729 cx.condition(|editor, _| editor.context_menu_visible())
14730 .await;
14731 cx.update_editor(|editor, _, _| {
14732 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14733 {
14734 assert_eq!(
14735 completion_menu_entries(menu),
14736 &["first", "last", "second"],
14737 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14738 );
14739 } else {
14740 panic!("expected completion menu to be open");
14741 }
14742 });
14743}
14744
14745#[gpui::test]
14746async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14747 init_test(cx, |language_settings| {
14748 language_settings.defaults.completions = Some(CompletionSettingsContent {
14749 words: Some(WordsCompletionMode::Disabled),
14750 words_min_length: Some(0),
14751 lsp_insert_mode: Some(LspInsertMode::Insert),
14752 ..Default::default()
14753 });
14754 });
14755
14756 let mut cx = EditorLspTestContext::new_rust(
14757 lsp::ServerCapabilities {
14758 completion_provider: Some(lsp::CompletionOptions {
14759 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14760 ..lsp::CompletionOptions::default()
14761 }),
14762 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14763 ..lsp::ServerCapabilities::default()
14764 },
14765 cx,
14766 )
14767 .await;
14768
14769 let _completion_requests_handler =
14770 cx.lsp
14771 .server
14772 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14773 panic!("LSP completions should not be queried when dealing with word completions")
14774 });
14775
14776 cx.set_state(indoc! {"ˇ
14777 first
14778 last
14779 second
14780 "});
14781 cx.update_editor(|editor, window, cx| {
14782 editor.show_word_completions(&ShowWordCompletions, window, cx);
14783 });
14784 cx.executor().run_until_parked();
14785 cx.condition(|editor, _| editor.context_menu_visible())
14786 .await;
14787 cx.update_editor(|editor, _, _| {
14788 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14789 {
14790 assert_eq!(
14791 completion_menu_entries(menu),
14792 &["first", "last", "second"],
14793 "`ShowWordCompletions` action should show word completions"
14794 );
14795 } else {
14796 panic!("expected completion menu to be open");
14797 }
14798 });
14799
14800 cx.simulate_keystroke("l");
14801 cx.executor().run_until_parked();
14802 cx.condition(|editor, _| editor.context_menu_visible())
14803 .await;
14804 cx.update_editor(|editor, _, _| {
14805 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14806 {
14807 assert_eq!(
14808 completion_menu_entries(menu),
14809 &["last"],
14810 "After showing word completions, further editing should filter them and not query the LSP"
14811 );
14812 } else {
14813 panic!("expected completion menu to be open");
14814 }
14815 });
14816}
14817
14818#[gpui::test]
14819async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14820 init_test(cx, |language_settings| {
14821 language_settings.defaults.completions = Some(CompletionSettingsContent {
14822 words_min_length: Some(0),
14823 lsp: Some(false),
14824 lsp_insert_mode: Some(LspInsertMode::Insert),
14825 ..Default::default()
14826 });
14827 });
14828
14829 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14830
14831 cx.set_state(indoc! {"ˇ
14832 0_usize
14833 let
14834 33
14835 4.5f32
14836 "});
14837 cx.update_editor(|editor, window, cx| {
14838 editor.show_completions(&ShowCompletions::default(), window, cx);
14839 });
14840 cx.executor().run_until_parked();
14841 cx.condition(|editor, _| editor.context_menu_visible())
14842 .await;
14843 cx.update_editor(|editor, window, cx| {
14844 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14845 {
14846 assert_eq!(
14847 completion_menu_entries(menu),
14848 &["let"],
14849 "With no digits in the completion query, no digits should be in the word completions"
14850 );
14851 } else {
14852 panic!("expected completion menu to be open");
14853 }
14854 editor.cancel(&Cancel, window, cx);
14855 });
14856
14857 cx.set_state(indoc! {"3ˇ
14858 0_usize
14859 let
14860 3
14861 33.35f32
14862 "});
14863 cx.update_editor(|editor, window, cx| {
14864 editor.show_completions(&ShowCompletions::default(), window, cx);
14865 });
14866 cx.executor().run_until_parked();
14867 cx.condition(|editor, _| editor.context_menu_visible())
14868 .await;
14869 cx.update_editor(|editor, _, _| {
14870 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14871 {
14872 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14873 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14874 } else {
14875 panic!("expected completion menu to be open");
14876 }
14877 });
14878}
14879
14880#[gpui::test]
14881async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14882 init_test(cx, |language_settings| {
14883 language_settings.defaults.completions = Some(CompletionSettingsContent {
14884 words: Some(WordsCompletionMode::Enabled),
14885 words_min_length: Some(3),
14886 lsp_insert_mode: Some(LspInsertMode::Insert),
14887 ..Default::default()
14888 });
14889 });
14890
14891 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14892 cx.set_state(indoc! {"ˇ
14893 wow
14894 wowen
14895 wowser
14896 "});
14897 cx.simulate_keystroke("w");
14898 cx.executor().run_until_parked();
14899 cx.update_editor(|editor, _, _| {
14900 if editor.context_menu.borrow_mut().is_some() {
14901 panic!(
14902 "expected completion menu to be hidden, as words completion threshold is not met"
14903 );
14904 }
14905 });
14906
14907 cx.update_editor(|editor, window, cx| {
14908 editor.show_word_completions(&ShowWordCompletions, window, cx);
14909 });
14910 cx.executor().run_until_parked();
14911 cx.update_editor(|editor, window, cx| {
14912 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14913 {
14914 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");
14915 } else {
14916 panic!("expected completion menu to be open after the word completions are called with an action");
14917 }
14918
14919 editor.cancel(&Cancel, window, cx);
14920 });
14921 cx.update_editor(|editor, _, _| {
14922 if editor.context_menu.borrow_mut().is_some() {
14923 panic!("expected completion menu to be hidden after canceling");
14924 }
14925 });
14926
14927 cx.simulate_keystroke("o");
14928 cx.executor().run_until_parked();
14929 cx.update_editor(|editor, _, _| {
14930 if editor.context_menu.borrow_mut().is_some() {
14931 panic!(
14932 "expected completion menu to be hidden, as words completion threshold is not met still"
14933 );
14934 }
14935 });
14936
14937 cx.simulate_keystroke("w");
14938 cx.executor().run_until_parked();
14939 cx.update_editor(|editor, _, _| {
14940 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14941 {
14942 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14943 } else {
14944 panic!("expected completion menu to be open after the word completions threshold is met");
14945 }
14946 });
14947}
14948
14949#[gpui::test]
14950async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14951 init_test(cx, |language_settings| {
14952 language_settings.defaults.completions = Some(CompletionSettingsContent {
14953 words: Some(WordsCompletionMode::Enabled),
14954 words_min_length: Some(0),
14955 lsp_insert_mode: Some(LspInsertMode::Insert),
14956 ..Default::default()
14957 });
14958 });
14959
14960 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14961 cx.update_editor(|editor, _, _| {
14962 editor.disable_word_completions();
14963 });
14964 cx.set_state(indoc! {"ˇ
14965 wow
14966 wowen
14967 wowser
14968 "});
14969 cx.simulate_keystroke("w");
14970 cx.executor().run_until_parked();
14971 cx.update_editor(|editor, _, _| {
14972 if editor.context_menu.borrow_mut().is_some() {
14973 panic!(
14974 "expected completion menu to be hidden, as words completion are disabled for this editor"
14975 );
14976 }
14977 });
14978
14979 cx.update_editor(|editor, window, cx| {
14980 editor.show_word_completions(&ShowWordCompletions, window, cx);
14981 });
14982 cx.executor().run_until_parked();
14983 cx.update_editor(|editor, _, _| {
14984 if editor.context_menu.borrow_mut().is_some() {
14985 panic!(
14986 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14987 );
14988 }
14989 });
14990}
14991
14992fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14993 let position = || lsp::Position {
14994 line: params.text_document_position.position.line,
14995 character: params.text_document_position.position.character,
14996 };
14997 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14998 range: lsp::Range {
14999 start: position(),
15000 end: position(),
15001 },
15002 new_text: text.to_string(),
15003 }))
15004}
15005
15006#[gpui::test]
15007async fn test_multiline_completion(cx: &mut TestAppContext) {
15008 init_test(cx, |_| {});
15009
15010 let fs = FakeFs::new(cx.executor());
15011 fs.insert_tree(
15012 path!("/a"),
15013 json!({
15014 "main.ts": "a",
15015 }),
15016 )
15017 .await;
15018
15019 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15020 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15021 let typescript_language = Arc::new(Language::new(
15022 LanguageConfig {
15023 name: "TypeScript".into(),
15024 matcher: LanguageMatcher {
15025 path_suffixes: vec!["ts".to_string()],
15026 ..LanguageMatcher::default()
15027 },
15028 line_comments: vec!["// ".into()],
15029 ..LanguageConfig::default()
15030 },
15031 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15032 ));
15033 language_registry.add(typescript_language.clone());
15034 let mut fake_servers = language_registry.register_fake_lsp(
15035 "TypeScript",
15036 FakeLspAdapter {
15037 capabilities: lsp::ServerCapabilities {
15038 completion_provider: Some(lsp::CompletionOptions {
15039 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15040 ..lsp::CompletionOptions::default()
15041 }),
15042 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15043 ..lsp::ServerCapabilities::default()
15044 },
15045 // Emulate vtsls label generation
15046 label_for_completion: Some(Box::new(|item, _| {
15047 let text = if let Some(description) = item
15048 .label_details
15049 .as_ref()
15050 .and_then(|label_details| label_details.description.as_ref())
15051 {
15052 format!("{} {}", item.label, description)
15053 } else if let Some(detail) = &item.detail {
15054 format!("{} {}", item.label, detail)
15055 } else {
15056 item.label.clone()
15057 };
15058 Some(language::CodeLabel::plain(text, None))
15059 })),
15060 ..FakeLspAdapter::default()
15061 },
15062 );
15063 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15064 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15065 let worktree_id = workspace
15066 .update(cx, |workspace, _window, cx| {
15067 workspace.project().update(cx, |project, cx| {
15068 project.worktrees(cx).next().unwrap().read(cx).id()
15069 })
15070 })
15071 .unwrap();
15072 let _buffer = project
15073 .update(cx, |project, cx| {
15074 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15075 })
15076 .await
15077 .unwrap();
15078 let editor = workspace
15079 .update(cx, |workspace, window, cx| {
15080 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15081 })
15082 .unwrap()
15083 .await
15084 .unwrap()
15085 .downcast::<Editor>()
15086 .unwrap();
15087 let fake_server = fake_servers.next().await.unwrap();
15088
15089 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15090 let multiline_label_2 = "a\nb\nc\n";
15091 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15092 let multiline_description = "d\ne\nf\n";
15093 let multiline_detail_2 = "g\nh\ni\n";
15094
15095 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15096 move |params, _| async move {
15097 Ok(Some(lsp::CompletionResponse::Array(vec![
15098 lsp::CompletionItem {
15099 label: multiline_label.to_string(),
15100 text_edit: gen_text_edit(¶ms, "new_text_1"),
15101 ..lsp::CompletionItem::default()
15102 },
15103 lsp::CompletionItem {
15104 label: "single line label 1".to_string(),
15105 detail: Some(multiline_detail.to_string()),
15106 text_edit: gen_text_edit(¶ms, "new_text_2"),
15107 ..lsp::CompletionItem::default()
15108 },
15109 lsp::CompletionItem {
15110 label: "single line label 2".to_string(),
15111 label_details: Some(lsp::CompletionItemLabelDetails {
15112 description: Some(multiline_description.to_string()),
15113 detail: None,
15114 }),
15115 text_edit: gen_text_edit(¶ms, "new_text_2"),
15116 ..lsp::CompletionItem::default()
15117 },
15118 lsp::CompletionItem {
15119 label: multiline_label_2.to_string(),
15120 detail: Some(multiline_detail_2.to_string()),
15121 text_edit: gen_text_edit(¶ms, "new_text_3"),
15122 ..lsp::CompletionItem::default()
15123 },
15124 lsp::CompletionItem {
15125 label: "Label with many spaces and \t but without newlines".to_string(),
15126 detail: Some(
15127 "Details with many spaces and \t but without newlines".to_string(),
15128 ),
15129 text_edit: gen_text_edit(¶ms, "new_text_4"),
15130 ..lsp::CompletionItem::default()
15131 },
15132 ])))
15133 },
15134 );
15135
15136 editor.update_in(cx, |editor, window, cx| {
15137 cx.focus_self(window);
15138 editor.move_to_end(&MoveToEnd, window, cx);
15139 editor.handle_input(".", window, cx);
15140 });
15141 cx.run_until_parked();
15142 completion_handle.next().await.unwrap();
15143
15144 editor.update(cx, |editor, _| {
15145 assert!(editor.context_menu_visible());
15146 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15147 {
15148 let completion_labels = menu
15149 .completions
15150 .borrow()
15151 .iter()
15152 .map(|c| c.label.text.clone())
15153 .collect::<Vec<_>>();
15154 assert_eq!(
15155 completion_labels,
15156 &[
15157 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15158 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15159 "single line label 2 d e f ",
15160 "a b c g h i ",
15161 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15162 ],
15163 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15164 );
15165
15166 for completion in menu
15167 .completions
15168 .borrow()
15169 .iter() {
15170 assert_eq!(
15171 completion.label.filter_range,
15172 0..completion.label.text.len(),
15173 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15174 );
15175 }
15176 } else {
15177 panic!("expected completion menu to be open");
15178 }
15179 });
15180}
15181
15182#[gpui::test]
15183async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15184 init_test(cx, |_| {});
15185 let mut cx = EditorLspTestContext::new_rust(
15186 lsp::ServerCapabilities {
15187 completion_provider: Some(lsp::CompletionOptions {
15188 trigger_characters: Some(vec![".".to_string()]),
15189 ..Default::default()
15190 }),
15191 ..Default::default()
15192 },
15193 cx,
15194 )
15195 .await;
15196 cx.lsp
15197 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15198 Ok(Some(lsp::CompletionResponse::Array(vec![
15199 lsp::CompletionItem {
15200 label: "first".into(),
15201 ..Default::default()
15202 },
15203 lsp::CompletionItem {
15204 label: "last".into(),
15205 ..Default::default()
15206 },
15207 ])))
15208 });
15209 cx.set_state("variableˇ");
15210 cx.simulate_keystroke(".");
15211 cx.executor().run_until_parked();
15212
15213 cx.update_editor(|editor, _, _| {
15214 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15215 {
15216 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15217 } else {
15218 panic!("expected completion menu to be open");
15219 }
15220 });
15221
15222 cx.update_editor(|editor, window, cx| {
15223 editor.move_page_down(&MovePageDown::default(), window, cx);
15224 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15225 {
15226 assert!(
15227 menu.selected_item == 1,
15228 "expected PageDown to select the last item from the context menu"
15229 );
15230 } else {
15231 panic!("expected completion menu to stay open after PageDown");
15232 }
15233 });
15234
15235 cx.update_editor(|editor, window, cx| {
15236 editor.move_page_up(&MovePageUp::default(), window, cx);
15237 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15238 {
15239 assert!(
15240 menu.selected_item == 0,
15241 "expected PageUp to select the first item from the context menu"
15242 );
15243 } else {
15244 panic!("expected completion menu to stay open after PageUp");
15245 }
15246 });
15247}
15248
15249#[gpui::test]
15250async fn test_as_is_completions(cx: &mut TestAppContext) {
15251 init_test(cx, |_| {});
15252 let mut cx = EditorLspTestContext::new_rust(
15253 lsp::ServerCapabilities {
15254 completion_provider: Some(lsp::CompletionOptions {
15255 ..Default::default()
15256 }),
15257 ..Default::default()
15258 },
15259 cx,
15260 )
15261 .await;
15262 cx.lsp
15263 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15264 Ok(Some(lsp::CompletionResponse::Array(vec![
15265 lsp::CompletionItem {
15266 label: "unsafe".into(),
15267 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15268 range: lsp::Range {
15269 start: lsp::Position {
15270 line: 1,
15271 character: 2,
15272 },
15273 end: lsp::Position {
15274 line: 1,
15275 character: 3,
15276 },
15277 },
15278 new_text: "unsafe".to_string(),
15279 })),
15280 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15281 ..Default::default()
15282 },
15283 ])))
15284 });
15285 cx.set_state("fn a() {}\n nˇ");
15286 cx.executor().run_until_parked();
15287 cx.update_editor(|editor, window, cx| {
15288 editor.show_completions(
15289 &ShowCompletions {
15290 trigger: Some("\n".into()),
15291 },
15292 window,
15293 cx,
15294 );
15295 });
15296 cx.executor().run_until_parked();
15297
15298 cx.update_editor(|editor, window, cx| {
15299 editor.confirm_completion(&Default::default(), window, cx)
15300 });
15301 cx.executor().run_until_parked();
15302 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15303}
15304
15305#[gpui::test]
15306async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15307 init_test(cx, |_| {});
15308 let language =
15309 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15310 let mut cx = EditorLspTestContext::new(
15311 language,
15312 lsp::ServerCapabilities {
15313 completion_provider: Some(lsp::CompletionOptions {
15314 ..lsp::CompletionOptions::default()
15315 }),
15316 ..lsp::ServerCapabilities::default()
15317 },
15318 cx,
15319 )
15320 .await;
15321
15322 cx.set_state(
15323 "#ifndef BAR_H
15324#define BAR_H
15325
15326#include <stdbool.h>
15327
15328int fn_branch(bool do_branch1, bool do_branch2);
15329
15330#endif // BAR_H
15331ˇ",
15332 );
15333 cx.executor().run_until_parked();
15334 cx.update_editor(|editor, window, cx| {
15335 editor.handle_input("#", window, cx);
15336 });
15337 cx.executor().run_until_parked();
15338 cx.update_editor(|editor, window, cx| {
15339 editor.handle_input("i", window, cx);
15340 });
15341 cx.executor().run_until_parked();
15342 cx.update_editor(|editor, window, cx| {
15343 editor.handle_input("n", window, cx);
15344 });
15345 cx.executor().run_until_parked();
15346 cx.assert_editor_state(
15347 "#ifndef BAR_H
15348#define BAR_H
15349
15350#include <stdbool.h>
15351
15352int fn_branch(bool do_branch1, bool do_branch2);
15353
15354#endif // BAR_H
15355#inˇ",
15356 );
15357
15358 cx.lsp
15359 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15360 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15361 is_incomplete: false,
15362 item_defaults: None,
15363 items: vec![lsp::CompletionItem {
15364 kind: Some(lsp::CompletionItemKind::SNIPPET),
15365 label_details: Some(lsp::CompletionItemLabelDetails {
15366 detail: Some("header".to_string()),
15367 description: None,
15368 }),
15369 label: " include".to_string(),
15370 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15371 range: lsp::Range {
15372 start: lsp::Position {
15373 line: 8,
15374 character: 1,
15375 },
15376 end: lsp::Position {
15377 line: 8,
15378 character: 1,
15379 },
15380 },
15381 new_text: "include \"$0\"".to_string(),
15382 })),
15383 sort_text: Some("40b67681include".to_string()),
15384 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15385 filter_text: Some("include".to_string()),
15386 insert_text: Some("include \"$0\"".to_string()),
15387 ..lsp::CompletionItem::default()
15388 }],
15389 })))
15390 });
15391 cx.update_editor(|editor, window, cx| {
15392 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15393 });
15394 cx.executor().run_until_parked();
15395 cx.update_editor(|editor, window, cx| {
15396 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15397 });
15398 cx.executor().run_until_parked();
15399 cx.assert_editor_state(
15400 "#ifndef BAR_H
15401#define BAR_H
15402
15403#include <stdbool.h>
15404
15405int fn_branch(bool do_branch1, bool do_branch2);
15406
15407#endif // BAR_H
15408#include \"ˇ\"",
15409 );
15410
15411 cx.lsp
15412 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15413 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15414 is_incomplete: true,
15415 item_defaults: None,
15416 items: vec![lsp::CompletionItem {
15417 kind: Some(lsp::CompletionItemKind::FILE),
15418 label: "AGL/".to_string(),
15419 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15420 range: lsp::Range {
15421 start: lsp::Position {
15422 line: 8,
15423 character: 10,
15424 },
15425 end: lsp::Position {
15426 line: 8,
15427 character: 11,
15428 },
15429 },
15430 new_text: "AGL/".to_string(),
15431 })),
15432 sort_text: Some("40b67681AGL/".to_string()),
15433 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15434 filter_text: Some("AGL/".to_string()),
15435 insert_text: Some("AGL/".to_string()),
15436 ..lsp::CompletionItem::default()
15437 }],
15438 })))
15439 });
15440 cx.update_editor(|editor, window, cx| {
15441 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15442 });
15443 cx.executor().run_until_parked();
15444 cx.update_editor(|editor, window, cx| {
15445 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15446 });
15447 cx.executor().run_until_parked();
15448 cx.assert_editor_state(
15449 r##"#ifndef BAR_H
15450#define BAR_H
15451
15452#include <stdbool.h>
15453
15454int fn_branch(bool do_branch1, bool do_branch2);
15455
15456#endif // BAR_H
15457#include "AGL/ˇ"##,
15458 );
15459
15460 cx.update_editor(|editor, window, cx| {
15461 editor.handle_input("\"", window, cx);
15462 });
15463 cx.executor().run_until_parked();
15464 cx.assert_editor_state(
15465 r##"#ifndef BAR_H
15466#define BAR_H
15467
15468#include <stdbool.h>
15469
15470int fn_branch(bool do_branch1, bool do_branch2);
15471
15472#endif // BAR_H
15473#include "AGL/"ˇ"##,
15474 );
15475}
15476
15477#[gpui::test]
15478async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15479 init_test(cx, |_| {});
15480
15481 let mut cx = EditorLspTestContext::new_rust(
15482 lsp::ServerCapabilities {
15483 completion_provider: Some(lsp::CompletionOptions {
15484 trigger_characters: Some(vec![".".to_string()]),
15485 resolve_provider: Some(true),
15486 ..Default::default()
15487 }),
15488 ..Default::default()
15489 },
15490 cx,
15491 )
15492 .await;
15493
15494 cx.set_state("fn main() { let a = 2ˇ; }");
15495 cx.simulate_keystroke(".");
15496 let completion_item = lsp::CompletionItem {
15497 label: "Some".into(),
15498 kind: Some(lsp::CompletionItemKind::SNIPPET),
15499 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15500 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15501 kind: lsp::MarkupKind::Markdown,
15502 value: "```rust\nSome(2)\n```".to_string(),
15503 })),
15504 deprecated: Some(false),
15505 sort_text: Some("Some".to_string()),
15506 filter_text: Some("Some".to_string()),
15507 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15508 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15509 range: lsp::Range {
15510 start: lsp::Position {
15511 line: 0,
15512 character: 22,
15513 },
15514 end: lsp::Position {
15515 line: 0,
15516 character: 22,
15517 },
15518 },
15519 new_text: "Some(2)".to_string(),
15520 })),
15521 additional_text_edits: Some(vec![lsp::TextEdit {
15522 range: lsp::Range {
15523 start: lsp::Position {
15524 line: 0,
15525 character: 20,
15526 },
15527 end: lsp::Position {
15528 line: 0,
15529 character: 22,
15530 },
15531 },
15532 new_text: "".to_string(),
15533 }]),
15534 ..Default::default()
15535 };
15536
15537 let closure_completion_item = completion_item.clone();
15538 let counter = Arc::new(AtomicUsize::new(0));
15539 let counter_clone = counter.clone();
15540 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15541 let task_completion_item = closure_completion_item.clone();
15542 counter_clone.fetch_add(1, atomic::Ordering::Release);
15543 async move {
15544 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15545 is_incomplete: true,
15546 item_defaults: None,
15547 items: vec![task_completion_item],
15548 })))
15549 }
15550 });
15551
15552 cx.condition(|editor, _| editor.context_menu_visible())
15553 .await;
15554 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15555 assert!(request.next().await.is_some());
15556 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15557
15558 cx.simulate_keystrokes("S o m");
15559 cx.condition(|editor, _| editor.context_menu_visible())
15560 .await;
15561 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15562 assert!(request.next().await.is_some());
15563 assert!(request.next().await.is_some());
15564 assert!(request.next().await.is_some());
15565 request.close();
15566 assert!(request.next().await.is_none());
15567 assert_eq!(
15568 counter.load(atomic::Ordering::Acquire),
15569 4,
15570 "With the completions menu open, only one LSP request should happen per input"
15571 );
15572}
15573
15574#[gpui::test]
15575async fn test_toggle_comment(cx: &mut TestAppContext) {
15576 init_test(cx, |_| {});
15577 let mut cx = EditorTestContext::new(cx).await;
15578 let language = Arc::new(Language::new(
15579 LanguageConfig {
15580 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15581 ..Default::default()
15582 },
15583 Some(tree_sitter_rust::LANGUAGE.into()),
15584 ));
15585 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15586
15587 // If multiple selections intersect a line, the line is only toggled once.
15588 cx.set_state(indoc! {"
15589 fn a() {
15590 «//b();
15591 ˇ»// «c();
15592 //ˇ» d();
15593 }
15594 "});
15595
15596 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15597
15598 cx.assert_editor_state(indoc! {"
15599 fn a() {
15600 «b();
15601 c();
15602 ˇ» d();
15603 }
15604 "});
15605
15606 // The comment prefix is inserted at the same column for every line in a
15607 // selection.
15608 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15609
15610 cx.assert_editor_state(indoc! {"
15611 fn a() {
15612 // «b();
15613 // c();
15614 ˇ»// d();
15615 }
15616 "});
15617
15618 // If a selection ends at the beginning of a line, that line is not toggled.
15619 cx.set_selections_state(indoc! {"
15620 fn a() {
15621 // b();
15622 «// c();
15623 ˇ» // d();
15624 }
15625 "});
15626
15627 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15628
15629 cx.assert_editor_state(indoc! {"
15630 fn a() {
15631 // b();
15632 «c();
15633 ˇ» // d();
15634 }
15635 "});
15636
15637 // If a selection span a single line and is empty, the line is toggled.
15638 cx.set_state(indoc! {"
15639 fn a() {
15640 a();
15641 b();
15642 ˇ
15643 }
15644 "});
15645
15646 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15647
15648 cx.assert_editor_state(indoc! {"
15649 fn a() {
15650 a();
15651 b();
15652 //•ˇ
15653 }
15654 "});
15655
15656 // If a selection span multiple lines, empty lines are not toggled.
15657 cx.set_state(indoc! {"
15658 fn a() {
15659 «a();
15660
15661 c();ˇ»
15662 }
15663 "});
15664
15665 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15666
15667 cx.assert_editor_state(indoc! {"
15668 fn a() {
15669 // «a();
15670
15671 // c();ˇ»
15672 }
15673 "});
15674
15675 // If a selection includes multiple comment prefixes, all lines are uncommented.
15676 cx.set_state(indoc! {"
15677 fn a() {
15678 «// a();
15679 /// b();
15680 //! c();ˇ»
15681 }
15682 "});
15683
15684 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15685
15686 cx.assert_editor_state(indoc! {"
15687 fn a() {
15688 «a();
15689 b();
15690 c();ˇ»
15691 }
15692 "});
15693}
15694
15695#[gpui::test]
15696async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15697 init_test(cx, |_| {});
15698 let mut cx = EditorTestContext::new(cx).await;
15699 let language = Arc::new(Language::new(
15700 LanguageConfig {
15701 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15702 ..Default::default()
15703 },
15704 Some(tree_sitter_rust::LANGUAGE.into()),
15705 ));
15706 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15707
15708 let toggle_comments = &ToggleComments {
15709 advance_downwards: false,
15710 ignore_indent: true,
15711 };
15712
15713 // If multiple selections intersect a line, the line is only toggled once.
15714 cx.set_state(indoc! {"
15715 fn a() {
15716 // «b();
15717 // c();
15718 // ˇ» d();
15719 }
15720 "});
15721
15722 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15723
15724 cx.assert_editor_state(indoc! {"
15725 fn a() {
15726 «b();
15727 c();
15728 ˇ» d();
15729 }
15730 "});
15731
15732 // The comment prefix is inserted at the beginning of each line
15733 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15734
15735 cx.assert_editor_state(indoc! {"
15736 fn a() {
15737 // «b();
15738 // c();
15739 // ˇ» d();
15740 }
15741 "});
15742
15743 // If a selection ends at the beginning of a line, that line is not toggled.
15744 cx.set_selections_state(indoc! {"
15745 fn a() {
15746 // b();
15747 // «c();
15748 ˇ»// d();
15749 }
15750 "});
15751
15752 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15753
15754 cx.assert_editor_state(indoc! {"
15755 fn a() {
15756 // b();
15757 «c();
15758 ˇ»// d();
15759 }
15760 "});
15761
15762 // If a selection span a single line and is empty, the line is toggled.
15763 cx.set_state(indoc! {"
15764 fn a() {
15765 a();
15766 b();
15767 ˇ
15768 }
15769 "});
15770
15771 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15772
15773 cx.assert_editor_state(indoc! {"
15774 fn a() {
15775 a();
15776 b();
15777 //ˇ
15778 }
15779 "});
15780
15781 // If a selection span multiple lines, empty lines are not toggled.
15782 cx.set_state(indoc! {"
15783 fn a() {
15784 «a();
15785
15786 c();ˇ»
15787 }
15788 "});
15789
15790 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15791
15792 cx.assert_editor_state(indoc! {"
15793 fn a() {
15794 // «a();
15795
15796 // c();ˇ»
15797 }
15798 "});
15799
15800 // If a selection includes multiple comment prefixes, all lines are uncommented.
15801 cx.set_state(indoc! {"
15802 fn a() {
15803 // «a();
15804 /// b();
15805 //! c();ˇ»
15806 }
15807 "});
15808
15809 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15810
15811 cx.assert_editor_state(indoc! {"
15812 fn a() {
15813 «a();
15814 b();
15815 c();ˇ»
15816 }
15817 "});
15818}
15819
15820#[gpui::test]
15821async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15822 init_test(cx, |_| {});
15823
15824 let language = Arc::new(Language::new(
15825 LanguageConfig {
15826 line_comments: vec!["// ".into()],
15827 ..Default::default()
15828 },
15829 Some(tree_sitter_rust::LANGUAGE.into()),
15830 ));
15831
15832 let mut cx = EditorTestContext::new(cx).await;
15833
15834 cx.language_registry().add(language.clone());
15835 cx.update_buffer(|buffer, cx| {
15836 buffer.set_language(Some(language), cx);
15837 });
15838
15839 let toggle_comments = &ToggleComments {
15840 advance_downwards: true,
15841 ignore_indent: false,
15842 };
15843
15844 // Single cursor on one line -> advance
15845 // Cursor moves horizontally 3 characters as well on non-blank line
15846 cx.set_state(indoc!(
15847 "fn a() {
15848 ˇdog();
15849 cat();
15850 }"
15851 ));
15852 cx.update_editor(|editor, window, cx| {
15853 editor.toggle_comments(toggle_comments, window, cx);
15854 });
15855 cx.assert_editor_state(indoc!(
15856 "fn a() {
15857 // dog();
15858 catˇ();
15859 }"
15860 ));
15861
15862 // Single selection on one line -> don't advance
15863 cx.set_state(indoc!(
15864 "fn a() {
15865 «dog()ˇ»;
15866 cat();
15867 }"
15868 ));
15869 cx.update_editor(|editor, window, cx| {
15870 editor.toggle_comments(toggle_comments, window, cx);
15871 });
15872 cx.assert_editor_state(indoc!(
15873 "fn a() {
15874 // «dog()ˇ»;
15875 cat();
15876 }"
15877 ));
15878
15879 // Multiple cursors on one line -> advance
15880 cx.set_state(indoc!(
15881 "fn a() {
15882 ˇdˇog();
15883 cat();
15884 }"
15885 ));
15886 cx.update_editor(|editor, window, cx| {
15887 editor.toggle_comments(toggle_comments, window, cx);
15888 });
15889 cx.assert_editor_state(indoc!(
15890 "fn a() {
15891 // dog();
15892 catˇ(ˇ);
15893 }"
15894 ));
15895
15896 // Multiple cursors on one line, with selection -> don't advance
15897 cx.set_state(indoc!(
15898 "fn a() {
15899 ˇdˇog«()ˇ»;
15900 cat();
15901 }"
15902 ));
15903 cx.update_editor(|editor, window, cx| {
15904 editor.toggle_comments(toggle_comments, window, cx);
15905 });
15906 cx.assert_editor_state(indoc!(
15907 "fn a() {
15908 // ˇdˇog«()ˇ»;
15909 cat();
15910 }"
15911 ));
15912
15913 // Single cursor on one line -> advance
15914 // Cursor moves to column 0 on blank line
15915 cx.set_state(indoc!(
15916 "fn a() {
15917 ˇdog();
15918
15919 cat();
15920 }"
15921 ));
15922 cx.update_editor(|editor, window, cx| {
15923 editor.toggle_comments(toggle_comments, window, cx);
15924 });
15925 cx.assert_editor_state(indoc!(
15926 "fn a() {
15927 // dog();
15928 ˇ
15929 cat();
15930 }"
15931 ));
15932
15933 // Single cursor on one line -> advance
15934 // Cursor starts and ends at column 0
15935 cx.set_state(indoc!(
15936 "fn a() {
15937 ˇ dog();
15938 cat();
15939 }"
15940 ));
15941 cx.update_editor(|editor, window, cx| {
15942 editor.toggle_comments(toggle_comments, window, cx);
15943 });
15944 cx.assert_editor_state(indoc!(
15945 "fn a() {
15946 // dog();
15947 ˇ cat();
15948 }"
15949 ));
15950}
15951
15952#[gpui::test]
15953async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15954 init_test(cx, |_| {});
15955
15956 let mut cx = EditorTestContext::new(cx).await;
15957
15958 let html_language = Arc::new(
15959 Language::new(
15960 LanguageConfig {
15961 name: "HTML".into(),
15962 block_comment: Some(BlockCommentConfig {
15963 start: "<!-- ".into(),
15964 prefix: "".into(),
15965 end: " -->".into(),
15966 tab_size: 0,
15967 }),
15968 ..Default::default()
15969 },
15970 Some(tree_sitter_html::LANGUAGE.into()),
15971 )
15972 .with_injection_query(
15973 r#"
15974 (script_element
15975 (raw_text) @injection.content
15976 (#set! injection.language "javascript"))
15977 "#,
15978 )
15979 .unwrap(),
15980 );
15981
15982 let javascript_language = Arc::new(Language::new(
15983 LanguageConfig {
15984 name: "JavaScript".into(),
15985 line_comments: vec!["// ".into()],
15986 ..Default::default()
15987 },
15988 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15989 ));
15990
15991 cx.language_registry().add(html_language.clone());
15992 cx.language_registry().add(javascript_language);
15993 cx.update_buffer(|buffer, cx| {
15994 buffer.set_language(Some(html_language), cx);
15995 });
15996
15997 // Toggle comments for empty selections
15998 cx.set_state(
15999 &r#"
16000 <p>A</p>ˇ
16001 <p>B</p>ˇ
16002 <p>C</p>ˇ
16003 "#
16004 .unindent(),
16005 );
16006 cx.update_editor(|editor, window, cx| {
16007 editor.toggle_comments(&ToggleComments::default(), window, cx)
16008 });
16009 cx.assert_editor_state(
16010 &r#"
16011 <!-- <p>A</p>ˇ -->
16012 <!-- <p>B</p>ˇ -->
16013 <!-- <p>C</p>ˇ -->
16014 "#
16015 .unindent(),
16016 );
16017 cx.update_editor(|editor, window, cx| {
16018 editor.toggle_comments(&ToggleComments::default(), window, cx)
16019 });
16020 cx.assert_editor_state(
16021 &r#"
16022 <p>A</p>ˇ
16023 <p>B</p>ˇ
16024 <p>C</p>ˇ
16025 "#
16026 .unindent(),
16027 );
16028
16029 // Toggle comments for mixture of empty and non-empty selections, where
16030 // multiple selections occupy a given line.
16031 cx.set_state(
16032 &r#"
16033 <p>A«</p>
16034 <p>ˇ»B</p>ˇ
16035 <p>C«</p>
16036 <p>ˇ»D</p>ˇ
16037 "#
16038 .unindent(),
16039 );
16040
16041 cx.update_editor(|editor, window, cx| {
16042 editor.toggle_comments(&ToggleComments::default(), window, cx)
16043 });
16044 cx.assert_editor_state(
16045 &r#"
16046 <!-- <p>A«</p>
16047 <p>ˇ»B</p>ˇ -->
16048 <!-- <p>C«</p>
16049 <p>ˇ»D</p>ˇ -->
16050 "#
16051 .unindent(),
16052 );
16053 cx.update_editor(|editor, window, cx| {
16054 editor.toggle_comments(&ToggleComments::default(), window, cx)
16055 });
16056 cx.assert_editor_state(
16057 &r#"
16058 <p>A«</p>
16059 <p>ˇ»B</p>ˇ
16060 <p>C«</p>
16061 <p>ˇ»D</p>ˇ
16062 "#
16063 .unindent(),
16064 );
16065
16066 // Toggle comments when different languages are active for different
16067 // selections.
16068 cx.set_state(
16069 &r#"
16070 ˇ<script>
16071 ˇvar x = new Y();
16072 ˇ</script>
16073 "#
16074 .unindent(),
16075 );
16076 cx.executor().run_until_parked();
16077 cx.update_editor(|editor, window, cx| {
16078 editor.toggle_comments(&ToggleComments::default(), window, cx)
16079 });
16080 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16081 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16082 cx.assert_editor_state(
16083 &r#"
16084 <!-- ˇ<script> -->
16085 // ˇvar x = new Y();
16086 <!-- ˇ</script> -->
16087 "#
16088 .unindent(),
16089 );
16090}
16091
16092#[gpui::test]
16093fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16094 init_test(cx, |_| {});
16095
16096 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16097 let multibuffer = cx.new(|cx| {
16098 let mut multibuffer = MultiBuffer::new(ReadWrite);
16099 multibuffer.push_excerpts(
16100 buffer.clone(),
16101 [
16102 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16103 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16104 ],
16105 cx,
16106 );
16107 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16108 multibuffer
16109 });
16110
16111 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16112 editor.update_in(cx, |editor, window, cx| {
16113 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16114 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16115 s.select_ranges([
16116 Point::new(0, 0)..Point::new(0, 0),
16117 Point::new(1, 0)..Point::new(1, 0),
16118 ])
16119 });
16120
16121 editor.handle_input("X", window, cx);
16122 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16123 assert_eq!(
16124 editor.selections.ranges(&editor.display_snapshot(cx)),
16125 [
16126 Point::new(0, 1)..Point::new(0, 1),
16127 Point::new(1, 1)..Point::new(1, 1),
16128 ]
16129 );
16130
16131 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16132 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16133 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16134 });
16135 editor.backspace(&Default::default(), window, cx);
16136 assert_eq!(editor.text(cx), "Xa\nbbb");
16137 assert_eq!(
16138 editor.selections.ranges(&editor.display_snapshot(cx)),
16139 [Point::new(1, 0)..Point::new(1, 0)]
16140 );
16141
16142 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16143 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16144 });
16145 editor.backspace(&Default::default(), window, cx);
16146 assert_eq!(editor.text(cx), "X\nbb");
16147 assert_eq!(
16148 editor.selections.ranges(&editor.display_snapshot(cx)),
16149 [Point::new(0, 1)..Point::new(0, 1)]
16150 );
16151 });
16152}
16153
16154#[gpui::test]
16155fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16156 init_test(cx, |_| {});
16157
16158 let markers = vec![('[', ']').into(), ('(', ')').into()];
16159 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16160 indoc! {"
16161 [aaaa
16162 (bbbb]
16163 cccc)",
16164 },
16165 markers.clone(),
16166 );
16167 let excerpt_ranges = markers.into_iter().map(|marker| {
16168 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16169 ExcerptRange::new(context)
16170 });
16171 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16172 let multibuffer = cx.new(|cx| {
16173 let mut multibuffer = MultiBuffer::new(ReadWrite);
16174 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16175 multibuffer
16176 });
16177
16178 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16179 editor.update_in(cx, |editor, window, cx| {
16180 let (expected_text, selection_ranges) = marked_text_ranges(
16181 indoc! {"
16182 aaaa
16183 bˇbbb
16184 bˇbbˇb
16185 cccc"
16186 },
16187 true,
16188 );
16189 assert_eq!(editor.text(cx), expected_text);
16190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16191 s.select_ranges(selection_ranges)
16192 });
16193
16194 editor.handle_input("X", window, cx);
16195
16196 let (expected_text, expected_selections) = marked_text_ranges(
16197 indoc! {"
16198 aaaa
16199 bXˇbbXb
16200 bXˇbbXˇb
16201 cccc"
16202 },
16203 false,
16204 );
16205 assert_eq!(editor.text(cx), expected_text);
16206 assert_eq!(
16207 editor.selections.ranges(&editor.display_snapshot(cx)),
16208 expected_selections
16209 );
16210
16211 editor.newline(&Newline, window, cx);
16212 let (expected_text, expected_selections) = marked_text_ranges(
16213 indoc! {"
16214 aaaa
16215 bX
16216 ˇbbX
16217 b
16218 bX
16219 ˇbbX
16220 ˇb
16221 cccc"
16222 },
16223 false,
16224 );
16225 assert_eq!(editor.text(cx), expected_text);
16226 assert_eq!(
16227 editor.selections.ranges(&editor.display_snapshot(cx)),
16228 expected_selections
16229 );
16230 });
16231}
16232
16233#[gpui::test]
16234fn test_refresh_selections(cx: &mut TestAppContext) {
16235 init_test(cx, |_| {});
16236
16237 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16238 let mut excerpt1_id = None;
16239 let multibuffer = cx.new(|cx| {
16240 let mut multibuffer = MultiBuffer::new(ReadWrite);
16241 excerpt1_id = multibuffer
16242 .push_excerpts(
16243 buffer.clone(),
16244 [
16245 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16246 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16247 ],
16248 cx,
16249 )
16250 .into_iter()
16251 .next();
16252 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16253 multibuffer
16254 });
16255
16256 let editor = cx.add_window(|window, cx| {
16257 let mut editor = build_editor(multibuffer.clone(), window, cx);
16258 let snapshot = editor.snapshot(window, cx);
16259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16260 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16261 });
16262 editor.begin_selection(
16263 Point::new(2, 1).to_display_point(&snapshot),
16264 true,
16265 1,
16266 window,
16267 cx,
16268 );
16269 assert_eq!(
16270 editor.selections.ranges(&editor.display_snapshot(cx)),
16271 [
16272 Point::new(1, 3)..Point::new(1, 3),
16273 Point::new(2, 1)..Point::new(2, 1),
16274 ]
16275 );
16276 editor
16277 });
16278
16279 // Refreshing selections is a no-op when excerpts haven't changed.
16280 _ = editor.update(cx, |editor, window, cx| {
16281 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16282 assert_eq!(
16283 editor.selections.ranges(&editor.display_snapshot(cx)),
16284 [
16285 Point::new(1, 3)..Point::new(1, 3),
16286 Point::new(2, 1)..Point::new(2, 1),
16287 ]
16288 );
16289 });
16290
16291 multibuffer.update(cx, |multibuffer, cx| {
16292 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16293 });
16294 _ = editor.update(cx, |editor, window, cx| {
16295 // Removing an excerpt causes the first selection to become degenerate.
16296 assert_eq!(
16297 editor.selections.ranges(&editor.display_snapshot(cx)),
16298 [
16299 Point::new(0, 0)..Point::new(0, 0),
16300 Point::new(0, 1)..Point::new(0, 1)
16301 ]
16302 );
16303
16304 // Refreshing selections will relocate the first selection to the original buffer
16305 // location.
16306 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16307 assert_eq!(
16308 editor.selections.ranges(&editor.display_snapshot(cx)),
16309 [
16310 Point::new(0, 1)..Point::new(0, 1),
16311 Point::new(0, 3)..Point::new(0, 3)
16312 ]
16313 );
16314 assert!(editor.selections.pending_anchor().is_some());
16315 });
16316}
16317
16318#[gpui::test]
16319fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16320 init_test(cx, |_| {});
16321
16322 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16323 let mut excerpt1_id = None;
16324 let multibuffer = cx.new(|cx| {
16325 let mut multibuffer = MultiBuffer::new(ReadWrite);
16326 excerpt1_id = multibuffer
16327 .push_excerpts(
16328 buffer.clone(),
16329 [
16330 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16331 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16332 ],
16333 cx,
16334 )
16335 .into_iter()
16336 .next();
16337 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16338 multibuffer
16339 });
16340
16341 let editor = cx.add_window(|window, cx| {
16342 let mut editor = build_editor(multibuffer.clone(), window, cx);
16343 let snapshot = editor.snapshot(window, cx);
16344 editor.begin_selection(
16345 Point::new(1, 3).to_display_point(&snapshot),
16346 false,
16347 1,
16348 window,
16349 cx,
16350 );
16351 assert_eq!(
16352 editor.selections.ranges(&editor.display_snapshot(cx)),
16353 [Point::new(1, 3)..Point::new(1, 3)]
16354 );
16355 editor
16356 });
16357
16358 multibuffer.update(cx, |multibuffer, cx| {
16359 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16360 });
16361 _ = editor.update(cx, |editor, window, cx| {
16362 assert_eq!(
16363 editor.selections.ranges(&editor.display_snapshot(cx)),
16364 [Point::new(0, 0)..Point::new(0, 0)]
16365 );
16366
16367 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16368 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16369 assert_eq!(
16370 editor.selections.ranges(&editor.display_snapshot(cx)),
16371 [Point::new(0, 3)..Point::new(0, 3)]
16372 );
16373 assert!(editor.selections.pending_anchor().is_some());
16374 });
16375}
16376
16377#[gpui::test]
16378async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16379 init_test(cx, |_| {});
16380
16381 let language = Arc::new(
16382 Language::new(
16383 LanguageConfig {
16384 brackets: BracketPairConfig {
16385 pairs: vec![
16386 BracketPair {
16387 start: "{".to_string(),
16388 end: "}".to_string(),
16389 close: true,
16390 surround: true,
16391 newline: true,
16392 },
16393 BracketPair {
16394 start: "/* ".to_string(),
16395 end: " */".to_string(),
16396 close: true,
16397 surround: true,
16398 newline: true,
16399 },
16400 ],
16401 ..Default::default()
16402 },
16403 ..Default::default()
16404 },
16405 Some(tree_sitter_rust::LANGUAGE.into()),
16406 )
16407 .with_indents_query("")
16408 .unwrap(),
16409 );
16410
16411 let text = concat!(
16412 "{ }\n", //
16413 " x\n", //
16414 " /* */\n", //
16415 "x\n", //
16416 "{{} }\n", //
16417 );
16418
16419 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16420 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16421 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16422 editor
16423 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16424 .await;
16425
16426 editor.update_in(cx, |editor, window, cx| {
16427 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16428 s.select_display_ranges([
16429 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16430 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16431 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16432 ])
16433 });
16434 editor.newline(&Newline, window, cx);
16435
16436 assert_eq!(
16437 editor.buffer().read(cx).read(cx).text(),
16438 concat!(
16439 "{ \n", // Suppress rustfmt
16440 "\n", //
16441 "}\n", //
16442 " x\n", //
16443 " /* \n", //
16444 " \n", //
16445 " */\n", //
16446 "x\n", //
16447 "{{} \n", //
16448 "}\n", //
16449 )
16450 );
16451 });
16452}
16453
16454#[gpui::test]
16455fn test_highlighted_ranges(cx: &mut TestAppContext) {
16456 init_test(cx, |_| {});
16457
16458 let editor = cx.add_window(|window, cx| {
16459 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16460 build_editor(buffer, window, cx)
16461 });
16462
16463 _ = editor.update(cx, |editor, window, cx| {
16464 struct Type1;
16465 struct Type2;
16466
16467 let buffer = editor.buffer.read(cx).snapshot(cx);
16468
16469 let anchor_range =
16470 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16471
16472 editor.highlight_background::<Type1>(
16473 &[
16474 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16475 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16476 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16477 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16478 ],
16479 |_| Hsla::red(),
16480 cx,
16481 );
16482 editor.highlight_background::<Type2>(
16483 &[
16484 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16485 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16486 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16487 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16488 ],
16489 |_| Hsla::green(),
16490 cx,
16491 );
16492
16493 let snapshot = editor.snapshot(window, cx);
16494 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16495 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16496 &snapshot,
16497 cx.theme(),
16498 );
16499 assert_eq!(
16500 highlighted_ranges,
16501 &[
16502 (
16503 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16504 Hsla::green(),
16505 ),
16506 (
16507 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16508 Hsla::red(),
16509 ),
16510 (
16511 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16512 Hsla::green(),
16513 ),
16514 (
16515 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16516 Hsla::red(),
16517 ),
16518 ]
16519 );
16520 assert_eq!(
16521 editor.sorted_background_highlights_in_range(
16522 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16523 &snapshot,
16524 cx.theme(),
16525 ),
16526 &[(
16527 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16528 Hsla::red(),
16529 )]
16530 );
16531 });
16532}
16533
16534#[gpui::test]
16535async fn test_following(cx: &mut TestAppContext) {
16536 init_test(cx, |_| {});
16537
16538 let fs = FakeFs::new(cx.executor());
16539 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16540
16541 let buffer = project.update(cx, |project, cx| {
16542 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16543 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16544 });
16545 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16546 let follower = cx.update(|cx| {
16547 cx.open_window(
16548 WindowOptions {
16549 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16550 gpui::Point::new(px(0.), px(0.)),
16551 gpui::Point::new(px(10.), px(80.)),
16552 ))),
16553 ..Default::default()
16554 },
16555 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16556 )
16557 .unwrap()
16558 });
16559
16560 let is_still_following = Rc::new(RefCell::new(true));
16561 let follower_edit_event_count = Rc::new(RefCell::new(0));
16562 let pending_update = Rc::new(RefCell::new(None));
16563 let leader_entity = leader.root(cx).unwrap();
16564 let follower_entity = follower.root(cx).unwrap();
16565 _ = follower.update(cx, {
16566 let update = pending_update.clone();
16567 let is_still_following = is_still_following.clone();
16568 let follower_edit_event_count = follower_edit_event_count.clone();
16569 |_, window, cx| {
16570 cx.subscribe_in(
16571 &leader_entity,
16572 window,
16573 move |_, leader, event, window, cx| {
16574 leader.read(cx).add_event_to_update_proto(
16575 event,
16576 &mut update.borrow_mut(),
16577 window,
16578 cx,
16579 );
16580 },
16581 )
16582 .detach();
16583
16584 cx.subscribe_in(
16585 &follower_entity,
16586 window,
16587 move |_, _, event: &EditorEvent, _window, _cx| {
16588 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16589 *is_still_following.borrow_mut() = false;
16590 }
16591
16592 if let EditorEvent::BufferEdited = event {
16593 *follower_edit_event_count.borrow_mut() += 1;
16594 }
16595 },
16596 )
16597 .detach();
16598 }
16599 });
16600
16601 // Update the selections only
16602 _ = leader.update(cx, |leader, window, cx| {
16603 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16604 s.select_ranges([1..1])
16605 });
16606 });
16607 follower
16608 .update(cx, |follower, window, cx| {
16609 follower.apply_update_proto(
16610 &project,
16611 pending_update.borrow_mut().take().unwrap(),
16612 window,
16613 cx,
16614 )
16615 })
16616 .unwrap()
16617 .await
16618 .unwrap();
16619 _ = follower.update(cx, |follower, _, cx| {
16620 assert_eq!(
16621 follower.selections.ranges(&follower.display_snapshot(cx)),
16622 vec![1..1]
16623 );
16624 });
16625 assert!(*is_still_following.borrow());
16626 assert_eq!(*follower_edit_event_count.borrow(), 0);
16627
16628 // Update the scroll position only
16629 _ = leader.update(cx, |leader, window, cx| {
16630 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16631 });
16632 follower
16633 .update(cx, |follower, window, cx| {
16634 follower.apply_update_proto(
16635 &project,
16636 pending_update.borrow_mut().take().unwrap(),
16637 window,
16638 cx,
16639 )
16640 })
16641 .unwrap()
16642 .await
16643 .unwrap();
16644 assert_eq!(
16645 follower
16646 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16647 .unwrap(),
16648 gpui::Point::new(1.5, 3.5)
16649 );
16650 assert!(*is_still_following.borrow());
16651 assert_eq!(*follower_edit_event_count.borrow(), 0);
16652
16653 // Update the selections and scroll position. The follower's scroll position is updated
16654 // via autoscroll, not via the leader's exact scroll position.
16655 _ = leader.update(cx, |leader, window, cx| {
16656 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16657 s.select_ranges([0..0])
16658 });
16659 leader.request_autoscroll(Autoscroll::newest(), cx);
16660 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16661 });
16662 follower
16663 .update(cx, |follower, window, cx| {
16664 follower.apply_update_proto(
16665 &project,
16666 pending_update.borrow_mut().take().unwrap(),
16667 window,
16668 cx,
16669 )
16670 })
16671 .unwrap()
16672 .await
16673 .unwrap();
16674 _ = follower.update(cx, |follower, _, cx| {
16675 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16676 assert_eq!(
16677 follower.selections.ranges(&follower.display_snapshot(cx)),
16678 vec![0..0]
16679 );
16680 });
16681 assert!(*is_still_following.borrow());
16682
16683 // Creating a pending selection that precedes another selection
16684 _ = leader.update(cx, |leader, window, cx| {
16685 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16686 s.select_ranges([1..1])
16687 });
16688 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16689 });
16690 follower
16691 .update(cx, |follower, window, cx| {
16692 follower.apply_update_proto(
16693 &project,
16694 pending_update.borrow_mut().take().unwrap(),
16695 window,
16696 cx,
16697 )
16698 })
16699 .unwrap()
16700 .await
16701 .unwrap();
16702 _ = follower.update(cx, |follower, _, cx| {
16703 assert_eq!(
16704 follower.selections.ranges(&follower.display_snapshot(cx)),
16705 vec![0..0, 1..1]
16706 );
16707 });
16708 assert!(*is_still_following.borrow());
16709
16710 // Extend the pending selection so that it surrounds another selection
16711 _ = leader.update(cx, |leader, window, cx| {
16712 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, 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!(
16728 follower.selections.ranges(&follower.display_snapshot(cx)),
16729 vec![0..2]
16730 );
16731 });
16732
16733 // Scrolling locally breaks the follow
16734 _ = follower.update(cx, |follower, window, cx| {
16735 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16736 follower.set_scroll_anchor(
16737 ScrollAnchor {
16738 anchor: top_anchor,
16739 offset: gpui::Point::new(0.0, 0.5),
16740 },
16741 window,
16742 cx,
16743 );
16744 });
16745 assert!(!(*is_still_following.borrow()));
16746}
16747
16748#[gpui::test]
16749async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16750 init_test(cx, |_| {});
16751
16752 let fs = FakeFs::new(cx.executor());
16753 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16754 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16755 let pane = workspace
16756 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16757 .unwrap();
16758
16759 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16760
16761 let leader = pane.update_in(cx, |_, window, cx| {
16762 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16763 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16764 });
16765
16766 // Start following the editor when it has no excerpts.
16767 let mut state_message =
16768 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16769 let workspace_entity = workspace.root(cx).unwrap();
16770 let follower_1 = cx
16771 .update_window(*workspace.deref(), |_, window, cx| {
16772 Editor::from_state_proto(
16773 workspace_entity,
16774 ViewId {
16775 creator: CollaboratorId::PeerId(PeerId::default()),
16776 id: 0,
16777 },
16778 &mut state_message,
16779 window,
16780 cx,
16781 )
16782 })
16783 .unwrap()
16784 .unwrap()
16785 .await
16786 .unwrap();
16787
16788 let update_message = Rc::new(RefCell::new(None));
16789 follower_1.update_in(cx, {
16790 let update = update_message.clone();
16791 |_, window, cx| {
16792 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16793 leader.read(cx).add_event_to_update_proto(
16794 event,
16795 &mut update.borrow_mut(),
16796 window,
16797 cx,
16798 );
16799 })
16800 .detach();
16801 }
16802 });
16803
16804 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16805 (
16806 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16807 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16808 )
16809 });
16810
16811 // Insert some excerpts.
16812 leader.update(cx, |leader, cx| {
16813 leader.buffer.update(cx, |multibuffer, cx| {
16814 multibuffer.set_excerpts_for_path(
16815 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16816 buffer_1.clone(),
16817 vec![
16818 Point::row_range(0..3),
16819 Point::row_range(1..6),
16820 Point::row_range(12..15),
16821 ],
16822 0,
16823 cx,
16824 );
16825 multibuffer.set_excerpts_for_path(
16826 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16827 buffer_2.clone(),
16828 vec![Point::row_range(0..6), Point::row_range(8..12)],
16829 0,
16830 cx,
16831 );
16832 });
16833 });
16834
16835 // Apply the update of adding the excerpts.
16836 follower_1
16837 .update_in(cx, |follower, window, cx| {
16838 follower.apply_update_proto(
16839 &project,
16840 update_message.borrow().clone().unwrap(),
16841 window,
16842 cx,
16843 )
16844 })
16845 .await
16846 .unwrap();
16847 assert_eq!(
16848 follower_1.update(cx, |editor, cx| editor.text(cx)),
16849 leader.update(cx, |editor, cx| editor.text(cx))
16850 );
16851 update_message.borrow_mut().take();
16852
16853 // Start following separately after it already has excerpts.
16854 let mut state_message =
16855 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16856 let workspace_entity = workspace.root(cx).unwrap();
16857 let follower_2 = cx
16858 .update_window(*workspace.deref(), |_, window, cx| {
16859 Editor::from_state_proto(
16860 workspace_entity,
16861 ViewId {
16862 creator: CollaboratorId::PeerId(PeerId::default()),
16863 id: 0,
16864 },
16865 &mut state_message,
16866 window,
16867 cx,
16868 )
16869 })
16870 .unwrap()
16871 .unwrap()
16872 .await
16873 .unwrap();
16874 assert_eq!(
16875 follower_2.update(cx, |editor, cx| editor.text(cx)),
16876 leader.update(cx, |editor, cx| editor.text(cx))
16877 );
16878
16879 // Remove some excerpts.
16880 leader.update(cx, |leader, cx| {
16881 leader.buffer.update(cx, |multibuffer, cx| {
16882 let excerpt_ids = multibuffer.excerpt_ids();
16883 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16884 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16885 });
16886 });
16887
16888 // Apply the update of removing the excerpts.
16889 follower_1
16890 .update_in(cx, |follower, window, cx| {
16891 follower.apply_update_proto(
16892 &project,
16893 update_message.borrow().clone().unwrap(),
16894 window,
16895 cx,
16896 )
16897 })
16898 .await
16899 .unwrap();
16900 follower_2
16901 .update_in(cx, |follower, window, cx| {
16902 follower.apply_update_proto(
16903 &project,
16904 update_message.borrow().clone().unwrap(),
16905 window,
16906 cx,
16907 )
16908 })
16909 .await
16910 .unwrap();
16911 update_message.borrow_mut().take();
16912 assert_eq!(
16913 follower_1.update(cx, |editor, cx| editor.text(cx)),
16914 leader.update(cx, |editor, cx| editor.text(cx))
16915 );
16916}
16917
16918#[gpui::test]
16919async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16920 init_test(cx, |_| {});
16921
16922 let mut cx = EditorTestContext::new(cx).await;
16923 let lsp_store =
16924 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16925
16926 cx.set_state(indoc! {"
16927 ˇfn func(abc def: i32) -> u32 {
16928 }
16929 "});
16930
16931 cx.update(|_, cx| {
16932 lsp_store.update(cx, |lsp_store, cx| {
16933 lsp_store
16934 .update_diagnostics(
16935 LanguageServerId(0),
16936 lsp::PublishDiagnosticsParams {
16937 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16938 version: None,
16939 diagnostics: vec![
16940 lsp::Diagnostic {
16941 range: lsp::Range::new(
16942 lsp::Position::new(0, 11),
16943 lsp::Position::new(0, 12),
16944 ),
16945 severity: Some(lsp::DiagnosticSeverity::ERROR),
16946 ..Default::default()
16947 },
16948 lsp::Diagnostic {
16949 range: lsp::Range::new(
16950 lsp::Position::new(0, 12),
16951 lsp::Position::new(0, 15),
16952 ),
16953 severity: Some(lsp::DiagnosticSeverity::ERROR),
16954 ..Default::default()
16955 },
16956 lsp::Diagnostic {
16957 range: lsp::Range::new(
16958 lsp::Position::new(0, 25),
16959 lsp::Position::new(0, 28),
16960 ),
16961 severity: Some(lsp::DiagnosticSeverity::ERROR),
16962 ..Default::default()
16963 },
16964 ],
16965 },
16966 None,
16967 DiagnosticSourceKind::Pushed,
16968 &[],
16969 cx,
16970 )
16971 .unwrap()
16972 });
16973 });
16974
16975 executor.run_until_parked();
16976
16977 cx.update_editor(|editor, window, cx| {
16978 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16979 });
16980
16981 cx.assert_editor_state(indoc! {"
16982 fn func(abc def: i32) -> ˇu32 {
16983 }
16984 "});
16985
16986 cx.update_editor(|editor, window, cx| {
16987 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16988 });
16989
16990 cx.assert_editor_state(indoc! {"
16991 fn func(abc ˇdef: i32) -> u32 {
16992 }
16993 "});
16994
16995 cx.update_editor(|editor, window, cx| {
16996 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16997 });
16998
16999 cx.assert_editor_state(indoc! {"
17000 fn func(abcˇ def: i32) -> u32 {
17001 }
17002 "});
17003
17004 cx.update_editor(|editor, window, cx| {
17005 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17006 });
17007
17008 cx.assert_editor_state(indoc! {"
17009 fn func(abc def: i32) -> ˇu32 {
17010 }
17011 "});
17012}
17013
17014#[gpui::test]
17015async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17016 init_test(cx, |_| {});
17017
17018 let mut cx = EditorTestContext::new(cx).await;
17019
17020 let diff_base = r#"
17021 use some::mod;
17022
17023 const A: u32 = 42;
17024
17025 fn main() {
17026 println!("hello");
17027
17028 println!("world");
17029 }
17030 "#
17031 .unindent();
17032
17033 // Edits are modified, removed, modified, added
17034 cx.set_state(
17035 &r#"
17036 use some::modified;
17037
17038 ˇ
17039 fn main() {
17040 println!("hello there");
17041
17042 println!("around the");
17043 println!("world");
17044 }
17045 "#
17046 .unindent(),
17047 );
17048
17049 cx.set_head_text(&diff_base);
17050 executor.run_until_parked();
17051
17052 cx.update_editor(|editor, window, cx| {
17053 //Wrap around the bottom of the buffer
17054 for _ in 0..3 {
17055 editor.go_to_next_hunk(&GoToHunk, window, cx);
17056 }
17057 });
17058
17059 cx.assert_editor_state(
17060 &r#"
17061 ˇuse some::modified;
17062
17063
17064 fn main() {
17065 println!("hello there");
17066
17067 println!("around the");
17068 println!("world");
17069 }
17070 "#
17071 .unindent(),
17072 );
17073
17074 cx.update_editor(|editor, window, cx| {
17075 //Wrap around the top of the buffer
17076 for _ in 0..2 {
17077 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17078 }
17079 });
17080
17081 cx.assert_editor_state(
17082 &r#"
17083 use some::modified;
17084
17085
17086 fn main() {
17087 ˇ println!("hello there");
17088
17089 println!("around the");
17090 println!("world");
17091 }
17092 "#
17093 .unindent(),
17094 );
17095
17096 cx.update_editor(|editor, window, cx| {
17097 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17098 });
17099
17100 cx.assert_editor_state(
17101 &r#"
17102 use some::modified;
17103
17104 ˇ
17105 fn main() {
17106 println!("hello there");
17107
17108 println!("around the");
17109 println!("world");
17110 }
17111 "#
17112 .unindent(),
17113 );
17114
17115 cx.update_editor(|editor, window, cx| {
17116 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17117 });
17118
17119 cx.assert_editor_state(
17120 &r#"
17121 ˇuse some::modified;
17122
17123
17124 fn main() {
17125 println!("hello there");
17126
17127 println!("around the");
17128 println!("world");
17129 }
17130 "#
17131 .unindent(),
17132 );
17133
17134 cx.update_editor(|editor, window, cx| {
17135 for _ in 0..2 {
17136 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17137 }
17138 });
17139
17140 cx.assert_editor_state(
17141 &r#"
17142 use some::modified;
17143
17144
17145 fn main() {
17146 ˇ println!("hello there");
17147
17148 println!("around the");
17149 println!("world");
17150 }
17151 "#
17152 .unindent(),
17153 );
17154
17155 cx.update_editor(|editor, window, cx| {
17156 editor.fold(&Fold, window, cx);
17157 });
17158
17159 cx.update_editor(|editor, window, cx| {
17160 editor.go_to_next_hunk(&GoToHunk, window, cx);
17161 });
17162
17163 cx.assert_editor_state(
17164 &r#"
17165 ˇuse some::modified;
17166
17167
17168 fn main() {
17169 println!("hello there");
17170
17171 println!("around the");
17172 println!("world");
17173 }
17174 "#
17175 .unindent(),
17176 );
17177}
17178
17179#[test]
17180fn test_split_words() {
17181 fn split(text: &str) -> Vec<&str> {
17182 split_words(text).collect()
17183 }
17184
17185 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17186 assert_eq!(split("hello_world"), &["hello_", "world"]);
17187 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17188 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17189 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17190 assert_eq!(split("helloworld"), &["helloworld"]);
17191
17192 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17193}
17194
17195#[gpui::test]
17196async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17197 init_test(cx, |_| {});
17198
17199 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17200 let mut assert = |before, after| {
17201 let _state_context = cx.set_state(before);
17202 cx.run_until_parked();
17203 cx.update_editor(|editor, window, cx| {
17204 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17205 });
17206 cx.run_until_parked();
17207 cx.assert_editor_state(after);
17208 };
17209
17210 // Outside bracket jumps to outside of matching bracket
17211 assert("console.logˇ(var);", "console.log(var)ˇ;");
17212 assert("console.log(var)ˇ;", "console.logˇ(var);");
17213
17214 // Inside bracket jumps to inside of matching bracket
17215 assert("console.log(ˇvar);", "console.log(varˇ);");
17216 assert("console.log(varˇ);", "console.log(ˇvar);");
17217
17218 // When outside a bracket and inside, favor jumping to the inside bracket
17219 assert(
17220 "console.log('foo', [1, 2, 3]ˇ);",
17221 "console.log(ˇ'foo', [1, 2, 3]);",
17222 );
17223 assert(
17224 "console.log(ˇ'foo', [1, 2, 3]);",
17225 "console.log('foo', [1, 2, 3]ˇ);",
17226 );
17227
17228 // Bias forward if two options are equally likely
17229 assert(
17230 "let result = curried_fun()ˇ();",
17231 "let result = curried_fun()()ˇ;",
17232 );
17233
17234 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17235 assert(
17236 indoc! {"
17237 function test() {
17238 console.log('test')ˇ
17239 }"},
17240 indoc! {"
17241 function test() {
17242 console.logˇ('test')
17243 }"},
17244 );
17245}
17246
17247#[gpui::test]
17248async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17249 init_test(cx, |_| {});
17250
17251 let fs = FakeFs::new(cx.executor());
17252 fs.insert_tree(
17253 path!("/a"),
17254 json!({
17255 "main.rs": "fn main() { let a = 5; }",
17256 "other.rs": "// Test file",
17257 }),
17258 )
17259 .await;
17260 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17261
17262 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17263 language_registry.add(Arc::new(Language::new(
17264 LanguageConfig {
17265 name: "Rust".into(),
17266 matcher: LanguageMatcher {
17267 path_suffixes: vec!["rs".to_string()],
17268 ..Default::default()
17269 },
17270 brackets: BracketPairConfig {
17271 pairs: vec![BracketPair {
17272 start: "{".to_string(),
17273 end: "}".to_string(),
17274 close: true,
17275 surround: true,
17276 newline: true,
17277 }],
17278 disabled_scopes_by_bracket_ix: Vec::new(),
17279 },
17280 ..Default::default()
17281 },
17282 Some(tree_sitter_rust::LANGUAGE.into()),
17283 )));
17284 let mut fake_servers = language_registry.register_fake_lsp(
17285 "Rust",
17286 FakeLspAdapter {
17287 capabilities: lsp::ServerCapabilities {
17288 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17289 first_trigger_character: "{".to_string(),
17290 more_trigger_character: None,
17291 }),
17292 ..Default::default()
17293 },
17294 ..Default::default()
17295 },
17296 );
17297
17298 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17299
17300 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17301
17302 let worktree_id = workspace
17303 .update(cx, |workspace, _, cx| {
17304 workspace.project().update(cx, |project, cx| {
17305 project.worktrees(cx).next().unwrap().read(cx).id()
17306 })
17307 })
17308 .unwrap();
17309
17310 let buffer = project
17311 .update(cx, |project, cx| {
17312 project.open_local_buffer(path!("/a/main.rs"), cx)
17313 })
17314 .await
17315 .unwrap();
17316 let editor_handle = workspace
17317 .update(cx, |workspace, window, cx| {
17318 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17319 })
17320 .unwrap()
17321 .await
17322 .unwrap()
17323 .downcast::<Editor>()
17324 .unwrap();
17325
17326 cx.executor().start_waiting();
17327 let fake_server = fake_servers.next().await.unwrap();
17328
17329 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17330 |params, _| async move {
17331 assert_eq!(
17332 params.text_document_position.text_document.uri,
17333 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17334 );
17335 assert_eq!(
17336 params.text_document_position.position,
17337 lsp::Position::new(0, 21),
17338 );
17339
17340 Ok(Some(vec![lsp::TextEdit {
17341 new_text: "]".to_string(),
17342 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17343 }]))
17344 },
17345 );
17346
17347 editor_handle.update_in(cx, |editor, window, cx| {
17348 window.focus(&editor.focus_handle(cx));
17349 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17350 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17351 });
17352 editor.handle_input("{", window, cx);
17353 });
17354
17355 cx.executor().run_until_parked();
17356
17357 buffer.update(cx, |buffer, _| {
17358 assert_eq!(
17359 buffer.text(),
17360 "fn main() { let a = {5}; }",
17361 "No extra braces from on type formatting should appear in the buffer"
17362 )
17363 });
17364}
17365
17366#[gpui::test(iterations = 20, seeds(31))]
17367async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17368 init_test(cx, |_| {});
17369
17370 let mut cx = EditorLspTestContext::new_rust(
17371 lsp::ServerCapabilities {
17372 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17373 first_trigger_character: ".".to_string(),
17374 more_trigger_character: None,
17375 }),
17376 ..Default::default()
17377 },
17378 cx,
17379 )
17380 .await;
17381
17382 cx.update_buffer(|buffer, _| {
17383 // This causes autoindent to be async.
17384 buffer.set_sync_parse_timeout(Duration::ZERO)
17385 });
17386
17387 cx.set_state("fn c() {\n d()ˇ\n}\n");
17388 cx.simulate_keystroke("\n");
17389 cx.run_until_parked();
17390
17391 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17392 let mut request =
17393 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17394 let buffer_cloned = buffer_cloned.clone();
17395 async move {
17396 buffer_cloned.update(&mut cx, |buffer, _| {
17397 assert_eq!(
17398 buffer.text(),
17399 "fn c() {\n d()\n .\n}\n",
17400 "OnTypeFormatting should triggered after autoindent applied"
17401 )
17402 })?;
17403
17404 Ok(Some(vec![]))
17405 }
17406 });
17407
17408 cx.simulate_keystroke(".");
17409 cx.run_until_parked();
17410
17411 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17412 assert!(request.next().await.is_some());
17413 request.close();
17414 assert!(request.next().await.is_none());
17415}
17416
17417#[gpui::test]
17418async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17419 init_test(cx, |_| {});
17420
17421 let fs = FakeFs::new(cx.executor());
17422 fs.insert_tree(
17423 path!("/a"),
17424 json!({
17425 "main.rs": "fn main() { let a = 5; }",
17426 "other.rs": "// Test file",
17427 }),
17428 )
17429 .await;
17430
17431 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17432
17433 let server_restarts = Arc::new(AtomicUsize::new(0));
17434 let closure_restarts = Arc::clone(&server_restarts);
17435 let language_server_name = "test language server";
17436 let language_name: LanguageName = "Rust".into();
17437
17438 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17439 language_registry.add(Arc::new(Language::new(
17440 LanguageConfig {
17441 name: language_name.clone(),
17442 matcher: LanguageMatcher {
17443 path_suffixes: vec!["rs".to_string()],
17444 ..Default::default()
17445 },
17446 ..Default::default()
17447 },
17448 Some(tree_sitter_rust::LANGUAGE.into()),
17449 )));
17450 let mut fake_servers = language_registry.register_fake_lsp(
17451 "Rust",
17452 FakeLspAdapter {
17453 name: language_server_name,
17454 initialization_options: Some(json!({
17455 "testOptionValue": true
17456 })),
17457 initializer: Some(Box::new(move |fake_server| {
17458 let task_restarts = Arc::clone(&closure_restarts);
17459 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17460 task_restarts.fetch_add(1, atomic::Ordering::Release);
17461 futures::future::ready(Ok(()))
17462 });
17463 })),
17464 ..Default::default()
17465 },
17466 );
17467
17468 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17469 let _buffer = project
17470 .update(cx, |project, cx| {
17471 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17472 })
17473 .await
17474 .unwrap();
17475 let _fake_server = fake_servers.next().await.unwrap();
17476 update_test_language_settings(cx, |language_settings| {
17477 language_settings.languages.0.insert(
17478 language_name.clone().0,
17479 LanguageSettingsContent {
17480 tab_size: NonZeroU32::new(8),
17481 ..Default::default()
17482 },
17483 );
17484 });
17485 cx.executor().run_until_parked();
17486 assert_eq!(
17487 server_restarts.load(atomic::Ordering::Acquire),
17488 0,
17489 "Should not restart LSP server on an unrelated change"
17490 );
17491
17492 update_test_project_settings(cx, |project_settings| {
17493 project_settings.lsp.insert(
17494 "Some other server name".into(),
17495 LspSettings {
17496 binary: None,
17497 settings: None,
17498 initialization_options: Some(json!({
17499 "some other init value": false
17500 })),
17501 enable_lsp_tasks: false,
17502 fetch: None,
17503 },
17504 );
17505 });
17506 cx.executor().run_until_parked();
17507 assert_eq!(
17508 server_restarts.load(atomic::Ordering::Acquire),
17509 0,
17510 "Should not restart LSP server on an unrelated LSP settings change"
17511 );
17512
17513 update_test_project_settings(cx, |project_settings| {
17514 project_settings.lsp.insert(
17515 language_server_name.into(),
17516 LspSettings {
17517 binary: None,
17518 settings: None,
17519 initialization_options: Some(json!({
17520 "anotherInitValue": false
17521 })),
17522 enable_lsp_tasks: false,
17523 fetch: None,
17524 },
17525 );
17526 });
17527 cx.executor().run_until_parked();
17528 assert_eq!(
17529 server_restarts.load(atomic::Ordering::Acquire),
17530 1,
17531 "Should restart LSP server on a related LSP settings change"
17532 );
17533
17534 update_test_project_settings(cx, |project_settings| {
17535 project_settings.lsp.insert(
17536 language_server_name.into(),
17537 LspSettings {
17538 binary: None,
17539 settings: None,
17540 initialization_options: Some(json!({
17541 "anotherInitValue": false
17542 })),
17543 enable_lsp_tasks: false,
17544 fetch: None,
17545 },
17546 );
17547 });
17548 cx.executor().run_until_parked();
17549 assert_eq!(
17550 server_restarts.load(atomic::Ordering::Acquire),
17551 1,
17552 "Should not restart LSP server on a related LSP settings change that is the same"
17553 );
17554
17555 update_test_project_settings(cx, |project_settings| {
17556 project_settings.lsp.insert(
17557 language_server_name.into(),
17558 LspSettings {
17559 binary: None,
17560 settings: None,
17561 initialization_options: None,
17562 enable_lsp_tasks: false,
17563 fetch: None,
17564 },
17565 );
17566 });
17567 cx.executor().run_until_parked();
17568 assert_eq!(
17569 server_restarts.load(atomic::Ordering::Acquire),
17570 2,
17571 "Should restart LSP server on another related LSP settings change"
17572 );
17573}
17574
17575#[gpui::test]
17576async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17577 init_test(cx, |_| {});
17578
17579 let mut cx = EditorLspTestContext::new_rust(
17580 lsp::ServerCapabilities {
17581 completion_provider: Some(lsp::CompletionOptions {
17582 trigger_characters: Some(vec![".".to_string()]),
17583 resolve_provider: Some(true),
17584 ..Default::default()
17585 }),
17586 ..Default::default()
17587 },
17588 cx,
17589 )
17590 .await;
17591
17592 cx.set_state("fn main() { let a = 2ˇ; }");
17593 cx.simulate_keystroke(".");
17594 let completion_item = lsp::CompletionItem {
17595 label: "some".into(),
17596 kind: Some(lsp::CompletionItemKind::SNIPPET),
17597 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17598 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17599 kind: lsp::MarkupKind::Markdown,
17600 value: "```rust\nSome(2)\n```".to_string(),
17601 })),
17602 deprecated: Some(false),
17603 sort_text: Some("fffffff2".to_string()),
17604 filter_text: Some("some".to_string()),
17605 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17606 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17607 range: lsp::Range {
17608 start: lsp::Position {
17609 line: 0,
17610 character: 22,
17611 },
17612 end: lsp::Position {
17613 line: 0,
17614 character: 22,
17615 },
17616 },
17617 new_text: "Some(2)".to_string(),
17618 })),
17619 additional_text_edits: Some(vec![lsp::TextEdit {
17620 range: lsp::Range {
17621 start: lsp::Position {
17622 line: 0,
17623 character: 20,
17624 },
17625 end: lsp::Position {
17626 line: 0,
17627 character: 22,
17628 },
17629 },
17630 new_text: "".to_string(),
17631 }]),
17632 ..Default::default()
17633 };
17634
17635 let closure_completion_item = completion_item.clone();
17636 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17637 let task_completion_item = closure_completion_item.clone();
17638 async move {
17639 Ok(Some(lsp::CompletionResponse::Array(vec![
17640 task_completion_item,
17641 ])))
17642 }
17643 });
17644
17645 request.next().await;
17646
17647 cx.condition(|editor, _| editor.context_menu_visible())
17648 .await;
17649 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17650 editor
17651 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17652 .unwrap()
17653 });
17654 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17655
17656 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17657 let task_completion_item = completion_item.clone();
17658 async move { Ok(task_completion_item) }
17659 })
17660 .next()
17661 .await
17662 .unwrap();
17663 apply_additional_edits.await.unwrap();
17664 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17665}
17666
17667#[gpui::test]
17668async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17669 init_test(cx, |_| {});
17670
17671 let mut cx = EditorLspTestContext::new_rust(
17672 lsp::ServerCapabilities {
17673 completion_provider: Some(lsp::CompletionOptions {
17674 trigger_characters: Some(vec![".".to_string()]),
17675 resolve_provider: Some(true),
17676 ..Default::default()
17677 }),
17678 ..Default::default()
17679 },
17680 cx,
17681 )
17682 .await;
17683
17684 cx.set_state("fn main() { let a = 2ˇ; }");
17685 cx.simulate_keystroke(".");
17686
17687 let item1 = lsp::CompletionItem {
17688 label: "method id()".to_string(),
17689 filter_text: Some("id".to_string()),
17690 detail: None,
17691 documentation: None,
17692 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17693 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17694 new_text: ".id".to_string(),
17695 })),
17696 ..lsp::CompletionItem::default()
17697 };
17698
17699 let item2 = lsp::CompletionItem {
17700 label: "other".to_string(),
17701 filter_text: Some("other".to_string()),
17702 detail: None,
17703 documentation: None,
17704 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17705 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17706 new_text: ".other".to_string(),
17707 })),
17708 ..lsp::CompletionItem::default()
17709 };
17710
17711 let item1 = item1.clone();
17712 cx.set_request_handler::<lsp::request::Completion, _, _>({
17713 let item1 = item1.clone();
17714 move |_, _, _| {
17715 let item1 = item1.clone();
17716 let item2 = item2.clone();
17717 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17718 }
17719 })
17720 .next()
17721 .await;
17722
17723 cx.condition(|editor, _| editor.context_menu_visible())
17724 .await;
17725 cx.update_editor(|editor, _, _| {
17726 let context_menu = editor.context_menu.borrow_mut();
17727 let context_menu = context_menu
17728 .as_ref()
17729 .expect("Should have the context menu deployed");
17730 match context_menu {
17731 CodeContextMenu::Completions(completions_menu) => {
17732 let completions = completions_menu.completions.borrow_mut();
17733 assert_eq!(
17734 completions
17735 .iter()
17736 .map(|completion| &completion.label.text)
17737 .collect::<Vec<_>>(),
17738 vec!["method id()", "other"]
17739 )
17740 }
17741 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17742 }
17743 });
17744
17745 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17746 let item1 = item1.clone();
17747 move |_, item_to_resolve, _| {
17748 let item1 = item1.clone();
17749 async move {
17750 if item1 == item_to_resolve {
17751 Ok(lsp::CompletionItem {
17752 label: "method id()".to_string(),
17753 filter_text: Some("id".to_string()),
17754 detail: Some("Now resolved!".to_string()),
17755 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17756 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17757 range: lsp::Range::new(
17758 lsp::Position::new(0, 22),
17759 lsp::Position::new(0, 22),
17760 ),
17761 new_text: ".id".to_string(),
17762 })),
17763 ..lsp::CompletionItem::default()
17764 })
17765 } else {
17766 Ok(item_to_resolve)
17767 }
17768 }
17769 }
17770 })
17771 .next()
17772 .await
17773 .unwrap();
17774 cx.run_until_parked();
17775
17776 cx.update_editor(|editor, window, cx| {
17777 editor.context_menu_next(&Default::default(), window, cx);
17778 });
17779
17780 cx.update_editor(|editor, _, _| {
17781 let context_menu = editor.context_menu.borrow_mut();
17782 let context_menu = context_menu
17783 .as_ref()
17784 .expect("Should have the context menu deployed");
17785 match context_menu {
17786 CodeContextMenu::Completions(completions_menu) => {
17787 let completions = completions_menu.completions.borrow_mut();
17788 assert_eq!(
17789 completions
17790 .iter()
17791 .map(|completion| &completion.label.text)
17792 .collect::<Vec<_>>(),
17793 vec!["method id() Now resolved!", "other"],
17794 "Should update first completion label, but not second as the filter text did not match."
17795 );
17796 }
17797 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17798 }
17799 });
17800}
17801
17802#[gpui::test]
17803async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17804 init_test(cx, |_| {});
17805 let mut cx = EditorLspTestContext::new_rust(
17806 lsp::ServerCapabilities {
17807 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17808 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17809 completion_provider: Some(lsp::CompletionOptions {
17810 resolve_provider: Some(true),
17811 ..Default::default()
17812 }),
17813 ..Default::default()
17814 },
17815 cx,
17816 )
17817 .await;
17818 cx.set_state(indoc! {"
17819 struct TestStruct {
17820 field: i32
17821 }
17822
17823 fn mainˇ() {
17824 let unused_var = 42;
17825 let test_struct = TestStruct { field: 42 };
17826 }
17827 "});
17828 let symbol_range = cx.lsp_range(indoc! {"
17829 struct TestStruct {
17830 field: i32
17831 }
17832
17833 «fn main»() {
17834 let unused_var = 42;
17835 let test_struct = TestStruct { field: 42 };
17836 }
17837 "});
17838 let mut hover_requests =
17839 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17840 Ok(Some(lsp::Hover {
17841 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17842 kind: lsp::MarkupKind::Markdown,
17843 value: "Function documentation".to_string(),
17844 }),
17845 range: Some(symbol_range),
17846 }))
17847 });
17848
17849 // Case 1: Test that code action menu hide hover popover
17850 cx.dispatch_action(Hover);
17851 hover_requests.next().await;
17852 cx.condition(|editor, _| editor.hover_state.visible()).await;
17853 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17854 move |_, _, _| async move {
17855 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17856 lsp::CodeAction {
17857 title: "Remove unused variable".to_string(),
17858 kind: Some(CodeActionKind::QUICKFIX),
17859 edit: Some(lsp::WorkspaceEdit {
17860 changes: Some(
17861 [(
17862 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17863 vec![lsp::TextEdit {
17864 range: lsp::Range::new(
17865 lsp::Position::new(5, 4),
17866 lsp::Position::new(5, 27),
17867 ),
17868 new_text: "".to_string(),
17869 }],
17870 )]
17871 .into_iter()
17872 .collect(),
17873 ),
17874 ..Default::default()
17875 }),
17876 ..Default::default()
17877 },
17878 )]))
17879 },
17880 );
17881 cx.update_editor(|editor, window, cx| {
17882 editor.toggle_code_actions(
17883 &ToggleCodeActions {
17884 deployed_from: None,
17885 quick_launch: false,
17886 },
17887 window,
17888 cx,
17889 );
17890 });
17891 code_action_requests.next().await;
17892 cx.run_until_parked();
17893 cx.condition(|editor, _| editor.context_menu_visible())
17894 .await;
17895 cx.update_editor(|editor, _, _| {
17896 assert!(
17897 !editor.hover_state.visible(),
17898 "Hover popover should be hidden when code action menu is shown"
17899 );
17900 // Hide code actions
17901 editor.context_menu.take();
17902 });
17903
17904 // Case 2: Test that code completions hide hover popover
17905 cx.dispatch_action(Hover);
17906 hover_requests.next().await;
17907 cx.condition(|editor, _| editor.hover_state.visible()).await;
17908 let counter = Arc::new(AtomicUsize::new(0));
17909 let mut completion_requests =
17910 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17911 let counter = counter.clone();
17912 async move {
17913 counter.fetch_add(1, atomic::Ordering::Release);
17914 Ok(Some(lsp::CompletionResponse::Array(vec![
17915 lsp::CompletionItem {
17916 label: "main".into(),
17917 kind: Some(lsp::CompletionItemKind::FUNCTION),
17918 detail: Some("() -> ()".to_string()),
17919 ..Default::default()
17920 },
17921 lsp::CompletionItem {
17922 label: "TestStruct".into(),
17923 kind: Some(lsp::CompletionItemKind::STRUCT),
17924 detail: Some("struct TestStruct".to_string()),
17925 ..Default::default()
17926 },
17927 ])))
17928 }
17929 });
17930 cx.update_editor(|editor, window, cx| {
17931 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17932 });
17933 completion_requests.next().await;
17934 cx.condition(|editor, _| editor.context_menu_visible())
17935 .await;
17936 cx.update_editor(|editor, _, _| {
17937 assert!(
17938 !editor.hover_state.visible(),
17939 "Hover popover should be hidden when completion menu is shown"
17940 );
17941 });
17942}
17943
17944#[gpui::test]
17945async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17946 init_test(cx, |_| {});
17947
17948 let mut cx = EditorLspTestContext::new_rust(
17949 lsp::ServerCapabilities {
17950 completion_provider: Some(lsp::CompletionOptions {
17951 trigger_characters: Some(vec![".".to_string()]),
17952 resolve_provider: Some(true),
17953 ..Default::default()
17954 }),
17955 ..Default::default()
17956 },
17957 cx,
17958 )
17959 .await;
17960
17961 cx.set_state("fn main() { let a = 2ˇ; }");
17962 cx.simulate_keystroke(".");
17963
17964 let unresolved_item_1 = lsp::CompletionItem {
17965 label: "id".to_string(),
17966 filter_text: Some("id".to_string()),
17967 detail: None,
17968 documentation: None,
17969 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17970 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17971 new_text: ".id".to_string(),
17972 })),
17973 ..lsp::CompletionItem::default()
17974 };
17975 let resolved_item_1 = lsp::CompletionItem {
17976 additional_text_edits: Some(vec![lsp::TextEdit {
17977 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17978 new_text: "!!".to_string(),
17979 }]),
17980 ..unresolved_item_1.clone()
17981 };
17982 let unresolved_item_2 = lsp::CompletionItem {
17983 label: "other".to_string(),
17984 filter_text: Some("other".to_string()),
17985 detail: None,
17986 documentation: None,
17987 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17988 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17989 new_text: ".other".to_string(),
17990 })),
17991 ..lsp::CompletionItem::default()
17992 };
17993 let resolved_item_2 = lsp::CompletionItem {
17994 additional_text_edits: Some(vec![lsp::TextEdit {
17995 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17996 new_text: "??".to_string(),
17997 }]),
17998 ..unresolved_item_2.clone()
17999 };
18000
18001 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18002 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18003 cx.lsp
18004 .server
18005 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18006 let unresolved_item_1 = unresolved_item_1.clone();
18007 let resolved_item_1 = resolved_item_1.clone();
18008 let unresolved_item_2 = unresolved_item_2.clone();
18009 let resolved_item_2 = resolved_item_2.clone();
18010 let resolve_requests_1 = resolve_requests_1.clone();
18011 let resolve_requests_2 = resolve_requests_2.clone();
18012 move |unresolved_request, _| {
18013 let unresolved_item_1 = unresolved_item_1.clone();
18014 let resolved_item_1 = resolved_item_1.clone();
18015 let unresolved_item_2 = unresolved_item_2.clone();
18016 let resolved_item_2 = resolved_item_2.clone();
18017 let resolve_requests_1 = resolve_requests_1.clone();
18018 let resolve_requests_2 = resolve_requests_2.clone();
18019 async move {
18020 if unresolved_request == unresolved_item_1 {
18021 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18022 Ok(resolved_item_1.clone())
18023 } else if unresolved_request == unresolved_item_2 {
18024 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18025 Ok(resolved_item_2.clone())
18026 } else {
18027 panic!("Unexpected completion item {unresolved_request:?}")
18028 }
18029 }
18030 }
18031 })
18032 .detach();
18033
18034 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18035 let unresolved_item_1 = unresolved_item_1.clone();
18036 let unresolved_item_2 = unresolved_item_2.clone();
18037 async move {
18038 Ok(Some(lsp::CompletionResponse::Array(vec![
18039 unresolved_item_1,
18040 unresolved_item_2,
18041 ])))
18042 }
18043 })
18044 .next()
18045 .await;
18046
18047 cx.condition(|editor, _| editor.context_menu_visible())
18048 .await;
18049 cx.update_editor(|editor, _, _| {
18050 let context_menu = editor.context_menu.borrow_mut();
18051 let context_menu = context_menu
18052 .as_ref()
18053 .expect("Should have the context menu deployed");
18054 match context_menu {
18055 CodeContextMenu::Completions(completions_menu) => {
18056 let completions = completions_menu.completions.borrow_mut();
18057 assert_eq!(
18058 completions
18059 .iter()
18060 .map(|completion| &completion.label.text)
18061 .collect::<Vec<_>>(),
18062 vec!["id", "other"]
18063 )
18064 }
18065 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18066 }
18067 });
18068 cx.run_until_parked();
18069
18070 cx.update_editor(|editor, window, cx| {
18071 editor.context_menu_next(&ContextMenuNext, window, cx);
18072 });
18073 cx.run_until_parked();
18074 cx.update_editor(|editor, window, cx| {
18075 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18076 });
18077 cx.run_until_parked();
18078 cx.update_editor(|editor, window, cx| {
18079 editor.context_menu_next(&ContextMenuNext, window, cx);
18080 });
18081 cx.run_until_parked();
18082 cx.update_editor(|editor, window, cx| {
18083 editor
18084 .compose_completion(&ComposeCompletion::default(), window, cx)
18085 .expect("No task returned")
18086 })
18087 .await
18088 .expect("Completion failed");
18089 cx.run_until_parked();
18090
18091 cx.update_editor(|editor, _, cx| {
18092 assert_eq!(
18093 resolve_requests_1.load(atomic::Ordering::Acquire),
18094 1,
18095 "Should always resolve once despite multiple selections"
18096 );
18097 assert_eq!(
18098 resolve_requests_2.load(atomic::Ordering::Acquire),
18099 1,
18100 "Should always resolve once after multiple selections and applying the completion"
18101 );
18102 assert_eq!(
18103 editor.text(cx),
18104 "fn main() { let a = ??.other; }",
18105 "Should use resolved data when applying the completion"
18106 );
18107 });
18108}
18109
18110#[gpui::test]
18111async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18112 init_test(cx, |_| {});
18113
18114 let item_0 = lsp::CompletionItem {
18115 label: "abs".into(),
18116 insert_text: Some("abs".into()),
18117 data: Some(json!({ "very": "special"})),
18118 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18119 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18120 lsp::InsertReplaceEdit {
18121 new_text: "abs".to_string(),
18122 insert: lsp::Range::default(),
18123 replace: lsp::Range::default(),
18124 },
18125 )),
18126 ..lsp::CompletionItem::default()
18127 };
18128 let items = iter::once(item_0.clone())
18129 .chain((11..51).map(|i| lsp::CompletionItem {
18130 label: format!("item_{}", i),
18131 insert_text: Some(format!("item_{}", i)),
18132 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18133 ..lsp::CompletionItem::default()
18134 }))
18135 .collect::<Vec<_>>();
18136
18137 let default_commit_characters = vec!["?".to_string()];
18138 let default_data = json!({ "default": "data"});
18139 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18140 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18141 let default_edit_range = lsp::Range {
18142 start: lsp::Position {
18143 line: 0,
18144 character: 5,
18145 },
18146 end: lsp::Position {
18147 line: 0,
18148 character: 5,
18149 },
18150 };
18151
18152 let mut cx = EditorLspTestContext::new_rust(
18153 lsp::ServerCapabilities {
18154 completion_provider: Some(lsp::CompletionOptions {
18155 trigger_characters: Some(vec![".".to_string()]),
18156 resolve_provider: Some(true),
18157 ..Default::default()
18158 }),
18159 ..Default::default()
18160 },
18161 cx,
18162 )
18163 .await;
18164
18165 cx.set_state("fn main() { let a = 2ˇ; }");
18166 cx.simulate_keystroke(".");
18167
18168 let completion_data = default_data.clone();
18169 let completion_characters = default_commit_characters.clone();
18170 let completion_items = items.clone();
18171 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18172 let default_data = completion_data.clone();
18173 let default_commit_characters = completion_characters.clone();
18174 let items = completion_items.clone();
18175 async move {
18176 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18177 items,
18178 item_defaults: Some(lsp::CompletionListItemDefaults {
18179 data: Some(default_data.clone()),
18180 commit_characters: Some(default_commit_characters.clone()),
18181 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18182 default_edit_range,
18183 )),
18184 insert_text_format: Some(default_insert_text_format),
18185 insert_text_mode: Some(default_insert_text_mode),
18186 }),
18187 ..lsp::CompletionList::default()
18188 })))
18189 }
18190 })
18191 .next()
18192 .await;
18193
18194 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18195 cx.lsp
18196 .server
18197 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18198 let closure_resolved_items = resolved_items.clone();
18199 move |item_to_resolve, _| {
18200 let closure_resolved_items = closure_resolved_items.clone();
18201 async move {
18202 closure_resolved_items.lock().push(item_to_resolve.clone());
18203 Ok(item_to_resolve)
18204 }
18205 }
18206 })
18207 .detach();
18208
18209 cx.condition(|editor, _| editor.context_menu_visible())
18210 .await;
18211 cx.run_until_parked();
18212 cx.update_editor(|editor, _, _| {
18213 let menu = editor.context_menu.borrow_mut();
18214 match menu.as_ref().expect("should have the completions menu") {
18215 CodeContextMenu::Completions(completions_menu) => {
18216 assert_eq!(
18217 completions_menu
18218 .entries
18219 .borrow()
18220 .iter()
18221 .map(|mat| mat.string.clone())
18222 .collect::<Vec<String>>(),
18223 items
18224 .iter()
18225 .map(|completion| completion.label.clone())
18226 .collect::<Vec<String>>()
18227 );
18228 }
18229 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18230 }
18231 });
18232 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18233 // with 4 from the end.
18234 assert_eq!(
18235 *resolved_items.lock(),
18236 [&items[0..16], &items[items.len() - 4..items.len()]]
18237 .concat()
18238 .iter()
18239 .cloned()
18240 .map(|mut item| {
18241 if item.data.is_none() {
18242 item.data = Some(default_data.clone());
18243 }
18244 item
18245 })
18246 .collect::<Vec<lsp::CompletionItem>>(),
18247 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18248 );
18249 resolved_items.lock().clear();
18250
18251 cx.update_editor(|editor, window, cx| {
18252 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18253 });
18254 cx.run_until_parked();
18255 // Completions that have already been resolved are skipped.
18256 assert_eq!(
18257 *resolved_items.lock(),
18258 items[items.len() - 17..items.len() - 4]
18259 .iter()
18260 .cloned()
18261 .map(|mut item| {
18262 if item.data.is_none() {
18263 item.data = Some(default_data.clone());
18264 }
18265 item
18266 })
18267 .collect::<Vec<lsp::CompletionItem>>()
18268 );
18269 resolved_items.lock().clear();
18270}
18271
18272#[gpui::test]
18273async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18274 init_test(cx, |_| {});
18275
18276 let mut cx = EditorLspTestContext::new(
18277 Language::new(
18278 LanguageConfig {
18279 matcher: LanguageMatcher {
18280 path_suffixes: vec!["jsx".into()],
18281 ..Default::default()
18282 },
18283 overrides: [(
18284 "element".into(),
18285 LanguageConfigOverride {
18286 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18287 ..Default::default()
18288 },
18289 )]
18290 .into_iter()
18291 .collect(),
18292 ..Default::default()
18293 },
18294 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18295 )
18296 .with_override_query("(jsx_self_closing_element) @element")
18297 .unwrap(),
18298 lsp::ServerCapabilities {
18299 completion_provider: Some(lsp::CompletionOptions {
18300 trigger_characters: Some(vec![":".to_string()]),
18301 ..Default::default()
18302 }),
18303 ..Default::default()
18304 },
18305 cx,
18306 )
18307 .await;
18308
18309 cx.lsp
18310 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18311 Ok(Some(lsp::CompletionResponse::Array(vec![
18312 lsp::CompletionItem {
18313 label: "bg-blue".into(),
18314 ..Default::default()
18315 },
18316 lsp::CompletionItem {
18317 label: "bg-red".into(),
18318 ..Default::default()
18319 },
18320 lsp::CompletionItem {
18321 label: "bg-yellow".into(),
18322 ..Default::default()
18323 },
18324 ])))
18325 });
18326
18327 cx.set_state(r#"<p class="bgˇ" />"#);
18328
18329 // Trigger completion when typing a dash, because the dash is an extra
18330 // word character in the 'element' scope, which contains the cursor.
18331 cx.simulate_keystroke("-");
18332 cx.executor().run_until_parked();
18333 cx.update_editor(|editor, _, _| {
18334 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18335 {
18336 assert_eq!(
18337 completion_menu_entries(menu),
18338 &["bg-blue", "bg-red", "bg-yellow"]
18339 );
18340 } else {
18341 panic!("expected completion menu to be open");
18342 }
18343 });
18344
18345 cx.simulate_keystroke("l");
18346 cx.executor().run_until_parked();
18347 cx.update_editor(|editor, _, _| {
18348 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18349 {
18350 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18351 } else {
18352 panic!("expected completion menu to be open");
18353 }
18354 });
18355
18356 // When filtering completions, consider the character after the '-' to
18357 // be the start of a subword.
18358 cx.set_state(r#"<p class="yelˇ" />"#);
18359 cx.simulate_keystroke("l");
18360 cx.executor().run_until_parked();
18361 cx.update_editor(|editor, _, _| {
18362 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18363 {
18364 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18365 } else {
18366 panic!("expected completion menu to be open");
18367 }
18368 });
18369}
18370
18371fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18372 let entries = menu.entries.borrow();
18373 entries.iter().map(|mat| mat.string.clone()).collect()
18374}
18375
18376#[gpui::test]
18377async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18378 init_test(cx, |settings| {
18379 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18380 });
18381
18382 let fs = FakeFs::new(cx.executor());
18383 fs.insert_file(path!("/file.ts"), Default::default()).await;
18384
18385 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18386 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18387
18388 language_registry.add(Arc::new(Language::new(
18389 LanguageConfig {
18390 name: "TypeScript".into(),
18391 matcher: LanguageMatcher {
18392 path_suffixes: vec!["ts".to_string()],
18393 ..Default::default()
18394 },
18395 ..Default::default()
18396 },
18397 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18398 )));
18399 update_test_language_settings(cx, |settings| {
18400 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18401 });
18402
18403 let test_plugin = "test_plugin";
18404 let _ = language_registry.register_fake_lsp(
18405 "TypeScript",
18406 FakeLspAdapter {
18407 prettier_plugins: vec![test_plugin],
18408 ..Default::default()
18409 },
18410 );
18411
18412 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18413 let buffer = project
18414 .update(cx, |project, cx| {
18415 project.open_local_buffer(path!("/file.ts"), cx)
18416 })
18417 .await
18418 .unwrap();
18419
18420 let buffer_text = "one\ntwo\nthree\n";
18421 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18422 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18423 editor.update_in(cx, |editor, window, cx| {
18424 editor.set_text(buffer_text, window, cx)
18425 });
18426
18427 editor
18428 .update_in(cx, |editor, window, cx| {
18429 editor.perform_format(
18430 project.clone(),
18431 FormatTrigger::Manual,
18432 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18433 window,
18434 cx,
18435 )
18436 })
18437 .unwrap()
18438 .await;
18439 assert_eq!(
18440 editor.update(cx, |editor, cx| editor.text(cx)),
18441 buffer_text.to_string() + prettier_format_suffix,
18442 "Test prettier formatting was not applied to the original buffer text",
18443 );
18444
18445 update_test_language_settings(cx, |settings| {
18446 settings.defaults.formatter = Some(FormatterList::default())
18447 });
18448 let format = editor.update_in(cx, |editor, window, cx| {
18449 editor.perform_format(
18450 project.clone(),
18451 FormatTrigger::Manual,
18452 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18453 window,
18454 cx,
18455 )
18456 });
18457 format.await.unwrap();
18458 assert_eq!(
18459 editor.update(cx, |editor, cx| editor.text(cx)),
18460 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18461 "Autoformatting (via test prettier) was not applied to the original buffer text",
18462 );
18463}
18464
18465#[gpui::test]
18466async fn test_addition_reverts(cx: &mut TestAppContext) {
18467 init_test(cx, |_| {});
18468 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18469 let base_text = indoc! {r#"
18470 struct Row;
18471 struct Row1;
18472 struct Row2;
18473
18474 struct Row4;
18475 struct Row5;
18476 struct Row6;
18477
18478 struct Row8;
18479 struct Row9;
18480 struct Row10;"#};
18481
18482 // When addition hunks are not adjacent to carets, no hunk revert is performed
18483 assert_hunk_revert(
18484 indoc! {r#"struct Row;
18485 struct Row1;
18486 struct Row1.1;
18487 struct Row1.2;
18488 struct Row2;ˇ
18489
18490 struct Row4;
18491 struct Row5;
18492 struct Row6;
18493
18494 struct Row8;
18495 ˇstruct Row9;
18496 struct Row9.1;
18497 struct Row9.2;
18498 struct Row9.3;
18499 struct Row10;"#},
18500 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18501 indoc! {r#"struct Row;
18502 struct Row1;
18503 struct Row1.1;
18504 struct Row1.2;
18505 struct Row2;ˇ
18506
18507 struct Row4;
18508 struct Row5;
18509 struct Row6;
18510
18511 struct Row8;
18512 ˇstruct Row9;
18513 struct Row9.1;
18514 struct Row9.2;
18515 struct Row9.3;
18516 struct Row10;"#},
18517 base_text,
18518 &mut cx,
18519 );
18520 // Same for selections
18521 assert_hunk_revert(
18522 indoc! {r#"struct Row;
18523 struct Row1;
18524 struct Row2;
18525 struct Row2.1;
18526 struct Row2.2;
18527 «ˇ
18528 struct Row4;
18529 struct» Row5;
18530 «struct Row6;
18531 ˇ»
18532 struct Row9.1;
18533 struct Row9.2;
18534 struct Row9.3;
18535 struct Row8;
18536 struct Row9;
18537 struct Row10;"#},
18538 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18539 indoc! {r#"struct Row;
18540 struct Row1;
18541 struct Row2;
18542 struct Row2.1;
18543 struct Row2.2;
18544 «ˇ
18545 struct Row4;
18546 struct» Row5;
18547 «struct Row6;
18548 ˇ»
18549 struct Row9.1;
18550 struct Row9.2;
18551 struct Row9.3;
18552 struct Row8;
18553 struct Row9;
18554 struct Row10;"#},
18555 base_text,
18556 &mut cx,
18557 );
18558
18559 // When carets and selections intersect the addition hunks, those are reverted.
18560 // Adjacent carets got merged.
18561 assert_hunk_revert(
18562 indoc! {r#"struct Row;
18563 ˇ// something on the top
18564 struct Row1;
18565 struct Row2;
18566 struct Roˇw3.1;
18567 struct Row2.2;
18568 struct Row2.3;ˇ
18569
18570 struct Row4;
18571 struct ˇRow5.1;
18572 struct Row5.2;
18573 struct «Rowˇ»5.3;
18574 struct Row5;
18575 struct Row6;
18576 ˇ
18577 struct Row9.1;
18578 struct «Rowˇ»9.2;
18579 struct «ˇRow»9.3;
18580 struct Row8;
18581 struct Row9;
18582 «ˇ// something on bottom»
18583 struct Row10;"#},
18584 vec![
18585 DiffHunkStatusKind::Added,
18586 DiffHunkStatusKind::Added,
18587 DiffHunkStatusKind::Added,
18588 DiffHunkStatusKind::Added,
18589 DiffHunkStatusKind::Added,
18590 ],
18591 indoc! {r#"struct Row;
18592 ˇstruct Row1;
18593 struct Row2;
18594 ˇ
18595 struct Row4;
18596 ˇstruct Row5;
18597 struct Row6;
18598 ˇ
18599 ˇstruct Row8;
18600 struct Row9;
18601 ˇstruct Row10;"#},
18602 base_text,
18603 &mut cx,
18604 );
18605}
18606
18607#[gpui::test]
18608async fn test_modification_reverts(cx: &mut TestAppContext) {
18609 init_test(cx, |_| {});
18610 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18611 let base_text = indoc! {r#"
18612 struct Row;
18613 struct Row1;
18614 struct Row2;
18615
18616 struct Row4;
18617 struct Row5;
18618 struct Row6;
18619
18620 struct Row8;
18621 struct Row9;
18622 struct Row10;"#};
18623
18624 // Modification hunks behave the same as the addition ones.
18625 assert_hunk_revert(
18626 indoc! {r#"struct Row;
18627 struct Row1;
18628 struct Row33;
18629 ˇ
18630 struct Row4;
18631 struct Row5;
18632 struct Row6;
18633 ˇ
18634 struct Row99;
18635 struct Row9;
18636 struct Row10;"#},
18637 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18638 indoc! {r#"struct Row;
18639 struct Row1;
18640 struct Row33;
18641 ˇ
18642 struct Row4;
18643 struct Row5;
18644 struct Row6;
18645 ˇ
18646 struct Row99;
18647 struct Row9;
18648 struct Row10;"#},
18649 base_text,
18650 &mut cx,
18651 );
18652 assert_hunk_revert(
18653 indoc! {r#"struct Row;
18654 struct Row1;
18655 struct Row33;
18656 «ˇ
18657 struct Row4;
18658 struct» Row5;
18659 «struct Row6;
18660 ˇ»
18661 struct Row99;
18662 struct Row9;
18663 struct Row10;"#},
18664 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18665 indoc! {r#"struct Row;
18666 struct Row1;
18667 struct Row33;
18668 «ˇ
18669 struct Row4;
18670 struct» Row5;
18671 «struct Row6;
18672 ˇ»
18673 struct Row99;
18674 struct Row9;
18675 struct Row10;"#},
18676 base_text,
18677 &mut cx,
18678 );
18679
18680 assert_hunk_revert(
18681 indoc! {r#"ˇstruct Row1.1;
18682 struct Row1;
18683 «ˇstr»uct Row22;
18684
18685 struct ˇRow44;
18686 struct Row5;
18687 struct «Rˇ»ow66;ˇ
18688
18689 «struˇ»ct Row88;
18690 struct Row9;
18691 struct Row1011;ˇ"#},
18692 vec![
18693 DiffHunkStatusKind::Modified,
18694 DiffHunkStatusKind::Modified,
18695 DiffHunkStatusKind::Modified,
18696 DiffHunkStatusKind::Modified,
18697 DiffHunkStatusKind::Modified,
18698 DiffHunkStatusKind::Modified,
18699 ],
18700 indoc! {r#"struct Row;
18701 ˇstruct Row1;
18702 struct Row2;
18703 ˇ
18704 struct Row4;
18705 ˇstruct Row5;
18706 struct Row6;
18707 ˇ
18708 struct Row8;
18709 ˇstruct Row9;
18710 struct Row10;ˇ"#},
18711 base_text,
18712 &mut cx,
18713 );
18714}
18715
18716#[gpui::test]
18717async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18718 init_test(cx, |_| {});
18719 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18720 let base_text = indoc! {r#"
18721 one
18722
18723 two
18724 three
18725 "#};
18726
18727 cx.set_head_text(base_text);
18728 cx.set_state("\nˇ\n");
18729 cx.executor().run_until_parked();
18730 cx.update_editor(|editor, _window, cx| {
18731 editor.expand_selected_diff_hunks(cx);
18732 });
18733 cx.executor().run_until_parked();
18734 cx.update_editor(|editor, window, cx| {
18735 editor.backspace(&Default::default(), window, cx);
18736 });
18737 cx.run_until_parked();
18738 cx.assert_state_with_diff(
18739 indoc! {r#"
18740
18741 - two
18742 - threeˇ
18743 +
18744 "#}
18745 .to_string(),
18746 );
18747}
18748
18749#[gpui::test]
18750async fn test_deletion_reverts(cx: &mut TestAppContext) {
18751 init_test(cx, |_| {});
18752 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18753 let base_text = indoc! {r#"struct Row;
18754struct Row1;
18755struct Row2;
18756
18757struct Row4;
18758struct Row5;
18759struct Row6;
18760
18761struct Row8;
18762struct Row9;
18763struct Row10;"#};
18764
18765 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18766 assert_hunk_revert(
18767 indoc! {r#"struct Row;
18768 struct Row2;
18769
18770 ˇstruct Row4;
18771 struct Row5;
18772 struct Row6;
18773 ˇ
18774 struct Row8;
18775 struct Row10;"#},
18776 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18777 indoc! {r#"struct Row;
18778 struct Row2;
18779
18780 ˇstruct Row4;
18781 struct Row5;
18782 struct Row6;
18783 ˇ
18784 struct Row8;
18785 struct Row10;"#},
18786 base_text,
18787 &mut cx,
18788 );
18789 assert_hunk_revert(
18790 indoc! {r#"struct Row;
18791 struct Row2;
18792
18793 «ˇstruct Row4;
18794 struct» Row5;
18795 «struct Row6;
18796 ˇ»
18797 struct Row8;
18798 struct Row10;"#},
18799 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18800 indoc! {r#"struct Row;
18801 struct Row2;
18802
18803 «ˇstruct Row4;
18804 struct» Row5;
18805 «struct Row6;
18806 ˇ»
18807 struct Row8;
18808 struct Row10;"#},
18809 base_text,
18810 &mut cx,
18811 );
18812
18813 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18814 assert_hunk_revert(
18815 indoc! {r#"struct Row;
18816 ˇstruct Row2;
18817
18818 struct Row4;
18819 struct Row5;
18820 struct Row6;
18821
18822 struct Row8;ˇ
18823 struct Row10;"#},
18824 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18825 indoc! {r#"struct Row;
18826 struct Row1;
18827 ˇstruct Row2;
18828
18829 struct Row4;
18830 struct Row5;
18831 struct Row6;
18832
18833 struct Row8;ˇ
18834 struct Row9;
18835 struct Row10;"#},
18836 base_text,
18837 &mut cx,
18838 );
18839 assert_hunk_revert(
18840 indoc! {r#"struct Row;
18841 struct Row2«ˇ;
18842 struct Row4;
18843 struct» Row5;
18844 «struct Row6;
18845
18846 struct Row8;ˇ»
18847 struct Row10;"#},
18848 vec![
18849 DiffHunkStatusKind::Deleted,
18850 DiffHunkStatusKind::Deleted,
18851 DiffHunkStatusKind::Deleted,
18852 ],
18853 indoc! {r#"struct Row;
18854 struct Row1;
18855 struct Row2«ˇ;
18856
18857 struct Row4;
18858 struct» Row5;
18859 «struct Row6;
18860
18861 struct Row8;ˇ»
18862 struct Row9;
18863 struct Row10;"#},
18864 base_text,
18865 &mut cx,
18866 );
18867}
18868
18869#[gpui::test]
18870async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18871 init_test(cx, |_| {});
18872
18873 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18874 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18875 let base_text_3 =
18876 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18877
18878 let text_1 = edit_first_char_of_every_line(base_text_1);
18879 let text_2 = edit_first_char_of_every_line(base_text_2);
18880 let text_3 = edit_first_char_of_every_line(base_text_3);
18881
18882 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18883 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18884 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18885
18886 let multibuffer = cx.new(|cx| {
18887 let mut multibuffer = MultiBuffer::new(ReadWrite);
18888 multibuffer.push_excerpts(
18889 buffer_1.clone(),
18890 [
18891 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18892 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18893 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18894 ],
18895 cx,
18896 );
18897 multibuffer.push_excerpts(
18898 buffer_2.clone(),
18899 [
18900 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18901 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18902 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18903 ],
18904 cx,
18905 );
18906 multibuffer.push_excerpts(
18907 buffer_3.clone(),
18908 [
18909 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18910 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18911 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18912 ],
18913 cx,
18914 );
18915 multibuffer
18916 });
18917
18918 let fs = FakeFs::new(cx.executor());
18919 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18920 let (editor, cx) = cx
18921 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18922 editor.update_in(cx, |editor, _window, cx| {
18923 for (buffer, diff_base) in [
18924 (buffer_1.clone(), base_text_1),
18925 (buffer_2.clone(), base_text_2),
18926 (buffer_3.clone(), base_text_3),
18927 ] {
18928 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18929 editor
18930 .buffer
18931 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18932 }
18933 });
18934 cx.executor().run_until_parked();
18935
18936 editor.update_in(cx, |editor, window, cx| {
18937 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}");
18938 editor.select_all(&SelectAll, window, cx);
18939 editor.git_restore(&Default::default(), window, cx);
18940 });
18941 cx.executor().run_until_parked();
18942
18943 // When all ranges are selected, all buffer hunks are reverted.
18944 editor.update(cx, |editor, cx| {
18945 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");
18946 });
18947 buffer_1.update(cx, |buffer, _| {
18948 assert_eq!(buffer.text(), base_text_1);
18949 });
18950 buffer_2.update(cx, |buffer, _| {
18951 assert_eq!(buffer.text(), base_text_2);
18952 });
18953 buffer_3.update(cx, |buffer, _| {
18954 assert_eq!(buffer.text(), base_text_3);
18955 });
18956
18957 editor.update_in(cx, |editor, window, cx| {
18958 editor.undo(&Default::default(), window, cx);
18959 });
18960
18961 editor.update_in(cx, |editor, window, cx| {
18962 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18963 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18964 });
18965 editor.git_restore(&Default::default(), window, cx);
18966 });
18967
18968 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18969 // but not affect buffer_2 and its related excerpts.
18970 editor.update(cx, |editor, cx| {
18971 assert_eq!(
18972 editor.text(cx),
18973 "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}"
18974 );
18975 });
18976 buffer_1.update(cx, |buffer, _| {
18977 assert_eq!(buffer.text(), base_text_1);
18978 });
18979 buffer_2.update(cx, |buffer, _| {
18980 assert_eq!(
18981 buffer.text(),
18982 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18983 );
18984 });
18985 buffer_3.update(cx, |buffer, _| {
18986 assert_eq!(
18987 buffer.text(),
18988 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18989 );
18990 });
18991
18992 fn edit_first_char_of_every_line(text: &str) -> String {
18993 text.split('\n')
18994 .map(|line| format!("X{}", &line[1..]))
18995 .collect::<Vec<_>>()
18996 .join("\n")
18997 }
18998}
18999
19000#[gpui::test]
19001async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19002 init_test(cx, |_| {});
19003
19004 let cols = 4;
19005 let rows = 10;
19006 let sample_text_1 = sample_text(rows, cols, 'a');
19007 assert_eq!(
19008 sample_text_1,
19009 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19010 );
19011 let sample_text_2 = sample_text(rows, cols, 'l');
19012 assert_eq!(
19013 sample_text_2,
19014 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19015 );
19016 let sample_text_3 = sample_text(rows, cols, 'v');
19017 assert_eq!(
19018 sample_text_3,
19019 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19020 );
19021
19022 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19023 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19024 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19025
19026 let multi_buffer = cx.new(|cx| {
19027 let mut multibuffer = MultiBuffer::new(ReadWrite);
19028 multibuffer.push_excerpts(
19029 buffer_1.clone(),
19030 [
19031 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19032 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19033 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19034 ],
19035 cx,
19036 );
19037 multibuffer.push_excerpts(
19038 buffer_2.clone(),
19039 [
19040 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19041 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19042 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19043 ],
19044 cx,
19045 );
19046 multibuffer.push_excerpts(
19047 buffer_3.clone(),
19048 [
19049 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19050 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19051 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19052 ],
19053 cx,
19054 );
19055 multibuffer
19056 });
19057
19058 let fs = FakeFs::new(cx.executor());
19059 fs.insert_tree(
19060 "/a",
19061 json!({
19062 "main.rs": sample_text_1,
19063 "other.rs": sample_text_2,
19064 "lib.rs": sample_text_3,
19065 }),
19066 )
19067 .await;
19068 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19069 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19070 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19071 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19072 Editor::new(
19073 EditorMode::full(),
19074 multi_buffer,
19075 Some(project.clone()),
19076 window,
19077 cx,
19078 )
19079 });
19080 let multibuffer_item_id = workspace
19081 .update(cx, |workspace, window, cx| {
19082 assert!(
19083 workspace.active_item(cx).is_none(),
19084 "active item should be None before the first item is added"
19085 );
19086 workspace.add_item_to_active_pane(
19087 Box::new(multi_buffer_editor.clone()),
19088 None,
19089 true,
19090 window,
19091 cx,
19092 );
19093 let active_item = workspace
19094 .active_item(cx)
19095 .expect("should have an active item after adding the multi buffer");
19096 assert_eq!(
19097 active_item.buffer_kind(cx),
19098 ItemBufferKind::Multibuffer,
19099 "A multi buffer was expected to active after adding"
19100 );
19101 active_item.item_id()
19102 })
19103 .unwrap();
19104 cx.executor().run_until_parked();
19105
19106 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19107 editor.change_selections(
19108 SelectionEffects::scroll(Autoscroll::Next),
19109 window,
19110 cx,
19111 |s| s.select_ranges(Some(1..2)),
19112 );
19113 editor.open_excerpts(&OpenExcerpts, window, cx);
19114 });
19115 cx.executor().run_until_parked();
19116 let first_item_id = workspace
19117 .update(cx, |workspace, window, cx| {
19118 let active_item = workspace
19119 .active_item(cx)
19120 .expect("should have an active item after navigating into the 1st buffer");
19121 let first_item_id = active_item.item_id();
19122 assert_ne!(
19123 first_item_id, multibuffer_item_id,
19124 "Should navigate into the 1st buffer and activate it"
19125 );
19126 assert_eq!(
19127 active_item.buffer_kind(cx),
19128 ItemBufferKind::Singleton,
19129 "New active item should be a singleton buffer"
19130 );
19131 assert_eq!(
19132 active_item
19133 .act_as::<Editor>(cx)
19134 .expect("should have navigated into an editor for the 1st buffer")
19135 .read(cx)
19136 .text(cx),
19137 sample_text_1
19138 );
19139
19140 workspace
19141 .go_back(workspace.active_pane().downgrade(), window, cx)
19142 .detach_and_log_err(cx);
19143
19144 first_item_id
19145 })
19146 .unwrap();
19147 cx.executor().run_until_parked();
19148 workspace
19149 .update(cx, |workspace, _, cx| {
19150 let active_item = workspace
19151 .active_item(cx)
19152 .expect("should have an active item after navigating back");
19153 assert_eq!(
19154 active_item.item_id(),
19155 multibuffer_item_id,
19156 "Should navigate back to the multi buffer"
19157 );
19158 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19159 })
19160 .unwrap();
19161
19162 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19163 editor.change_selections(
19164 SelectionEffects::scroll(Autoscroll::Next),
19165 window,
19166 cx,
19167 |s| s.select_ranges(Some(39..40)),
19168 );
19169 editor.open_excerpts(&OpenExcerpts, window, cx);
19170 });
19171 cx.executor().run_until_parked();
19172 let second_item_id = workspace
19173 .update(cx, |workspace, window, cx| {
19174 let active_item = workspace
19175 .active_item(cx)
19176 .expect("should have an active item after navigating into the 2nd buffer");
19177 let second_item_id = active_item.item_id();
19178 assert_ne!(
19179 second_item_id, multibuffer_item_id,
19180 "Should navigate away from the multibuffer"
19181 );
19182 assert_ne!(
19183 second_item_id, first_item_id,
19184 "Should navigate into the 2nd buffer and activate it"
19185 );
19186 assert_eq!(
19187 active_item.buffer_kind(cx),
19188 ItemBufferKind::Singleton,
19189 "New active item should be a singleton buffer"
19190 );
19191 assert_eq!(
19192 active_item
19193 .act_as::<Editor>(cx)
19194 .expect("should have navigated into an editor")
19195 .read(cx)
19196 .text(cx),
19197 sample_text_2
19198 );
19199
19200 workspace
19201 .go_back(workspace.active_pane().downgrade(), window, cx)
19202 .detach_and_log_err(cx);
19203
19204 second_item_id
19205 })
19206 .unwrap();
19207 cx.executor().run_until_parked();
19208 workspace
19209 .update(cx, |workspace, _, cx| {
19210 let active_item = workspace
19211 .active_item(cx)
19212 .expect("should have an active item after navigating back from the 2nd buffer");
19213 assert_eq!(
19214 active_item.item_id(),
19215 multibuffer_item_id,
19216 "Should navigate back from the 2nd buffer to the multi buffer"
19217 );
19218 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19219 })
19220 .unwrap();
19221
19222 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19223 editor.change_selections(
19224 SelectionEffects::scroll(Autoscroll::Next),
19225 window,
19226 cx,
19227 |s| s.select_ranges(Some(70..70)),
19228 );
19229 editor.open_excerpts(&OpenExcerpts, window, cx);
19230 });
19231 cx.executor().run_until_parked();
19232 workspace
19233 .update(cx, |workspace, window, cx| {
19234 let active_item = workspace
19235 .active_item(cx)
19236 .expect("should have an active item after navigating into the 3rd buffer");
19237 let third_item_id = active_item.item_id();
19238 assert_ne!(
19239 third_item_id, multibuffer_item_id,
19240 "Should navigate into the 3rd buffer and activate it"
19241 );
19242 assert_ne!(third_item_id, first_item_id);
19243 assert_ne!(third_item_id, second_item_id);
19244 assert_eq!(
19245 active_item.buffer_kind(cx),
19246 ItemBufferKind::Singleton,
19247 "New active item should be a singleton buffer"
19248 );
19249 assert_eq!(
19250 active_item
19251 .act_as::<Editor>(cx)
19252 .expect("should have navigated into an editor")
19253 .read(cx)
19254 .text(cx),
19255 sample_text_3
19256 );
19257
19258 workspace
19259 .go_back(workspace.active_pane().downgrade(), window, cx)
19260 .detach_and_log_err(cx);
19261 })
19262 .unwrap();
19263 cx.executor().run_until_parked();
19264 workspace
19265 .update(cx, |workspace, _, cx| {
19266 let active_item = workspace
19267 .active_item(cx)
19268 .expect("should have an active item after navigating back from the 3rd buffer");
19269 assert_eq!(
19270 active_item.item_id(),
19271 multibuffer_item_id,
19272 "Should navigate back from the 3rd buffer to the multi buffer"
19273 );
19274 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19275 })
19276 .unwrap();
19277}
19278
19279#[gpui::test]
19280async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19281 init_test(cx, |_| {});
19282
19283 let mut cx = EditorTestContext::new(cx).await;
19284
19285 let diff_base = r#"
19286 use some::mod;
19287
19288 const A: u32 = 42;
19289
19290 fn main() {
19291 println!("hello");
19292
19293 println!("world");
19294 }
19295 "#
19296 .unindent();
19297
19298 cx.set_state(
19299 &r#"
19300 use some::modified;
19301
19302 ˇ
19303 fn main() {
19304 println!("hello there");
19305
19306 println!("around the");
19307 println!("world");
19308 }
19309 "#
19310 .unindent(),
19311 );
19312
19313 cx.set_head_text(&diff_base);
19314 executor.run_until_parked();
19315
19316 cx.update_editor(|editor, window, cx| {
19317 editor.go_to_next_hunk(&GoToHunk, window, cx);
19318 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19319 });
19320 executor.run_until_parked();
19321 cx.assert_state_with_diff(
19322 r#"
19323 use some::modified;
19324
19325
19326 fn main() {
19327 - println!("hello");
19328 + ˇ println!("hello there");
19329
19330 println!("around the");
19331 println!("world");
19332 }
19333 "#
19334 .unindent(),
19335 );
19336
19337 cx.update_editor(|editor, window, cx| {
19338 for _ in 0..2 {
19339 editor.go_to_next_hunk(&GoToHunk, window, cx);
19340 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19341 }
19342 });
19343 executor.run_until_parked();
19344 cx.assert_state_with_diff(
19345 r#"
19346 - use some::mod;
19347 + ˇuse some::modified;
19348
19349
19350 fn main() {
19351 - println!("hello");
19352 + println!("hello there");
19353
19354 + println!("around the");
19355 println!("world");
19356 }
19357 "#
19358 .unindent(),
19359 );
19360
19361 cx.update_editor(|editor, window, cx| {
19362 editor.go_to_next_hunk(&GoToHunk, window, cx);
19363 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19364 });
19365 executor.run_until_parked();
19366 cx.assert_state_with_diff(
19367 r#"
19368 - use some::mod;
19369 + use some::modified;
19370
19371 - const A: u32 = 42;
19372 ˇ
19373 fn main() {
19374 - println!("hello");
19375 + println!("hello there");
19376
19377 + println!("around the");
19378 println!("world");
19379 }
19380 "#
19381 .unindent(),
19382 );
19383
19384 cx.update_editor(|editor, window, cx| {
19385 editor.cancel(&Cancel, window, cx);
19386 });
19387
19388 cx.assert_state_with_diff(
19389 r#"
19390 use some::modified;
19391
19392 ˇ
19393 fn main() {
19394 println!("hello there");
19395
19396 println!("around the");
19397 println!("world");
19398 }
19399 "#
19400 .unindent(),
19401 );
19402}
19403
19404#[gpui::test]
19405async fn test_diff_base_change_with_expanded_diff_hunks(
19406 executor: BackgroundExecutor,
19407 cx: &mut TestAppContext,
19408) {
19409 init_test(cx, |_| {});
19410
19411 let mut cx = EditorTestContext::new(cx).await;
19412
19413 let diff_base = r#"
19414 use some::mod1;
19415 use some::mod2;
19416
19417 const A: u32 = 42;
19418 const B: u32 = 42;
19419 const C: u32 = 42;
19420
19421 fn main() {
19422 println!("hello");
19423
19424 println!("world");
19425 }
19426 "#
19427 .unindent();
19428
19429 cx.set_state(
19430 &r#"
19431 use some::mod2;
19432
19433 const A: u32 = 42;
19434 const C: u32 = 42;
19435
19436 fn main(ˇ) {
19437 //println!("hello");
19438
19439 println!("world");
19440 //
19441 //
19442 }
19443 "#
19444 .unindent(),
19445 );
19446
19447 cx.set_head_text(&diff_base);
19448 executor.run_until_parked();
19449
19450 cx.update_editor(|editor, window, cx| {
19451 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19452 });
19453 executor.run_until_parked();
19454 cx.assert_state_with_diff(
19455 r#"
19456 - use some::mod1;
19457 use some::mod2;
19458
19459 const A: u32 = 42;
19460 - const B: u32 = 42;
19461 const C: u32 = 42;
19462
19463 fn main(ˇ) {
19464 - println!("hello");
19465 + //println!("hello");
19466
19467 println!("world");
19468 + //
19469 + //
19470 }
19471 "#
19472 .unindent(),
19473 );
19474
19475 cx.set_head_text("new diff base!");
19476 executor.run_until_parked();
19477 cx.assert_state_with_diff(
19478 r#"
19479 - new diff base!
19480 + use some::mod2;
19481 +
19482 + const A: u32 = 42;
19483 + const C: u32 = 42;
19484 +
19485 + fn main(ˇ) {
19486 + //println!("hello");
19487 +
19488 + println!("world");
19489 + //
19490 + //
19491 + }
19492 "#
19493 .unindent(),
19494 );
19495}
19496
19497#[gpui::test]
19498async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19499 init_test(cx, |_| {});
19500
19501 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19502 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19503 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19504 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19505 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19506 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19507
19508 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19509 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19510 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19511
19512 let multi_buffer = cx.new(|cx| {
19513 let mut multibuffer = MultiBuffer::new(ReadWrite);
19514 multibuffer.push_excerpts(
19515 buffer_1.clone(),
19516 [
19517 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19518 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19519 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19520 ],
19521 cx,
19522 );
19523 multibuffer.push_excerpts(
19524 buffer_2.clone(),
19525 [
19526 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19527 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19528 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19529 ],
19530 cx,
19531 );
19532 multibuffer.push_excerpts(
19533 buffer_3.clone(),
19534 [
19535 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19536 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19537 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19538 ],
19539 cx,
19540 );
19541 multibuffer
19542 });
19543
19544 let editor =
19545 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19546 editor
19547 .update(cx, |editor, _window, cx| {
19548 for (buffer, diff_base) in [
19549 (buffer_1.clone(), file_1_old),
19550 (buffer_2.clone(), file_2_old),
19551 (buffer_3.clone(), file_3_old),
19552 ] {
19553 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19554 editor
19555 .buffer
19556 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19557 }
19558 })
19559 .unwrap();
19560
19561 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19562 cx.run_until_parked();
19563
19564 cx.assert_editor_state(
19565 &"
19566 ˇaaa
19567 ccc
19568 ddd
19569
19570 ggg
19571 hhh
19572
19573
19574 lll
19575 mmm
19576 NNN
19577
19578 qqq
19579 rrr
19580
19581 uuu
19582 111
19583 222
19584 333
19585
19586 666
19587 777
19588
19589 000
19590 !!!"
19591 .unindent(),
19592 );
19593
19594 cx.update_editor(|editor, window, cx| {
19595 editor.select_all(&SelectAll, window, cx);
19596 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19597 });
19598 cx.executor().run_until_parked();
19599
19600 cx.assert_state_with_diff(
19601 "
19602 «aaa
19603 - bbb
19604 ccc
19605 ddd
19606
19607 ggg
19608 hhh
19609
19610
19611 lll
19612 mmm
19613 - nnn
19614 + NNN
19615
19616 qqq
19617 rrr
19618
19619 uuu
19620 111
19621 222
19622 333
19623
19624 + 666
19625 777
19626
19627 000
19628 !!!ˇ»"
19629 .unindent(),
19630 );
19631}
19632
19633#[gpui::test]
19634async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19635 init_test(cx, |_| {});
19636
19637 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19638 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19639
19640 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19641 let multi_buffer = cx.new(|cx| {
19642 let mut multibuffer = MultiBuffer::new(ReadWrite);
19643 multibuffer.push_excerpts(
19644 buffer.clone(),
19645 [
19646 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19647 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19648 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19649 ],
19650 cx,
19651 );
19652 multibuffer
19653 });
19654
19655 let editor =
19656 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19657 editor
19658 .update(cx, |editor, _window, cx| {
19659 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19660 editor
19661 .buffer
19662 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19663 })
19664 .unwrap();
19665
19666 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19667 cx.run_until_parked();
19668
19669 cx.update_editor(|editor, window, cx| {
19670 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19671 });
19672 cx.executor().run_until_parked();
19673
19674 // When the start of a hunk coincides with the start of its excerpt,
19675 // the hunk is expanded. When the start of a hunk is earlier than
19676 // the start of its excerpt, the hunk is not expanded.
19677 cx.assert_state_with_diff(
19678 "
19679 ˇaaa
19680 - bbb
19681 + BBB
19682
19683 - ddd
19684 - eee
19685 + DDD
19686 + EEE
19687 fff
19688
19689 iii
19690 "
19691 .unindent(),
19692 );
19693}
19694
19695#[gpui::test]
19696async fn test_edits_around_expanded_insertion_hunks(
19697 executor: BackgroundExecutor,
19698 cx: &mut TestAppContext,
19699) {
19700 init_test(cx, |_| {});
19701
19702 let mut cx = EditorTestContext::new(cx).await;
19703
19704 let diff_base = r#"
19705 use some::mod1;
19706 use some::mod2;
19707
19708 const A: u32 = 42;
19709
19710 fn main() {
19711 println!("hello");
19712
19713 println!("world");
19714 }
19715 "#
19716 .unindent();
19717 executor.run_until_parked();
19718 cx.set_state(
19719 &r#"
19720 use some::mod1;
19721 use some::mod2;
19722
19723 const A: u32 = 42;
19724 const B: u32 = 42;
19725 const C: u32 = 42;
19726 ˇ
19727
19728 fn main() {
19729 println!("hello");
19730
19731 println!("world");
19732 }
19733 "#
19734 .unindent(),
19735 );
19736
19737 cx.set_head_text(&diff_base);
19738 executor.run_until_parked();
19739
19740 cx.update_editor(|editor, window, cx| {
19741 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19742 });
19743 executor.run_until_parked();
19744
19745 cx.assert_state_with_diff(
19746 r#"
19747 use some::mod1;
19748 use some::mod2;
19749
19750 const A: u32 = 42;
19751 + const B: u32 = 42;
19752 + const C: u32 = 42;
19753 + ˇ
19754
19755 fn main() {
19756 println!("hello");
19757
19758 println!("world");
19759 }
19760 "#
19761 .unindent(),
19762 );
19763
19764 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19765 executor.run_until_parked();
19766
19767 cx.assert_state_with_diff(
19768 r#"
19769 use some::mod1;
19770 use some::mod2;
19771
19772 const A: u32 = 42;
19773 + const B: u32 = 42;
19774 + const C: u32 = 42;
19775 + const D: u32 = 42;
19776 + ˇ
19777
19778 fn main() {
19779 println!("hello");
19780
19781 println!("world");
19782 }
19783 "#
19784 .unindent(),
19785 );
19786
19787 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19788 executor.run_until_parked();
19789
19790 cx.assert_state_with_diff(
19791 r#"
19792 use some::mod1;
19793 use some::mod2;
19794
19795 const A: u32 = 42;
19796 + const B: u32 = 42;
19797 + const C: u32 = 42;
19798 + const D: u32 = 42;
19799 + const E: u32 = 42;
19800 + ˇ
19801
19802 fn main() {
19803 println!("hello");
19804
19805 println!("world");
19806 }
19807 "#
19808 .unindent(),
19809 );
19810
19811 cx.update_editor(|editor, window, cx| {
19812 editor.delete_line(&DeleteLine, window, cx);
19813 });
19814 executor.run_until_parked();
19815
19816 cx.assert_state_with_diff(
19817 r#"
19818 use some::mod1;
19819 use some::mod2;
19820
19821 const A: u32 = 42;
19822 + const B: u32 = 42;
19823 + const C: u32 = 42;
19824 + const D: u32 = 42;
19825 + const E: u32 = 42;
19826 ˇ
19827 fn main() {
19828 println!("hello");
19829
19830 println!("world");
19831 }
19832 "#
19833 .unindent(),
19834 );
19835
19836 cx.update_editor(|editor, window, cx| {
19837 editor.move_up(&MoveUp, window, cx);
19838 editor.delete_line(&DeleteLine, window, cx);
19839 editor.move_up(&MoveUp, window, cx);
19840 editor.delete_line(&DeleteLine, window, cx);
19841 editor.move_up(&MoveUp, window, cx);
19842 editor.delete_line(&DeleteLine, window, cx);
19843 });
19844 executor.run_until_parked();
19845 cx.assert_state_with_diff(
19846 r#"
19847 use some::mod1;
19848 use some::mod2;
19849
19850 const A: u32 = 42;
19851 + const B: u32 = 42;
19852 ˇ
19853 fn main() {
19854 println!("hello");
19855
19856 println!("world");
19857 }
19858 "#
19859 .unindent(),
19860 );
19861
19862 cx.update_editor(|editor, window, cx| {
19863 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19864 editor.delete_line(&DeleteLine, window, cx);
19865 });
19866 executor.run_until_parked();
19867 cx.assert_state_with_diff(
19868 r#"
19869 ˇ
19870 fn main() {
19871 println!("hello");
19872
19873 println!("world");
19874 }
19875 "#
19876 .unindent(),
19877 );
19878}
19879
19880#[gpui::test]
19881async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19882 init_test(cx, |_| {});
19883
19884 let mut cx = EditorTestContext::new(cx).await;
19885 cx.set_head_text(indoc! { "
19886 one
19887 two
19888 three
19889 four
19890 five
19891 "
19892 });
19893 cx.set_state(indoc! { "
19894 one
19895 ˇthree
19896 five
19897 "});
19898 cx.run_until_parked();
19899 cx.update_editor(|editor, window, cx| {
19900 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19901 });
19902 cx.assert_state_with_diff(
19903 indoc! { "
19904 one
19905 - two
19906 ˇthree
19907 - four
19908 five
19909 "}
19910 .to_string(),
19911 );
19912 cx.update_editor(|editor, window, cx| {
19913 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19914 });
19915
19916 cx.assert_state_with_diff(
19917 indoc! { "
19918 one
19919 ˇthree
19920 five
19921 "}
19922 .to_string(),
19923 );
19924
19925 cx.set_state(indoc! { "
19926 one
19927 ˇTWO
19928 three
19929 four
19930 five
19931 "});
19932 cx.run_until_parked();
19933 cx.update_editor(|editor, window, cx| {
19934 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19935 });
19936
19937 cx.assert_state_with_diff(
19938 indoc! { "
19939 one
19940 - two
19941 + ˇTWO
19942 three
19943 four
19944 five
19945 "}
19946 .to_string(),
19947 );
19948 cx.update_editor(|editor, window, cx| {
19949 editor.move_up(&Default::default(), window, cx);
19950 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19951 });
19952 cx.assert_state_with_diff(
19953 indoc! { "
19954 one
19955 ˇTWO
19956 three
19957 four
19958 five
19959 "}
19960 .to_string(),
19961 );
19962}
19963
19964#[gpui::test]
19965async fn test_edits_around_expanded_deletion_hunks(
19966 executor: BackgroundExecutor,
19967 cx: &mut TestAppContext,
19968) {
19969 init_test(cx, |_| {});
19970
19971 let mut cx = EditorTestContext::new(cx).await;
19972
19973 let diff_base = r#"
19974 use some::mod1;
19975 use some::mod2;
19976
19977 const A: u32 = 42;
19978 const B: u32 = 42;
19979 const C: u32 = 42;
19980
19981
19982 fn main() {
19983 println!("hello");
19984
19985 println!("world");
19986 }
19987 "#
19988 .unindent();
19989 executor.run_until_parked();
19990 cx.set_state(
19991 &r#"
19992 use some::mod1;
19993 use some::mod2;
19994
19995 ˇconst B: u32 = 42;
19996 const C: u32 = 42;
19997
19998
19999 fn main() {
20000 println!("hello");
20001
20002 println!("world");
20003 }
20004 "#
20005 .unindent(),
20006 );
20007
20008 cx.set_head_text(&diff_base);
20009 executor.run_until_parked();
20010
20011 cx.update_editor(|editor, window, cx| {
20012 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20013 });
20014 executor.run_until_parked();
20015
20016 cx.assert_state_with_diff(
20017 r#"
20018 use some::mod1;
20019 use some::mod2;
20020
20021 - const A: u32 = 42;
20022 ˇconst B: u32 = 42;
20023 const C: u32 = 42;
20024
20025
20026 fn main() {
20027 println!("hello");
20028
20029 println!("world");
20030 }
20031 "#
20032 .unindent(),
20033 );
20034
20035 cx.update_editor(|editor, window, cx| {
20036 editor.delete_line(&DeleteLine, window, cx);
20037 });
20038 executor.run_until_parked();
20039 cx.assert_state_with_diff(
20040 r#"
20041 use some::mod1;
20042 use some::mod2;
20043
20044 - const A: u32 = 42;
20045 - const B: u32 = 42;
20046 ˇconst C: u32 = 42;
20047
20048
20049 fn main() {
20050 println!("hello");
20051
20052 println!("world");
20053 }
20054 "#
20055 .unindent(),
20056 );
20057
20058 cx.update_editor(|editor, window, cx| {
20059 editor.delete_line(&DeleteLine, window, cx);
20060 });
20061 executor.run_until_parked();
20062 cx.assert_state_with_diff(
20063 r#"
20064 use some::mod1;
20065 use some::mod2;
20066
20067 - const A: u32 = 42;
20068 - const B: u32 = 42;
20069 - const C: u32 = 42;
20070 ˇ
20071
20072 fn main() {
20073 println!("hello");
20074
20075 println!("world");
20076 }
20077 "#
20078 .unindent(),
20079 );
20080
20081 cx.update_editor(|editor, window, cx| {
20082 editor.handle_input("replacement", window, cx);
20083 });
20084 executor.run_until_parked();
20085 cx.assert_state_with_diff(
20086 r#"
20087 use some::mod1;
20088 use some::mod2;
20089
20090 - const A: u32 = 42;
20091 - const B: u32 = 42;
20092 - const C: u32 = 42;
20093 -
20094 + replacementˇ
20095
20096 fn main() {
20097 println!("hello");
20098
20099 println!("world");
20100 }
20101 "#
20102 .unindent(),
20103 );
20104}
20105
20106#[gpui::test]
20107async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20108 init_test(cx, |_| {});
20109
20110 let mut cx = EditorTestContext::new(cx).await;
20111
20112 let base_text = r#"
20113 one
20114 two
20115 three
20116 four
20117 five
20118 "#
20119 .unindent();
20120 executor.run_until_parked();
20121 cx.set_state(
20122 &r#"
20123 one
20124 two
20125 fˇour
20126 five
20127 "#
20128 .unindent(),
20129 );
20130
20131 cx.set_head_text(&base_text);
20132 executor.run_until_parked();
20133
20134 cx.update_editor(|editor, window, cx| {
20135 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20136 });
20137 executor.run_until_parked();
20138
20139 cx.assert_state_with_diff(
20140 r#"
20141 one
20142 two
20143 - three
20144 fˇour
20145 five
20146 "#
20147 .unindent(),
20148 );
20149
20150 cx.update_editor(|editor, window, cx| {
20151 editor.backspace(&Backspace, window, cx);
20152 editor.backspace(&Backspace, window, cx);
20153 });
20154 executor.run_until_parked();
20155 cx.assert_state_with_diff(
20156 r#"
20157 one
20158 two
20159 - threeˇ
20160 - four
20161 + our
20162 five
20163 "#
20164 .unindent(),
20165 );
20166}
20167
20168#[gpui::test]
20169async fn test_edit_after_expanded_modification_hunk(
20170 executor: BackgroundExecutor,
20171 cx: &mut TestAppContext,
20172) {
20173 init_test(cx, |_| {});
20174
20175 let mut cx = EditorTestContext::new(cx).await;
20176
20177 let diff_base = r#"
20178 use some::mod1;
20179 use some::mod2;
20180
20181 const A: u32 = 42;
20182 const B: u32 = 42;
20183 const C: u32 = 42;
20184 const D: u32 = 42;
20185
20186
20187 fn main() {
20188 println!("hello");
20189
20190 println!("world");
20191 }"#
20192 .unindent();
20193
20194 cx.set_state(
20195 &r#"
20196 use some::mod1;
20197 use some::mod2;
20198
20199 const A: u32 = 42;
20200 const B: u32 = 42;
20201 const C: u32 = 43ˇ
20202 const D: u32 = 42;
20203
20204
20205 fn main() {
20206 println!("hello");
20207
20208 println!("world");
20209 }"#
20210 .unindent(),
20211 );
20212
20213 cx.set_head_text(&diff_base);
20214 executor.run_until_parked();
20215 cx.update_editor(|editor, window, cx| {
20216 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20217 });
20218 executor.run_until_parked();
20219
20220 cx.assert_state_with_diff(
20221 r#"
20222 use some::mod1;
20223 use some::mod2;
20224
20225 const A: u32 = 42;
20226 const B: u32 = 42;
20227 - const C: u32 = 42;
20228 + const C: u32 = 43ˇ
20229 const D: u32 = 42;
20230
20231
20232 fn main() {
20233 println!("hello");
20234
20235 println!("world");
20236 }"#
20237 .unindent(),
20238 );
20239
20240 cx.update_editor(|editor, window, cx| {
20241 editor.handle_input("\nnew_line\n", window, cx);
20242 });
20243 executor.run_until_parked();
20244
20245 cx.assert_state_with_diff(
20246 r#"
20247 use some::mod1;
20248 use some::mod2;
20249
20250 const A: u32 = 42;
20251 const B: u32 = 42;
20252 - const C: u32 = 42;
20253 + const C: u32 = 43
20254 + new_line
20255 + ˇ
20256 const D: u32 = 42;
20257
20258
20259 fn main() {
20260 println!("hello");
20261
20262 println!("world");
20263 }"#
20264 .unindent(),
20265 );
20266}
20267
20268#[gpui::test]
20269async fn test_stage_and_unstage_added_file_hunk(
20270 executor: BackgroundExecutor,
20271 cx: &mut TestAppContext,
20272) {
20273 init_test(cx, |_| {});
20274
20275 let mut cx = EditorTestContext::new(cx).await;
20276 cx.update_editor(|editor, _, cx| {
20277 editor.set_expand_all_diff_hunks(cx);
20278 });
20279
20280 let working_copy = r#"
20281 ˇfn main() {
20282 println!("hello, world!");
20283 }
20284 "#
20285 .unindent();
20286
20287 cx.set_state(&working_copy);
20288 executor.run_until_parked();
20289
20290 cx.assert_state_with_diff(
20291 r#"
20292 + ˇfn main() {
20293 + println!("hello, world!");
20294 + }
20295 "#
20296 .unindent(),
20297 );
20298 cx.assert_index_text(None);
20299
20300 cx.update_editor(|editor, window, cx| {
20301 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20302 });
20303 executor.run_until_parked();
20304 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20305 cx.assert_state_with_diff(
20306 r#"
20307 + ˇfn main() {
20308 + println!("hello, world!");
20309 + }
20310 "#
20311 .unindent(),
20312 );
20313
20314 cx.update_editor(|editor, window, cx| {
20315 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20316 });
20317 executor.run_until_parked();
20318 cx.assert_index_text(None);
20319}
20320
20321async fn setup_indent_guides_editor(
20322 text: &str,
20323 cx: &mut TestAppContext,
20324) -> (BufferId, EditorTestContext) {
20325 init_test(cx, |_| {});
20326
20327 let mut cx = EditorTestContext::new(cx).await;
20328
20329 let buffer_id = cx.update_editor(|editor, window, cx| {
20330 editor.set_text(text, window, cx);
20331 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20332
20333 buffer_ids[0]
20334 });
20335
20336 (buffer_id, cx)
20337}
20338
20339fn assert_indent_guides(
20340 range: Range<u32>,
20341 expected: Vec<IndentGuide>,
20342 active_indices: Option<Vec<usize>>,
20343 cx: &mut EditorTestContext,
20344) {
20345 let indent_guides = cx.update_editor(|editor, window, cx| {
20346 let snapshot = editor.snapshot(window, cx).display_snapshot;
20347 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20348 editor,
20349 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20350 true,
20351 &snapshot,
20352 cx,
20353 );
20354
20355 indent_guides.sort_by(|a, b| {
20356 a.depth.cmp(&b.depth).then(
20357 a.start_row
20358 .cmp(&b.start_row)
20359 .then(a.end_row.cmp(&b.end_row)),
20360 )
20361 });
20362 indent_guides
20363 });
20364
20365 if let Some(expected) = active_indices {
20366 let active_indices = cx.update_editor(|editor, window, cx| {
20367 let snapshot = editor.snapshot(window, cx).display_snapshot;
20368 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20369 });
20370
20371 assert_eq!(
20372 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20373 expected,
20374 "Active indent guide indices do not match"
20375 );
20376 }
20377
20378 assert_eq!(indent_guides, expected, "Indent guides do not match");
20379}
20380
20381fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20382 IndentGuide {
20383 buffer_id,
20384 start_row: MultiBufferRow(start_row),
20385 end_row: MultiBufferRow(end_row),
20386 depth,
20387 tab_size: 4,
20388 settings: IndentGuideSettings {
20389 enabled: true,
20390 line_width: 1,
20391 active_line_width: 1,
20392 coloring: IndentGuideColoring::default(),
20393 background_coloring: IndentGuideBackgroundColoring::default(),
20394 },
20395 }
20396}
20397
20398#[gpui::test]
20399async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20400 let (buffer_id, mut cx) = setup_indent_guides_editor(
20401 &"
20402 fn main() {
20403 let a = 1;
20404 }"
20405 .unindent(),
20406 cx,
20407 )
20408 .await;
20409
20410 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20411}
20412
20413#[gpui::test]
20414async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20415 let (buffer_id, mut cx) = setup_indent_guides_editor(
20416 &"
20417 fn main() {
20418 let a = 1;
20419 let b = 2;
20420 }"
20421 .unindent(),
20422 cx,
20423 )
20424 .await;
20425
20426 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20427}
20428
20429#[gpui::test]
20430async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20431 let (buffer_id, mut cx) = setup_indent_guides_editor(
20432 &"
20433 fn main() {
20434 let a = 1;
20435 if a == 3 {
20436 let b = 2;
20437 } else {
20438 let c = 3;
20439 }
20440 }"
20441 .unindent(),
20442 cx,
20443 )
20444 .await;
20445
20446 assert_indent_guides(
20447 0..8,
20448 vec![
20449 indent_guide(buffer_id, 1, 6, 0),
20450 indent_guide(buffer_id, 3, 3, 1),
20451 indent_guide(buffer_id, 5, 5, 1),
20452 ],
20453 None,
20454 &mut cx,
20455 );
20456}
20457
20458#[gpui::test]
20459async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20460 let (buffer_id, mut cx) = setup_indent_guides_editor(
20461 &"
20462 fn main() {
20463 let a = 1;
20464 let b = 2;
20465 let c = 3;
20466 }"
20467 .unindent(),
20468 cx,
20469 )
20470 .await;
20471
20472 assert_indent_guides(
20473 0..5,
20474 vec![
20475 indent_guide(buffer_id, 1, 3, 0),
20476 indent_guide(buffer_id, 2, 2, 1),
20477 ],
20478 None,
20479 &mut cx,
20480 );
20481}
20482
20483#[gpui::test]
20484async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20485 let (buffer_id, mut cx) = setup_indent_guides_editor(
20486 &"
20487 fn main() {
20488 let a = 1;
20489
20490 let c = 3;
20491 }"
20492 .unindent(),
20493 cx,
20494 )
20495 .await;
20496
20497 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20498}
20499
20500#[gpui::test]
20501async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20502 let (buffer_id, mut cx) = setup_indent_guides_editor(
20503 &"
20504 fn main() {
20505 let a = 1;
20506
20507 let c = 3;
20508
20509 if a == 3 {
20510 let b = 2;
20511 } else {
20512 let c = 3;
20513 }
20514 }"
20515 .unindent(),
20516 cx,
20517 )
20518 .await;
20519
20520 assert_indent_guides(
20521 0..11,
20522 vec![
20523 indent_guide(buffer_id, 1, 9, 0),
20524 indent_guide(buffer_id, 6, 6, 1),
20525 indent_guide(buffer_id, 8, 8, 1),
20526 ],
20527 None,
20528 &mut cx,
20529 );
20530}
20531
20532#[gpui::test]
20533async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20534 let (buffer_id, mut cx) = setup_indent_guides_editor(
20535 &"
20536 fn main() {
20537 let a = 1;
20538
20539 let c = 3;
20540
20541 if a == 3 {
20542 let b = 2;
20543 } else {
20544 let c = 3;
20545 }
20546 }"
20547 .unindent(),
20548 cx,
20549 )
20550 .await;
20551
20552 assert_indent_guides(
20553 1..11,
20554 vec![
20555 indent_guide(buffer_id, 1, 9, 0),
20556 indent_guide(buffer_id, 6, 6, 1),
20557 indent_guide(buffer_id, 8, 8, 1),
20558 ],
20559 None,
20560 &mut cx,
20561 );
20562}
20563
20564#[gpui::test]
20565async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20566 let (buffer_id, mut cx) = setup_indent_guides_editor(
20567 &"
20568 fn main() {
20569 let a = 1;
20570
20571 let c = 3;
20572
20573 if a == 3 {
20574 let b = 2;
20575 } else {
20576 let c = 3;
20577 }
20578 }"
20579 .unindent(),
20580 cx,
20581 )
20582 .await;
20583
20584 assert_indent_guides(
20585 1..10,
20586 vec![
20587 indent_guide(buffer_id, 1, 9, 0),
20588 indent_guide(buffer_id, 6, 6, 1),
20589 indent_guide(buffer_id, 8, 8, 1),
20590 ],
20591 None,
20592 &mut cx,
20593 );
20594}
20595
20596#[gpui::test]
20597async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20598 let (buffer_id, mut cx) = setup_indent_guides_editor(
20599 &"
20600 fn main() {
20601 if a {
20602 b(
20603 c,
20604 d,
20605 )
20606 } else {
20607 e(
20608 f
20609 )
20610 }
20611 }"
20612 .unindent(),
20613 cx,
20614 )
20615 .await;
20616
20617 assert_indent_guides(
20618 0..11,
20619 vec![
20620 indent_guide(buffer_id, 1, 10, 0),
20621 indent_guide(buffer_id, 2, 5, 1),
20622 indent_guide(buffer_id, 7, 9, 1),
20623 indent_guide(buffer_id, 3, 4, 2),
20624 indent_guide(buffer_id, 8, 8, 2),
20625 ],
20626 None,
20627 &mut cx,
20628 );
20629
20630 cx.update_editor(|editor, window, cx| {
20631 editor.fold_at(MultiBufferRow(2), window, cx);
20632 assert_eq!(
20633 editor.display_text(cx),
20634 "
20635 fn main() {
20636 if a {
20637 b(⋯
20638 )
20639 } else {
20640 e(
20641 f
20642 )
20643 }
20644 }"
20645 .unindent()
20646 );
20647 });
20648
20649 assert_indent_guides(
20650 0..11,
20651 vec![
20652 indent_guide(buffer_id, 1, 10, 0),
20653 indent_guide(buffer_id, 2, 5, 1),
20654 indent_guide(buffer_id, 7, 9, 1),
20655 indent_guide(buffer_id, 8, 8, 2),
20656 ],
20657 None,
20658 &mut cx,
20659 );
20660}
20661
20662#[gpui::test]
20663async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20664 let (buffer_id, mut cx) = setup_indent_guides_editor(
20665 &"
20666 block1
20667 block2
20668 block3
20669 block4
20670 block2
20671 block1
20672 block1"
20673 .unindent(),
20674 cx,
20675 )
20676 .await;
20677
20678 assert_indent_guides(
20679 1..10,
20680 vec![
20681 indent_guide(buffer_id, 1, 4, 0),
20682 indent_guide(buffer_id, 2, 3, 1),
20683 indent_guide(buffer_id, 3, 3, 2),
20684 ],
20685 None,
20686 &mut cx,
20687 );
20688}
20689
20690#[gpui::test]
20691async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20692 let (buffer_id, mut cx) = setup_indent_guides_editor(
20693 &"
20694 block1
20695 block2
20696 block3
20697
20698 block1
20699 block1"
20700 .unindent(),
20701 cx,
20702 )
20703 .await;
20704
20705 assert_indent_guides(
20706 0..6,
20707 vec![
20708 indent_guide(buffer_id, 1, 2, 0),
20709 indent_guide(buffer_id, 2, 2, 1),
20710 ],
20711 None,
20712 &mut cx,
20713 );
20714}
20715
20716#[gpui::test]
20717async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20718 let (buffer_id, mut cx) = setup_indent_guides_editor(
20719 &"
20720 function component() {
20721 \treturn (
20722 \t\t\t
20723 \t\t<div>
20724 \t\t\t<abc></abc>
20725 \t\t</div>
20726 \t)
20727 }"
20728 .unindent(),
20729 cx,
20730 )
20731 .await;
20732
20733 assert_indent_guides(
20734 0..8,
20735 vec![
20736 indent_guide(buffer_id, 1, 6, 0),
20737 indent_guide(buffer_id, 2, 5, 1),
20738 indent_guide(buffer_id, 4, 4, 2),
20739 ],
20740 None,
20741 &mut cx,
20742 );
20743}
20744
20745#[gpui::test]
20746async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20747 let (buffer_id, mut cx) = setup_indent_guides_editor(
20748 &"
20749 function component() {
20750 \treturn (
20751 \t
20752 \t\t<div>
20753 \t\t\t<abc></abc>
20754 \t\t</div>
20755 \t)
20756 }"
20757 .unindent(),
20758 cx,
20759 )
20760 .await;
20761
20762 assert_indent_guides(
20763 0..8,
20764 vec![
20765 indent_guide(buffer_id, 1, 6, 0),
20766 indent_guide(buffer_id, 2, 5, 1),
20767 indent_guide(buffer_id, 4, 4, 2),
20768 ],
20769 None,
20770 &mut cx,
20771 );
20772}
20773
20774#[gpui::test]
20775async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20776 let (buffer_id, mut cx) = setup_indent_guides_editor(
20777 &"
20778 block1
20779
20780
20781
20782 block2
20783 "
20784 .unindent(),
20785 cx,
20786 )
20787 .await;
20788
20789 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20790}
20791
20792#[gpui::test]
20793async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20794 let (buffer_id, mut cx) = setup_indent_guides_editor(
20795 &"
20796 def a:
20797 \tb = 3
20798 \tif True:
20799 \t\tc = 4
20800 \t\td = 5
20801 \tprint(b)
20802 "
20803 .unindent(),
20804 cx,
20805 )
20806 .await;
20807
20808 assert_indent_guides(
20809 0..6,
20810 vec![
20811 indent_guide(buffer_id, 1, 5, 0),
20812 indent_guide(buffer_id, 3, 4, 1),
20813 ],
20814 None,
20815 &mut cx,
20816 );
20817}
20818
20819#[gpui::test]
20820async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20821 let (buffer_id, mut cx) = setup_indent_guides_editor(
20822 &"
20823 fn main() {
20824 let a = 1;
20825 }"
20826 .unindent(),
20827 cx,
20828 )
20829 .await;
20830
20831 cx.update_editor(|editor, window, cx| {
20832 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20833 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20834 });
20835 });
20836
20837 assert_indent_guides(
20838 0..3,
20839 vec![indent_guide(buffer_id, 1, 1, 0)],
20840 Some(vec![0]),
20841 &mut cx,
20842 );
20843}
20844
20845#[gpui::test]
20846async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20847 let (buffer_id, mut cx) = setup_indent_guides_editor(
20848 &"
20849 fn main() {
20850 if 1 == 2 {
20851 let a = 1;
20852 }
20853 }"
20854 .unindent(),
20855 cx,
20856 )
20857 .await;
20858
20859 cx.update_editor(|editor, window, cx| {
20860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20861 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20862 });
20863 });
20864
20865 assert_indent_guides(
20866 0..4,
20867 vec![
20868 indent_guide(buffer_id, 1, 3, 0),
20869 indent_guide(buffer_id, 2, 2, 1),
20870 ],
20871 Some(vec![1]),
20872 &mut cx,
20873 );
20874
20875 cx.update_editor(|editor, window, cx| {
20876 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20877 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20878 });
20879 });
20880
20881 assert_indent_guides(
20882 0..4,
20883 vec![
20884 indent_guide(buffer_id, 1, 3, 0),
20885 indent_guide(buffer_id, 2, 2, 1),
20886 ],
20887 Some(vec![1]),
20888 &mut cx,
20889 );
20890
20891 cx.update_editor(|editor, window, cx| {
20892 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20893 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20894 });
20895 });
20896
20897 assert_indent_guides(
20898 0..4,
20899 vec![
20900 indent_guide(buffer_id, 1, 3, 0),
20901 indent_guide(buffer_id, 2, 2, 1),
20902 ],
20903 Some(vec![0]),
20904 &mut cx,
20905 );
20906}
20907
20908#[gpui::test]
20909async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20910 let (buffer_id, mut cx) = setup_indent_guides_editor(
20911 &"
20912 fn main() {
20913 let a = 1;
20914
20915 let b = 2;
20916 }"
20917 .unindent(),
20918 cx,
20919 )
20920 .await;
20921
20922 cx.update_editor(|editor, window, cx| {
20923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20924 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20925 });
20926 });
20927
20928 assert_indent_guides(
20929 0..5,
20930 vec![indent_guide(buffer_id, 1, 3, 0)],
20931 Some(vec![0]),
20932 &mut cx,
20933 );
20934}
20935
20936#[gpui::test]
20937async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20938 let (buffer_id, mut cx) = setup_indent_guides_editor(
20939 &"
20940 def m:
20941 a = 1
20942 pass"
20943 .unindent(),
20944 cx,
20945 )
20946 .await;
20947
20948 cx.update_editor(|editor, window, cx| {
20949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20950 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20951 });
20952 });
20953
20954 assert_indent_guides(
20955 0..3,
20956 vec![indent_guide(buffer_id, 1, 2, 0)],
20957 Some(vec![0]),
20958 &mut cx,
20959 );
20960}
20961
20962#[gpui::test]
20963async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20964 init_test(cx, |_| {});
20965 let mut cx = EditorTestContext::new(cx).await;
20966 let text = indoc! {
20967 "
20968 impl A {
20969 fn b() {
20970 0;
20971 3;
20972 5;
20973 6;
20974 7;
20975 }
20976 }
20977 "
20978 };
20979 let base_text = indoc! {
20980 "
20981 impl A {
20982 fn b() {
20983 0;
20984 1;
20985 2;
20986 3;
20987 4;
20988 }
20989 fn c() {
20990 5;
20991 6;
20992 7;
20993 }
20994 }
20995 "
20996 };
20997
20998 cx.update_editor(|editor, window, cx| {
20999 editor.set_text(text, window, cx);
21000
21001 editor.buffer().update(cx, |multibuffer, cx| {
21002 let buffer = multibuffer.as_singleton().unwrap();
21003 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21004
21005 multibuffer.set_all_diff_hunks_expanded(cx);
21006 multibuffer.add_diff(diff, cx);
21007
21008 buffer.read(cx).remote_id()
21009 })
21010 });
21011 cx.run_until_parked();
21012
21013 cx.assert_state_with_diff(
21014 indoc! { "
21015 impl A {
21016 fn b() {
21017 0;
21018 - 1;
21019 - 2;
21020 3;
21021 - 4;
21022 - }
21023 - fn c() {
21024 5;
21025 6;
21026 7;
21027 }
21028 }
21029 ˇ"
21030 }
21031 .to_string(),
21032 );
21033
21034 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21035 editor
21036 .snapshot(window, cx)
21037 .buffer_snapshot()
21038 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21039 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21040 .collect::<Vec<_>>()
21041 });
21042 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21043 assert_eq!(
21044 actual_guides,
21045 vec![
21046 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21047 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21048 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21049 ]
21050 );
21051}
21052
21053#[gpui::test]
21054async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21055 init_test(cx, |_| {});
21056 let mut cx = EditorTestContext::new(cx).await;
21057
21058 let diff_base = r#"
21059 a
21060 b
21061 c
21062 "#
21063 .unindent();
21064
21065 cx.set_state(
21066 &r#"
21067 ˇA
21068 b
21069 C
21070 "#
21071 .unindent(),
21072 );
21073 cx.set_head_text(&diff_base);
21074 cx.update_editor(|editor, window, cx| {
21075 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21076 });
21077 executor.run_until_parked();
21078
21079 let both_hunks_expanded = r#"
21080 - a
21081 + ˇA
21082 b
21083 - c
21084 + C
21085 "#
21086 .unindent();
21087
21088 cx.assert_state_with_diff(both_hunks_expanded.clone());
21089
21090 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21091 let snapshot = editor.snapshot(window, cx);
21092 let hunks = editor
21093 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21094 .collect::<Vec<_>>();
21095 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21096 let buffer_id = hunks[0].buffer_id;
21097 hunks
21098 .into_iter()
21099 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21100 .collect::<Vec<_>>()
21101 });
21102 assert_eq!(hunk_ranges.len(), 2);
21103
21104 cx.update_editor(|editor, _, cx| {
21105 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21106 });
21107 executor.run_until_parked();
21108
21109 let second_hunk_expanded = r#"
21110 ˇA
21111 b
21112 - c
21113 + C
21114 "#
21115 .unindent();
21116
21117 cx.assert_state_with_diff(second_hunk_expanded);
21118
21119 cx.update_editor(|editor, _, cx| {
21120 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21121 });
21122 executor.run_until_parked();
21123
21124 cx.assert_state_with_diff(both_hunks_expanded.clone());
21125
21126 cx.update_editor(|editor, _, cx| {
21127 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21128 });
21129 executor.run_until_parked();
21130
21131 let first_hunk_expanded = r#"
21132 - a
21133 + ˇA
21134 b
21135 C
21136 "#
21137 .unindent();
21138
21139 cx.assert_state_with_diff(first_hunk_expanded);
21140
21141 cx.update_editor(|editor, _, cx| {
21142 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21143 });
21144 executor.run_until_parked();
21145
21146 cx.assert_state_with_diff(both_hunks_expanded);
21147
21148 cx.set_state(
21149 &r#"
21150 ˇA
21151 b
21152 "#
21153 .unindent(),
21154 );
21155 cx.run_until_parked();
21156
21157 // TODO this cursor position seems bad
21158 cx.assert_state_with_diff(
21159 r#"
21160 - ˇa
21161 + A
21162 b
21163 "#
21164 .unindent(),
21165 );
21166
21167 cx.update_editor(|editor, window, cx| {
21168 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21169 });
21170
21171 cx.assert_state_with_diff(
21172 r#"
21173 - ˇa
21174 + A
21175 b
21176 - c
21177 "#
21178 .unindent(),
21179 );
21180
21181 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21182 let snapshot = editor.snapshot(window, cx);
21183 let hunks = editor
21184 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21185 .collect::<Vec<_>>();
21186 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21187 let buffer_id = hunks[0].buffer_id;
21188 hunks
21189 .into_iter()
21190 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21191 .collect::<Vec<_>>()
21192 });
21193 assert_eq!(hunk_ranges.len(), 2);
21194
21195 cx.update_editor(|editor, _, cx| {
21196 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21197 });
21198 executor.run_until_parked();
21199
21200 cx.assert_state_with_diff(
21201 r#"
21202 - ˇa
21203 + A
21204 b
21205 "#
21206 .unindent(),
21207 );
21208}
21209
21210#[gpui::test]
21211async fn test_toggle_deletion_hunk_at_start_of_file(
21212 executor: BackgroundExecutor,
21213 cx: &mut TestAppContext,
21214) {
21215 init_test(cx, |_| {});
21216 let mut cx = EditorTestContext::new(cx).await;
21217
21218 let diff_base = r#"
21219 a
21220 b
21221 c
21222 "#
21223 .unindent();
21224
21225 cx.set_state(
21226 &r#"
21227 ˇb
21228 c
21229 "#
21230 .unindent(),
21231 );
21232 cx.set_head_text(&diff_base);
21233 cx.update_editor(|editor, window, cx| {
21234 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21235 });
21236 executor.run_until_parked();
21237
21238 let hunk_expanded = r#"
21239 - a
21240 ˇb
21241 c
21242 "#
21243 .unindent();
21244
21245 cx.assert_state_with_diff(hunk_expanded.clone());
21246
21247 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21248 let snapshot = editor.snapshot(window, cx);
21249 let hunks = editor
21250 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21251 .collect::<Vec<_>>();
21252 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21253 let buffer_id = hunks[0].buffer_id;
21254 hunks
21255 .into_iter()
21256 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21257 .collect::<Vec<_>>()
21258 });
21259 assert_eq!(hunk_ranges.len(), 1);
21260
21261 cx.update_editor(|editor, _, cx| {
21262 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21263 });
21264 executor.run_until_parked();
21265
21266 let hunk_collapsed = r#"
21267 ˇb
21268 c
21269 "#
21270 .unindent();
21271
21272 cx.assert_state_with_diff(hunk_collapsed);
21273
21274 cx.update_editor(|editor, _, cx| {
21275 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21276 });
21277 executor.run_until_parked();
21278
21279 cx.assert_state_with_diff(hunk_expanded);
21280}
21281
21282#[gpui::test]
21283async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21284 init_test(cx, |_| {});
21285
21286 let fs = FakeFs::new(cx.executor());
21287 fs.insert_tree(
21288 path!("/test"),
21289 json!({
21290 ".git": {},
21291 "file-1": "ONE\n",
21292 "file-2": "TWO\n",
21293 "file-3": "THREE\n",
21294 }),
21295 )
21296 .await;
21297
21298 fs.set_head_for_repo(
21299 path!("/test/.git").as_ref(),
21300 &[
21301 ("file-1", "one\n".into()),
21302 ("file-2", "two\n".into()),
21303 ("file-3", "three\n".into()),
21304 ],
21305 "deadbeef",
21306 );
21307
21308 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21309 let mut buffers = vec![];
21310 for i in 1..=3 {
21311 let buffer = project
21312 .update(cx, |project, cx| {
21313 let path = format!(path!("/test/file-{}"), i);
21314 project.open_local_buffer(path, cx)
21315 })
21316 .await
21317 .unwrap();
21318 buffers.push(buffer);
21319 }
21320
21321 let multibuffer = cx.new(|cx| {
21322 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21323 multibuffer.set_all_diff_hunks_expanded(cx);
21324 for buffer in &buffers {
21325 let snapshot = buffer.read(cx).snapshot();
21326 multibuffer.set_excerpts_for_path(
21327 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21328 buffer.clone(),
21329 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21330 2,
21331 cx,
21332 );
21333 }
21334 multibuffer
21335 });
21336
21337 let editor = cx.add_window(|window, cx| {
21338 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21339 });
21340 cx.run_until_parked();
21341
21342 let snapshot = editor
21343 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21344 .unwrap();
21345 let hunks = snapshot
21346 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21347 .map(|hunk| match hunk {
21348 DisplayDiffHunk::Unfolded {
21349 display_row_range, ..
21350 } => display_row_range,
21351 DisplayDiffHunk::Folded { .. } => unreachable!(),
21352 })
21353 .collect::<Vec<_>>();
21354 assert_eq!(
21355 hunks,
21356 [
21357 DisplayRow(2)..DisplayRow(4),
21358 DisplayRow(7)..DisplayRow(9),
21359 DisplayRow(12)..DisplayRow(14),
21360 ]
21361 );
21362}
21363
21364#[gpui::test]
21365async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21366 init_test(cx, |_| {});
21367
21368 let mut cx = EditorTestContext::new(cx).await;
21369 cx.set_head_text(indoc! { "
21370 one
21371 two
21372 three
21373 four
21374 five
21375 "
21376 });
21377 cx.set_index_text(indoc! { "
21378 one
21379 two
21380 three
21381 four
21382 five
21383 "
21384 });
21385 cx.set_state(indoc! {"
21386 one
21387 TWO
21388 ˇTHREE
21389 FOUR
21390 five
21391 "});
21392 cx.run_until_parked();
21393 cx.update_editor(|editor, window, cx| {
21394 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21395 });
21396 cx.run_until_parked();
21397 cx.assert_index_text(Some(indoc! {"
21398 one
21399 TWO
21400 THREE
21401 FOUR
21402 five
21403 "}));
21404 cx.set_state(indoc! { "
21405 one
21406 TWO
21407 ˇTHREE-HUNDRED
21408 FOUR
21409 five
21410 "});
21411 cx.run_until_parked();
21412 cx.update_editor(|editor, window, cx| {
21413 let snapshot = editor.snapshot(window, cx);
21414 let hunks = editor
21415 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21416 .collect::<Vec<_>>();
21417 assert_eq!(hunks.len(), 1);
21418 assert_eq!(
21419 hunks[0].status(),
21420 DiffHunkStatus {
21421 kind: DiffHunkStatusKind::Modified,
21422 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21423 }
21424 );
21425
21426 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21427 });
21428 cx.run_until_parked();
21429 cx.assert_index_text(Some(indoc! {"
21430 one
21431 TWO
21432 THREE-HUNDRED
21433 FOUR
21434 five
21435 "}));
21436}
21437
21438#[gpui::test]
21439fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21440 init_test(cx, |_| {});
21441
21442 let editor = cx.add_window(|window, cx| {
21443 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21444 build_editor(buffer, window, cx)
21445 });
21446
21447 let render_args = Arc::new(Mutex::new(None));
21448 let snapshot = editor
21449 .update(cx, |editor, window, cx| {
21450 let snapshot = editor.buffer().read(cx).snapshot(cx);
21451 let range =
21452 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21453
21454 struct RenderArgs {
21455 row: MultiBufferRow,
21456 folded: bool,
21457 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21458 }
21459
21460 let crease = Crease::inline(
21461 range,
21462 FoldPlaceholder::test(),
21463 {
21464 let toggle_callback = render_args.clone();
21465 move |row, folded, callback, _window, _cx| {
21466 *toggle_callback.lock() = Some(RenderArgs {
21467 row,
21468 folded,
21469 callback,
21470 });
21471 div()
21472 }
21473 },
21474 |_row, _folded, _window, _cx| div(),
21475 );
21476
21477 editor.insert_creases(Some(crease), cx);
21478 let snapshot = editor.snapshot(window, cx);
21479 let _div =
21480 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21481 snapshot
21482 })
21483 .unwrap();
21484
21485 let render_args = render_args.lock().take().unwrap();
21486 assert_eq!(render_args.row, MultiBufferRow(1));
21487 assert!(!render_args.folded);
21488 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21489
21490 cx.update_window(*editor, |_, window, cx| {
21491 (render_args.callback)(true, window, cx)
21492 })
21493 .unwrap();
21494 let snapshot = editor
21495 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21496 .unwrap();
21497 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21498
21499 cx.update_window(*editor, |_, window, cx| {
21500 (render_args.callback)(false, window, cx)
21501 })
21502 .unwrap();
21503 let snapshot = editor
21504 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21505 .unwrap();
21506 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21507}
21508
21509#[gpui::test]
21510async fn test_input_text(cx: &mut TestAppContext) {
21511 init_test(cx, |_| {});
21512 let mut cx = EditorTestContext::new(cx).await;
21513
21514 cx.set_state(
21515 &r#"ˇone
21516 two
21517
21518 three
21519 fourˇ
21520 five
21521
21522 siˇx"#
21523 .unindent(),
21524 );
21525
21526 cx.dispatch_action(HandleInput(String::new()));
21527 cx.assert_editor_state(
21528 &r#"ˇone
21529 two
21530
21531 three
21532 fourˇ
21533 five
21534
21535 siˇx"#
21536 .unindent(),
21537 );
21538
21539 cx.dispatch_action(HandleInput("AAAA".to_string()));
21540 cx.assert_editor_state(
21541 &r#"AAAAˇone
21542 two
21543
21544 three
21545 fourAAAAˇ
21546 five
21547
21548 siAAAAˇx"#
21549 .unindent(),
21550 );
21551}
21552
21553#[gpui::test]
21554async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21555 init_test(cx, |_| {});
21556
21557 let mut cx = EditorTestContext::new(cx).await;
21558 cx.set_state(
21559 r#"let foo = 1;
21560let foo = 2;
21561let foo = 3;
21562let fooˇ = 4;
21563let foo = 5;
21564let foo = 6;
21565let foo = 7;
21566let foo = 8;
21567let foo = 9;
21568let foo = 10;
21569let foo = 11;
21570let foo = 12;
21571let foo = 13;
21572let foo = 14;
21573let foo = 15;"#,
21574 );
21575
21576 cx.update_editor(|e, window, cx| {
21577 assert_eq!(
21578 e.next_scroll_position,
21579 NextScrollCursorCenterTopBottom::Center,
21580 "Default next scroll direction is center",
21581 );
21582
21583 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21584 assert_eq!(
21585 e.next_scroll_position,
21586 NextScrollCursorCenterTopBottom::Top,
21587 "After center, next scroll direction should be top",
21588 );
21589
21590 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21591 assert_eq!(
21592 e.next_scroll_position,
21593 NextScrollCursorCenterTopBottom::Bottom,
21594 "After top, next scroll direction should be bottom",
21595 );
21596
21597 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21598 assert_eq!(
21599 e.next_scroll_position,
21600 NextScrollCursorCenterTopBottom::Center,
21601 "After bottom, scrolling should start over",
21602 );
21603
21604 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21605 assert_eq!(
21606 e.next_scroll_position,
21607 NextScrollCursorCenterTopBottom::Top,
21608 "Scrolling continues if retriggered fast enough"
21609 );
21610 });
21611
21612 cx.executor()
21613 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21614 cx.executor().run_until_parked();
21615 cx.update_editor(|e, _, _| {
21616 assert_eq!(
21617 e.next_scroll_position,
21618 NextScrollCursorCenterTopBottom::Center,
21619 "If scrolling is not triggered fast enough, it should reset"
21620 );
21621 });
21622}
21623
21624#[gpui::test]
21625async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21626 init_test(cx, |_| {});
21627 let mut cx = EditorLspTestContext::new_rust(
21628 lsp::ServerCapabilities {
21629 definition_provider: Some(lsp::OneOf::Left(true)),
21630 references_provider: Some(lsp::OneOf::Left(true)),
21631 ..lsp::ServerCapabilities::default()
21632 },
21633 cx,
21634 )
21635 .await;
21636
21637 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21638 let go_to_definition = cx
21639 .lsp
21640 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21641 move |params, _| async move {
21642 if empty_go_to_definition {
21643 Ok(None)
21644 } else {
21645 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21646 uri: params.text_document_position_params.text_document.uri,
21647 range: lsp::Range::new(
21648 lsp::Position::new(4, 3),
21649 lsp::Position::new(4, 6),
21650 ),
21651 })))
21652 }
21653 },
21654 );
21655 let references = cx
21656 .lsp
21657 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21658 Ok(Some(vec![lsp::Location {
21659 uri: params.text_document_position.text_document.uri,
21660 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21661 }]))
21662 });
21663 (go_to_definition, references)
21664 };
21665
21666 cx.set_state(
21667 &r#"fn one() {
21668 let mut a = ˇtwo();
21669 }
21670
21671 fn two() {}"#
21672 .unindent(),
21673 );
21674 set_up_lsp_handlers(false, &mut cx);
21675 let navigated = cx
21676 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21677 .await
21678 .expect("Failed to navigate to definition");
21679 assert_eq!(
21680 navigated,
21681 Navigated::Yes,
21682 "Should have navigated to definition from the GetDefinition response"
21683 );
21684 cx.assert_editor_state(
21685 &r#"fn one() {
21686 let mut a = two();
21687 }
21688
21689 fn «twoˇ»() {}"#
21690 .unindent(),
21691 );
21692
21693 let editors = cx.update_workspace(|workspace, _, cx| {
21694 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21695 });
21696 cx.update_editor(|_, _, test_editor_cx| {
21697 assert_eq!(
21698 editors.len(),
21699 1,
21700 "Initially, only one, test, editor should be open in the workspace"
21701 );
21702 assert_eq!(
21703 test_editor_cx.entity(),
21704 editors.last().expect("Asserted len is 1").clone()
21705 );
21706 });
21707
21708 set_up_lsp_handlers(true, &mut cx);
21709 let navigated = cx
21710 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21711 .await
21712 .expect("Failed to navigate to lookup references");
21713 assert_eq!(
21714 navigated,
21715 Navigated::Yes,
21716 "Should have navigated to references as a fallback after empty GoToDefinition response"
21717 );
21718 // We should not change the selections in the existing file,
21719 // if opening another milti buffer with the references
21720 cx.assert_editor_state(
21721 &r#"fn one() {
21722 let mut a = two();
21723 }
21724
21725 fn «twoˇ»() {}"#
21726 .unindent(),
21727 );
21728 let editors = cx.update_workspace(|workspace, _, cx| {
21729 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21730 });
21731 cx.update_editor(|_, _, test_editor_cx| {
21732 assert_eq!(
21733 editors.len(),
21734 2,
21735 "After falling back to references search, we open a new editor with the results"
21736 );
21737 let references_fallback_text = editors
21738 .into_iter()
21739 .find(|new_editor| *new_editor != test_editor_cx.entity())
21740 .expect("Should have one non-test editor now")
21741 .read(test_editor_cx)
21742 .text(test_editor_cx);
21743 assert_eq!(
21744 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21745 "Should use the range from the references response and not the GoToDefinition one"
21746 );
21747 });
21748}
21749
21750#[gpui::test]
21751async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21752 init_test(cx, |_| {});
21753 cx.update(|cx| {
21754 let mut editor_settings = EditorSettings::get_global(cx).clone();
21755 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21756 EditorSettings::override_global(editor_settings, cx);
21757 });
21758 let mut cx = EditorLspTestContext::new_rust(
21759 lsp::ServerCapabilities {
21760 definition_provider: Some(lsp::OneOf::Left(true)),
21761 references_provider: Some(lsp::OneOf::Left(true)),
21762 ..lsp::ServerCapabilities::default()
21763 },
21764 cx,
21765 )
21766 .await;
21767 let original_state = r#"fn one() {
21768 let mut a = ˇtwo();
21769 }
21770
21771 fn two() {}"#
21772 .unindent();
21773 cx.set_state(&original_state);
21774
21775 let mut go_to_definition = cx
21776 .lsp
21777 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21778 move |_, _| async move { Ok(None) },
21779 );
21780 let _references = cx
21781 .lsp
21782 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21783 panic!("Should not call for references with no go to definition fallback")
21784 });
21785
21786 let navigated = cx
21787 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21788 .await
21789 .expect("Failed to navigate to lookup references");
21790 go_to_definition
21791 .next()
21792 .await
21793 .expect("Should have called the go_to_definition handler");
21794
21795 assert_eq!(
21796 navigated,
21797 Navigated::No,
21798 "Should have navigated to references as a fallback after empty GoToDefinition response"
21799 );
21800 cx.assert_editor_state(&original_state);
21801 let editors = cx.update_workspace(|workspace, _, cx| {
21802 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21803 });
21804 cx.update_editor(|_, _, _| {
21805 assert_eq!(
21806 editors.len(),
21807 1,
21808 "After unsuccessful fallback, no other editor should have been opened"
21809 );
21810 });
21811}
21812
21813#[gpui::test]
21814async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21815 init_test(cx, |_| {});
21816 let mut cx = EditorLspTestContext::new_rust(
21817 lsp::ServerCapabilities {
21818 references_provider: Some(lsp::OneOf::Left(true)),
21819 ..lsp::ServerCapabilities::default()
21820 },
21821 cx,
21822 )
21823 .await;
21824
21825 cx.set_state(
21826 &r#"
21827 fn one() {
21828 let mut a = two();
21829 }
21830
21831 fn ˇtwo() {}"#
21832 .unindent(),
21833 );
21834 cx.lsp
21835 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21836 Ok(Some(vec![
21837 lsp::Location {
21838 uri: params.text_document_position.text_document.uri.clone(),
21839 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21840 },
21841 lsp::Location {
21842 uri: params.text_document_position.text_document.uri,
21843 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21844 },
21845 ]))
21846 });
21847 let navigated = cx
21848 .update_editor(|editor, window, cx| {
21849 editor.find_all_references(&FindAllReferences, window, cx)
21850 })
21851 .unwrap()
21852 .await
21853 .expect("Failed to navigate to references");
21854 assert_eq!(
21855 navigated,
21856 Navigated::Yes,
21857 "Should have navigated to references from the FindAllReferences response"
21858 );
21859 cx.assert_editor_state(
21860 &r#"fn one() {
21861 let mut a = two();
21862 }
21863
21864 fn ˇtwo() {}"#
21865 .unindent(),
21866 );
21867
21868 let editors = cx.update_workspace(|workspace, _, cx| {
21869 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21870 });
21871 cx.update_editor(|_, _, _| {
21872 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21873 });
21874
21875 cx.set_state(
21876 &r#"fn one() {
21877 let mut a = ˇtwo();
21878 }
21879
21880 fn two() {}"#
21881 .unindent(),
21882 );
21883 let navigated = cx
21884 .update_editor(|editor, window, cx| {
21885 editor.find_all_references(&FindAllReferences, window, cx)
21886 })
21887 .unwrap()
21888 .await
21889 .expect("Failed to navigate to references");
21890 assert_eq!(
21891 navigated,
21892 Navigated::Yes,
21893 "Should have navigated to references from the FindAllReferences response"
21894 );
21895 cx.assert_editor_state(
21896 &r#"fn one() {
21897 let mut a = ˇtwo();
21898 }
21899
21900 fn two() {}"#
21901 .unindent(),
21902 );
21903 let editors = cx.update_workspace(|workspace, _, cx| {
21904 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21905 });
21906 cx.update_editor(|_, _, _| {
21907 assert_eq!(
21908 editors.len(),
21909 2,
21910 "should have re-used the previous multibuffer"
21911 );
21912 });
21913
21914 cx.set_state(
21915 &r#"fn one() {
21916 let mut a = ˇtwo();
21917 }
21918 fn three() {}
21919 fn two() {}"#
21920 .unindent(),
21921 );
21922 cx.lsp
21923 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21924 Ok(Some(vec![
21925 lsp::Location {
21926 uri: params.text_document_position.text_document.uri.clone(),
21927 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21928 },
21929 lsp::Location {
21930 uri: params.text_document_position.text_document.uri,
21931 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21932 },
21933 ]))
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 fn three() {}
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 3,
21962 "should have used a new multibuffer as offsets changed"
21963 );
21964 });
21965}
21966#[gpui::test]
21967async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21968 init_test(cx, |_| {});
21969
21970 let language = Arc::new(Language::new(
21971 LanguageConfig::default(),
21972 Some(tree_sitter_rust::LANGUAGE.into()),
21973 ));
21974
21975 let text = r#"
21976 #[cfg(test)]
21977 mod tests() {
21978 #[test]
21979 fn runnable_1() {
21980 let a = 1;
21981 }
21982
21983 #[test]
21984 fn runnable_2() {
21985 let a = 1;
21986 let b = 2;
21987 }
21988 }
21989 "#
21990 .unindent();
21991
21992 let fs = FakeFs::new(cx.executor());
21993 fs.insert_file("/file.rs", Default::default()).await;
21994
21995 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21996 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21997 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21998 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21999 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22000
22001 let editor = cx.new_window_entity(|window, cx| {
22002 Editor::new(
22003 EditorMode::full(),
22004 multi_buffer,
22005 Some(project.clone()),
22006 window,
22007 cx,
22008 )
22009 });
22010
22011 editor.update_in(cx, |editor, window, cx| {
22012 let snapshot = editor.buffer().read(cx).snapshot(cx);
22013 editor.tasks.insert(
22014 (buffer.read(cx).remote_id(), 3),
22015 RunnableTasks {
22016 templates: vec![],
22017 offset: snapshot.anchor_before(43),
22018 column: 0,
22019 extra_variables: HashMap::default(),
22020 context_range: BufferOffset(43)..BufferOffset(85),
22021 },
22022 );
22023 editor.tasks.insert(
22024 (buffer.read(cx).remote_id(), 8),
22025 RunnableTasks {
22026 templates: vec![],
22027 offset: snapshot.anchor_before(86),
22028 column: 0,
22029 extra_variables: HashMap::default(),
22030 context_range: BufferOffset(86)..BufferOffset(191),
22031 },
22032 );
22033
22034 // Test finding task when cursor is inside function body
22035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22036 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22037 });
22038 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22039 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22040
22041 // Test finding task when cursor is on function name
22042 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22043 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22044 });
22045 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22046 assert_eq!(row, 8, "Should find task when cursor is on function name");
22047 });
22048}
22049
22050#[gpui::test]
22051async fn test_folding_buffers(cx: &mut TestAppContext) {
22052 init_test(cx, |_| {});
22053
22054 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22055 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22056 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22057
22058 let fs = FakeFs::new(cx.executor());
22059 fs.insert_tree(
22060 path!("/a"),
22061 json!({
22062 "first.rs": sample_text_1,
22063 "second.rs": sample_text_2,
22064 "third.rs": sample_text_3,
22065 }),
22066 )
22067 .await;
22068 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22069 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22070 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22071 let worktree = project.update(cx, |project, cx| {
22072 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22073 assert_eq!(worktrees.len(), 1);
22074 worktrees.pop().unwrap()
22075 });
22076 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22077
22078 let buffer_1 = project
22079 .update(cx, |project, cx| {
22080 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22081 })
22082 .await
22083 .unwrap();
22084 let buffer_2 = project
22085 .update(cx, |project, cx| {
22086 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22087 })
22088 .await
22089 .unwrap();
22090 let buffer_3 = project
22091 .update(cx, |project, cx| {
22092 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22093 })
22094 .await
22095 .unwrap();
22096
22097 let multi_buffer = cx.new(|cx| {
22098 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22099 multi_buffer.push_excerpts(
22100 buffer_1.clone(),
22101 [
22102 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22103 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22104 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22105 ],
22106 cx,
22107 );
22108 multi_buffer.push_excerpts(
22109 buffer_2.clone(),
22110 [
22111 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22112 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22113 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22114 ],
22115 cx,
22116 );
22117 multi_buffer.push_excerpts(
22118 buffer_3.clone(),
22119 [
22120 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22121 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22122 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22123 ],
22124 cx,
22125 );
22126 multi_buffer
22127 });
22128 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22129 Editor::new(
22130 EditorMode::full(),
22131 multi_buffer.clone(),
22132 Some(project.clone()),
22133 window,
22134 cx,
22135 )
22136 });
22137
22138 assert_eq!(
22139 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22140 "\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",
22141 );
22142
22143 multi_buffer_editor.update(cx, |editor, cx| {
22144 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22145 });
22146 assert_eq!(
22147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22148 "\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",
22149 "After folding the first buffer, its text should not be displayed"
22150 );
22151
22152 multi_buffer_editor.update(cx, |editor, cx| {
22153 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22154 });
22155 assert_eq!(
22156 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22157 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22158 "After folding the second buffer, its text should not be displayed"
22159 );
22160
22161 multi_buffer_editor.update(cx, |editor, cx| {
22162 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22163 });
22164 assert_eq!(
22165 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22166 "\n\n\n\n\n",
22167 "After folding the third buffer, its text should not be displayed"
22168 );
22169
22170 // Emulate selection inside the fold logic, that should work
22171 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22172 editor
22173 .snapshot(window, cx)
22174 .next_line_boundary(Point::new(0, 4));
22175 });
22176
22177 multi_buffer_editor.update(cx, |editor, cx| {
22178 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22179 });
22180 assert_eq!(
22181 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22182 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22183 "After unfolding the second buffer, its text should be displayed"
22184 );
22185
22186 // Typing inside of buffer 1 causes that buffer to be unfolded.
22187 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22188 assert_eq!(
22189 multi_buffer
22190 .read(cx)
22191 .snapshot(cx)
22192 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22193 .collect::<String>(),
22194 "bbbb"
22195 );
22196 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22197 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22198 });
22199 editor.handle_input("B", window, cx);
22200 });
22201
22202 assert_eq!(
22203 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22204 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22205 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22206 );
22207
22208 multi_buffer_editor.update(cx, |editor, cx| {
22209 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22210 });
22211 assert_eq!(
22212 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22213 "\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",
22214 "After unfolding the all buffers, all original text should be displayed"
22215 );
22216}
22217
22218#[gpui::test]
22219async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22220 init_test(cx, |_| {});
22221
22222 let sample_text_1 = "1111\n2222\n3333".to_string();
22223 let sample_text_2 = "4444\n5555\n6666".to_string();
22224 let sample_text_3 = "7777\n8888\n9999".to_string();
22225
22226 let fs = FakeFs::new(cx.executor());
22227 fs.insert_tree(
22228 path!("/a"),
22229 json!({
22230 "first.rs": sample_text_1,
22231 "second.rs": sample_text_2,
22232 "third.rs": sample_text_3,
22233 }),
22234 )
22235 .await;
22236 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22237 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22238 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22239 let worktree = project.update(cx, |project, cx| {
22240 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22241 assert_eq!(worktrees.len(), 1);
22242 worktrees.pop().unwrap()
22243 });
22244 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22245
22246 let buffer_1 = project
22247 .update(cx, |project, cx| {
22248 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22249 })
22250 .await
22251 .unwrap();
22252 let buffer_2 = project
22253 .update(cx, |project, cx| {
22254 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22255 })
22256 .await
22257 .unwrap();
22258 let buffer_3 = project
22259 .update(cx, |project, cx| {
22260 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22261 })
22262 .await
22263 .unwrap();
22264
22265 let multi_buffer = cx.new(|cx| {
22266 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22267 multi_buffer.push_excerpts(
22268 buffer_1.clone(),
22269 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22270 cx,
22271 );
22272 multi_buffer.push_excerpts(
22273 buffer_2.clone(),
22274 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22275 cx,
22276 );
22277 multi_buffer.push_excerpts(
22278 buffer_3.clone(),
22279 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22280 cx,
22281 );
22282 multi_buffer
22283 });
22284
22285 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22286 Editor::new(
22287 EditorMode::full(),
22288 multi_buffer,
22289 Some(project.clone()),
22290 window,
22291 cx,
22292 )
22293 });
22294
22295 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22296 assert_eq!(
22297 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22298 full_text,
22299 );
22300
22301 multi_buffer_editor.update(cx, |editor, cx| {
22302 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22303 });
22304 assert_eq!(
22305 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22306 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22307 "After folding the first buffer, its text should not be displayed"
22308 );
22309
22310 multi_buffer_editor.update(cx, |editor, cx| {
22311 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22312 });
22313
22314 assert_eq!(
22315 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22316 "\n\n\n\n\n\n7777\n8888\n9999",
22317 "After folding the second buffer, its text should not be displayed"
22318 );
22319
22320 multi_buffer_editor.update(cx, |editor, cx| {
22321 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22322 });
22323 assert_eq!(
22324 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22325 "\n\n\n\n\n",
22326 "After folding the third buffer, its text should not be displayed"
22327 );
22328
22329 multi_buffer_editor.update(cx, |editor, cx| {
22330 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22331 });
22332 assert_eq!(
22333 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22334 "\n\n\n\n4444\n5555\n6666\n\n",
22335 "After unfolding the second buffer, its text should be displayed"
22336 );
22337
22338 multi_buffer_editor.update(cx, |editor, cx| {
22339 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22340 });
22341 assert_eq!(
22342 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22343 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22344 "After unfolding the first buffer, its text should be displayed"
22345 );
22346
22347 multi_buffer_editor.update(cx, |editor, cx| {
22348 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22349 });
22350 assert_eq!(
22351 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22352 full_text,
22353 "After unfolding all buffers, all original text should be displayed"
22354 );
22355}
22356
22357#[gpui::test]
22358async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22359 init_test(cx, |_| {});
22360
22361 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22362
22363 let fs = FakeFs::new(cx.executor());
22364 fs.insert_tree(
22365 path!("/a"),
22366 json!({
22367 "main.rs": sample_text,
22368 }),
22369 )
22370 .await;
22371 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22372 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22373 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22374 let worktree = project.update(cx, |project, cx| {
22375 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22376 assert_eq!(worktrees.len(), 1);
22377 worktrees.pop().unwrap()
22378 });
22379 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22380
22381 let buffer_1 = project
22382 .update(cx, |project, cx| {
22383 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22384 })
22385 .await
22386 .unwrap();
22387
22388 let multi_buffer = cx.new(|cx| {
22389 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22390 multi_buffer.push_excerpts(
22391 buffer_1.clone(),
22392 [ExcerptRange::new(
22393 Point::new(0, 0)
22394 ..Point::new(
22395 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22396 0,
22397 ),
22398 )],
22399 cx,
22400 );
22401 multi_buffer
22402 });
22403 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22404 Editor::new(
22405 EditorMode::full(),
22406 multi_buffer,
22407 Some(project.clone()),
22408 window,
22409 cx,
22410 )
22411 });
22412
22413 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22414 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22415 enum TestHighlight {}
22416 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22417 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22418 editor.highlight_text::<TestHighlight>(
22419 vec![highlight_range.clone()],
22420 HighlightStyle::color(Hsla::green()),
22421 cx,
22422 );
22423 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22424 s.select_ranges(Some(highlight_range))
22425 });
22426 });
22427
22428 let full_text = format!("\n\n{sample_text}");
22429 assert_eq!(
22430 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22431 full_text,
22432 );
22433}
22434
22435#[gpui::test]
22436async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22437 init_test(cx, |_| {});
22438 cx.update(|cx| {
22439 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22440 "keymaps/default-linux.json",
22441 cx,
22442 )
22443 .unwrap();
22444 cx.bind_keys(default_key_bindings);
22445 });
22446
22447 let (editor, cx) = cx.add_window_view(|window, cx| {
22448 let multi_buffer = MultiBuffer::build_multi(
22449 [
22450 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22451 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22452 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22453 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22454 ],
22455 cx,
22456 );
22457 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22458
22459 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22460 // fold all but the second buffer, so that we test navigating between two
22461 // adjacent folded buffers, as well as folded buffers at the start and
22462 // end the multibuffer
22463 editor.fold_buffer(buffer_ids[0], cx);
22464 editor.fold_buffer(buffer_ids[2], cx);
22465 editor.fold_buffer(buffer_ids[3], cx);
22466
22467 editor
22468 });
22469 cx.simulate_resize(size(px(1000.), px(1000.)));
22470
22471 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22472 cx.assert_excerpts_with_selections(indoc! {"
22473 [EXCERPT]
22474 ˇ[FOLDED]
22475 [EXCERPT]
22476 a1
22477 b1
22478 [EXCERPT]
22479 [FOLDED]
22480 [EXCERPT]
22481 [FOLDED]
22482 "
22483 });
22484 cx.simulate_keystroke("down");
22485 cx.assert_excerpts_with_selections(indoc! {"
22486 [EXCERPT]
22487 [FOLDED]
22488 [EXCERPT]
22489 ˇa1
22490 b1
22491 [EXCERPT]
22492 [FOLDED]
22493 [EXCERPT]
22494 [FOLDED]
22495 "
22496 });
22497 cx.simulate_keystroke("down");
22498 cx.assert_excerpts_with_selections(indoc! {"
22499 [EXCERPT]
22500 [FOLDED]
22501 [EXCERPT]
22502 a1
22503 ˇb1
22504 [EXCERPT]
22505 [FOLDED]
22506 [EXCERPT]
22507 [FOLDED]
22508 "
22509 });
22510 cx.simulate_keystroke("down");
22511 cx.assert_excerpts_with_selections(indoc! {"
22512 [EXCERPT]
22513 [FOLDED]
22514 [EXCERPT]
22515 a1
22516 b1
22517 ˇ[EXCERPT]
22518 [FOLDED]
22519 [EXCERPT]
22520 [FOLDED]
22521 "
22522 });
22523 cx.simulate_keystroke("down");
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 for _ in 0..5 {
22537 cx.simulate_keystroke("down");
22538 cx.assert_excerpts_with_selections(indoc! {"
22539 [EXCERPT]
22540 [FOLDED]
22541 [EXCERPT]
22542 a1
22543 b1
22544 [EXCERPT]
22545 [FOLDED]
22546 [EXCERPT]
22547 ˇ[FOLDED]
22548 "
22549 });
22550 }
22551
22552 cx.simulate_keystroke("up");
22553 cx.assert_excerpts_with_selections(indoc! {"
22554 [EXCERPT]
22555 [FOLDED]
22556 [EXCERPT]
22557 a1
22558 b1
22559 [EXCERPT]
22560 ˇ[FOLDED]
22561 [EXCERPT]
22562 [FOLDED]
22563 "
22564 });
22565 cx.simulate_keystroke("up");
22566 cx.assert_excerpts_with_selections(indoc! {"
22567 [EXCERPT]
22568 [FOLDED]
22569 [EXCERPT]
22570 a1
22571 b1
22572 ˇ[EXCERPT]
22573 [FOLDED]
22574 [EXCERPT]
22575 [FOLDED]
22576 "
22577 });
22578 cx.simulate_keystroke("up");
22579 cx.assert_excerpts_with_selections(indoc! {"
22580 [EXCERPT]
22581 [FOLDED]
22582 [EXCERPT]
22583 a1
22584 ˇb1
22585 [EXCERPT]
22586 [FOLDED]
22587 [EXCERPT]
22588 [FOLDED]
22589 "
22590 });
22591 cx.simulate_keystroke("up");
22592 cx.assert_excerpts_with_selections(indoc! {"
22593 [EXCERPT]
22594 [FOLDED]
22595 [EXCERPT]
22596 ˇa1
22597 b1
22598 [EXCERPT]
22599 [FOLDED]
22600 [EXCERPT]
22601 [FOLDED]
22602 "
22603 });
22604 for _ in 0..5 {
22605 cx.simulate_keystroke("up");
22606 cx.assert_excerpts_with_selections(indoc! {"
22607 [EXCERPT]
22608 ˇ[FOLDED]
22609 [EXCERPT]
22610 a1
22611 b1
22612 [EXCERPT]
22613 [FOLDED]
22614 [EXCERPT]
22615 [FOLDED]
22616 "
22617 });
22618 }
22619}
22620
22621#[gpui::test]
22622async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22623 init_test(cx, |_| {});
22624
22625 // Simple insertion
22626 assert_highlighted_edits(
22627 "Hello, world!",
22628 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22629 true,
22630 cx,
22631 |highlighted_edits, cx| {
22632 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22633 assert_eq!(highlighted_edits.highlights.len(), 1);
22634 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22635 assert_eq!(
22636 highlighted_edits.highlights[0].1.background_color,
22637 Some(cx.theme().status().created_background)
22638 );
22639 },
22640 )
22641 .await;
22642
22643 // Replacement
22644 assert_highlighted_edits(
22645 "This is a test.",
22646 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22647 false,
22648 cx,
22649 |highlighted_edits, cx| {
22650 assert_eq!(highlighted_edits.text, "That is a test.");
22651 assert_eq!(highlighted_edits.highlights.len(), 1);
22652 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22653 assert_eq!(
22654 highlighted_edits.highlights[0].1.background_color,
22655 Some(cx.theme().status().created_background)
22656 );
22657 },
22658 )
22659 .await;
22660
22661 // Multiple edits
22662 assert_highlighted_edits(
22663 "Hello, world!",
22664 vec![
22665 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22666 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22667 ],
22668 false,
22669 cx,
22670 |highlighted_edits, cx| {
22671 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22672 assert_eq!(highlighted_edits.highlights.len(), 2);
22673 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22674 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22675 assert_eq!(
22676 highlighted_edits.highlights[0].1.background_color,
22677 Some(cx.theme().status().created_background)
22678 );
22679 assert_eq!(
22680 highlighted_edits.highlights[1].1.background_color,
22681 Some(cx.theme().status().created_background)
22682 );
22683 },
22684 )
22685 .await;
22686
22687 // Multiple lines with edits
22688 assert_highlighted_edits(
22689 "First line\nSecond line\nThird line\nFourth line",
22690 vec![
22691 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22692 (
22693 Point::new(2, 0)..Point::new(2, 10),
22694 "New third line".to_string(),
22695 ),
22696 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22697 ],
22698 false,
22699 cx,
22700 |highlighted_edits, cx| {
22701 assert_eq!(
22702 highlighted_edits.text,
22703 "Second modified\nNew third line\nFourth updated line"
22704 );
22705 assert_eq!(highlighted_edits.highlights.len(), 3);
22706 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22707 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22708 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22709 for highlight in &highlighted_edits.highlights {
22710 assert_eq!(
22711 highlight.1.background_color,
22712 Some(cx.theme().status().created_background)
22713 );
22714 }
22715 },
22716 )
22717 .await;
22718}
22719
22720#[gpui::test]
22721async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22722 init_test(cx, |_| {});
22723
22724 // Deletion
22725 assert_highlighted_edits(
22726 "Hello, world!",
22727 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22728 true,
22729 cx,
22730 |highlighted_edits, cx| {
22731 assert_eq!(highlighted_edits.text, "Hello, world!");
22732 assert_eq!(highlighted_edits.highlights.len(), 1);
22733 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22734 assert_eq!(
22735 highlighted_edits.highlights[0].1.background_color,
22736 Some(cx.theme().status().deleted_background)
22737 );
22738 },
22739 )
22740 .await;
22741
22742 // Insertion
22743 assert_highlighted_edits(
22744 "Hello, world!",
22745 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22746 true,
22747 cx,
22748 |highlighted_edits, cx| {
22749 assert_eq!(highlighted_edits.highlights.len(), 1);
22750 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22751 assert_eq!(
22752 highlighted_edits.highlights[0].1.background_color,
22753 Some(cx.theme().status().created_background)
22754 );
22755 },
22756 )
22757 .await;
22758}
22759
22760async fn assert_highlighted_edits(
22761 text: &str,
22762 edits: Vec<(Range<Point>, String)>,
22763 include_deletions: bool,
22764 cx: &mut TestAppContext,
22765 assertion_fn: impl Fn(HighlightedText, &App),
22766) {
22767 let window = cx.add_window(|window, cx| {
22768 let buffer = MultiBuffer::build_simple(text, cx);
22769 Editor::new(EditorMode::full(), buffer, None, window, cx)
22770 });
22771 let cx = &mut VisualTestContext::from_window(*window, cx);
22772
22773 let (buffer, snapshot) = window
22774 .update(cx, |editor, _window, cx| {
22775 (
22776 editor.buffer().clone(),
22777 editor.buffer().read(cx).snapshot(cx),
22778 )
22779 })
22780 .unwrap();
22781
22782 let edits = edits
22783 .into_iter()
22784 .map(|(range, edit)| {
22785 (
22786 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22787 edit,
22788 )
22789 })
22790 .collect::<Vec<_>>();
22791
22792 let text_anchor_edits = edits
22793 .clone()
22794 .into_iter()
22795 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22796 .collect::<Vec<_>>();
22797
22798 let edit_preview = window
22799 .update(cx, |_, _window, cx| {
22800 buffer
22801 .read(cx)
22802 .as_singleton()
22803 .unwrap()
22804 .read(cx)
22805 .preview_edits(text_anchor_edits.into(), cx)
22806 })
22807 .unwrap()
22808 .await;
22809
22810 cx.update(|_window, cx| {
22811 let highlighted_edits = edit_prediction_edit_text(
22812 snapshot.as_singleton().unwrap().2,
22813 &edits,
22814 &edit_preview,
22815 include_deletions,
22816 cx,
22817 );
22818 assertion_fn(highlighted_edits, cx)
22819 });
22820}
22821
22822#[track_caller]
22823fn assert_breakpoint(
22824 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22825 path: &Arc<Path>,
22826 expected: Vec<(u32, Breakpoint)>,
22827) {
22828 if expected.is_empty() {
22829 assert!(!breakpoints.contains_key(path), "{}", path.display());
22830 } else {
22831 let mut breakpoint = breakpoints
22832 .get(path)
22833 .unwrap()
22834 .iter()
22835 .map(|breakpoint| {
22836 (
22837 breakpoint.row,
22838 Breakpoint {
22839 message: breakpoint.message.clone(),
22840 state: breakpoint.state,
22841 condition: breakpoint.condition.clone(),
22842 hit_condition: breakpoint.hit_condition.clone(),
22843 },
22844 )
22845 })
22846 .collect::<Vec<_>>();
22847
22848 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22849
22850 assert_eq!(expected, breakpoint);
22851 }
22852}
22853
22854fn add_log_breakpoint_at_cursor(
22855 editor: &mut Editor,
22856 log_message: &str,
22857 window: &mut Window,
22858 cx: &mut Context<Editor>,
22859) {
22860 let (anchor, bp) = editor
22861 .breakpoints_at_cursors(window, cx)
22862 .first()
22863 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22864 .unwrap_or_else(|| {
22865 let snapshot = editor.snapshot(window, cx);
22866 let cursor_position: Point =
22867 editor.selections.newest(&snapshot.display_snapshot).head();
22868
22869 let breakpoint_position = snapshot
22870 .buffer_snapshot()
22871 .anchor_before(Point::new(cursor_position.row, 0));
22872
22873 (breakpoint_position, Breakpoint::new_log(log_message))
22874 });
22875
22876 editor.edit_breakpoint_at_anchor(
22877 anchor,
22878 bp,
22879 BreakpointEditAction::EditLogMessage(log_message.into()),
22880 cx,
22881 );
22882}
22883
22884#[gpui::test]
22885async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22886 init_test(cx, |_| {});
22887
22888 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22889 let fs = FakeFs::new(cx.executor());
22890 fs.insert_tree(
22891 path!("/a"),
22892 json!({
22893 "main.rs": sample_text,
22894 }),
22895 )
22896 .await;
22897 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22898 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22899 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22900
22901 let fs = FakeFs::new(cx.executor());
22902 fs.insert_tree(
22903 path!("/a"),
22904 json!({
22905 "main.rs": sample_text,
22906 }),
22907 )
22908 .await;
22909 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22910 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22911 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22912 let worktree_id = workspace
22913 .update(cx, |workspace, _window, cx| {
22914 workspace.project().update(cx, |project, cx| {
22915 project.worktrees(cx).next().unwrap().read(cx).id()
22916 })
22917 })
22918 .unwrap();
22919
22920 let buffer = project
22921 .update(cx, |project, cx| {
22922 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22923 })
22924 .await
22925 .unwrap();
22926
22927 let (editor, cx) = cx.add_window_view(|window, cx| {
22928 Editor::new(
22929 EditorMode::full(),
22930 MultiBuffer::build_from_buffer(buffer, cx),
22931 Some(project.clone()),
22932 window,
22933 cx,
22934 )
22935 });
22936
22937 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22938 let abs_path = project.read_with(cx, |project, cx| {
22939 project
22940 .absolute_path(&project_path, cx)
22941 .map(Arc::from)
22942 .unwrap()
22943 });
22944
22945 // assert we can add breakpoint on the first line
22946 editor.update_in(cx, |editor, window, cx| {
22947 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22948 editor.move_to_end(&MoveToEnd, window, cx);
22949 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22950 });
22951
22952 let breakpoints = editor.update(cx, |editor, cx| {
22953 editor
22954 .breakpoint_store()
22955 .as_ref()
22956 .unwrap()
22957 .read(cx)
22958 .all_source_breakpoints(cx)
22959 });
22960
22961 assert_eq!(1, breakpoints.len());
22962 assert_breakpoint(
22963 &breakpoints,
22964 &abs_path,
22965 vec![
22966 (0, Breakpoint::new_standard()),
22967 (3, Breakpoint::new_standard()),
22968 ],
22969 );
22970
22971 editor.update_in(cx, |editor, window, cx| {
22972 editor.move_to_beginning(&MoveToBeginning, window, cx);
22973 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22974 });
22975
22976 let breakpoints = editor.update(cx, |editor, cx| {
22977 editor
22978 .breakpoint_store()
22979 .as_ref()
22980 .unwrap()
22981 .read(cx)
22982 .all_source_breakpoints(cx)
22983 });
22984
22985 assert_eq!(1, breakpoints.len());
22986 assert_breakpoint(
22987 &breakpoints,
22988 &abs_path,
22989 vec![(3, Breakpoint::new_standard())],
22990 );
22991
22992 editor.update_in(cx, |editor, window, cx| {
22993 editor.move_to_end(&MoveToEnd, window, cx);
22994 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22995 });
22996
22997 let breakpoints = editor.update(cx, |editor, cx| {
22998 editor
22999 .breakpoint_store()
23000 .as_ref()
23001 .unwrap()
23002 .read(cx)
23003 .all_source_breakpoints(cx)
23004 });
23005
23006 assert_eq!(0, breakpoints.len());
23007 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23008}
23009
23010#[gpui::test]
23011async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23012 init_test(cx, |_| {});
23013
23014 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23015
23016 let fs = FakeFs::new(cx.executor());
23017 fs.insert_tree(
23018 path!("/a"),
23019 json!({
23020 "main.rs": sample_text,
23021 }),
23022 )
23023 .await;
23024 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23025 let (workspace, cx) =
23026 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23027
23028 let worktree_id = workspace.update(cx, |workspace, cx| {
23029 workspace.project().update(cx, |project, cx| {
23030 project.worktrees(cx).next().unwrap().read(cx).id()
23031 })
23032 });
23033
23034 let buffer = project
23035 .update(cx, |project, cx| {
23036 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23037 })
23038 .await
23039 .unwrap();
23040
23041 let (editor, cx) = cx.add_window_view(|window, cx| {
23042 Editor::new(
23043 EditorMode::full(),
23044 MultiBuffer::build_from_buffer(buffer, cx),
23045 Some(project.clone()),
23046 window,
23047 cx,
23048 )
23049 });
23050
23051 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23052 let abs_path = project.read_with(cx, |project, cx| {
23053 project
23054 .absolute_path(&project_path, cx)
23055 .map(Arc::from)
23056 .unwrap()
23057 });
23058
23059 editor.update_in(cx, |editor, window, cx| {
23060 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23061 });
23062
23063 let breakpoints = editor.update(cx, |editor, cx| {
23064 editor
23065 .breakpoint_store()
23066 .as_ref()
23067 .unwrap()
23068 .read(cx)
23069 .all_source_breakpoints(cx)
23070 });
23071
23072 assert_breakpoint(
23073 &breakpoints,
23074 &abs_path,
23075 vec![(0, Breakpoint::new_log("hello world"))],
23076 );
23077
23078 // Removing a log message from a log breakpoint should remove it
23079 editor.update_in(cx, |editor, window, cx| {
23080 add_log_breakpoint_at_cursor(editor, "", window, cx);
23081 });
23082
23083 let breakpoints = editor.update(cx, |editor, cx| {
23084 editor
23085 .breakpoint_store()
23086 .as_ref()
23087 .unwrap()
23088 .read(cx)
23089 .all_source_breakpoints(cx)
23090 });
23091
23092 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23093
23094 editor.update_in(cx, |editor, window, cx| {
23095 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23096 editor.move_to_end(&MoveToEnd, window, cx);
23097 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23098 // Not adding a log message to a standard breakpoint shouldn't remove it
23099 add_log_breakpoint_at_cursor(editor, "", window, cx);
23100 });
23101
23102 let breakpoints = editor.update(cx, |editor, cx| {
23103 editor
23104 .breakpoint_store()
23105 .as_ref()
23106 .unwrap()
23107 .read(cx)
23108 .all_source_breakpoints(cx)
23109 });
23110
23111 assert_breakpoint(
23112 &breakpoints,
23113 &abs_path,
23114 vec![
23115 (0, Breakpoint::new_standard()),
23116 (3, Breakpoint::new_standard()),
23117 ],
23118 );
23119
23120 editor.update_in(cx, |editor, window, cx| {
23121 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23122 });
23123
23124 let breakpoints = editor.update(cx, |editor, cx| {
23125 editor
23126 .breakpoint_store()
23127 .as_ref()
23128 .unwrap()
23129 .read(cx)
23130 .all_source_breakpoints(cx)
23131 });
23132
23133 assert_breakpoint(
23134 &breakpoints,
23135 &abs_path,
23136 vec![
23137 (0, Breakpoint::new_standard()),
23138 (3, Breakpoint::new_log("hello world")),
23139 ],
23140 );
23141
23142 editor.update_in(cx, |editor, window, cx| {
23143 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23144 });
23145
23146 let breakpoints = editor.update(cx, |editor, cx| {
23147 editor
23148 .breakpoint_store()
23149 .as_ref()
23150 .unwrap()
23151 .read(cx)
23152 .all_source_breakpoints(cx)
23153 });
23154
23155 assert_breakpoint(
23156 &breakpoints,
23157 &abs_path,
23158 vec![
23159 (0, Breakpoint::new_standard()),
23160 (3, Breakpoint::new_log("hello Earth!!")),
23161 ],
23162 );
23163}
23164
23165/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23166/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23167/// or when breakpoints were placed out of order. This tests for a regression too
23168#[gpui::test]
23169async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23170 init_test(cx, |_| {});
23171
23172 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23173 let fs = FakeFs::new(cx.executor());
23174 fs.insert_tree(
23175 path!("/a"),
23176 json!({
23177 "main.rs": sample_text,
23178 }),
23179 )
23180 .await;
23181 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23182 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23183 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23184
23185 let fs = FakeFs::new(cx.executor());
23186 fs.insert_tree(
23187 path!("/a"),
23188 json!({
23189 "main.rs": sample_text,
23190 }),
23191 )
23192 .await;
23193 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23194 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23195 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23196 let worktree_id = workspace
23197 .update(cx, |workspace, _window, cx| {
23198 workspace.project().update(cx, |project, cx| {
23199 project.worktrees(cx).next().unwrap().read(cx).id()
23200 })
23201 })
23202 .unwrap();
23203
23204 let buffer = project
23205 .update(cx, |project, cx| {
23206 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23207 })
23208 .await
23209 .unwrap();
23210
23211 let (editor, cx) = cx.add_window_view(|window, cx| {
23212 Editor::new(
23213 EditorMode::full(),
23214 MultiBuffer::build_from_buffer(buffer, cx),
23215 Some(project.clone()),
23216 window,
23217 cx,
23218 )
23219 });
23220
23221 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23222 let abs_path = project.read_with(cx, |project, cx| {
23223 project
23224 .absolute_path(&project_path, cx)
23225 .map(Arc::from)
23226 .unwrap()
23227 });
23228
23229 // assert we can add breakpoint on the first line
23230 editor.update_in(cx, |editor, window, cx| {
23231 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23232 editor.move_to_end(&MoveToEnd, window, cx);
23233 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23234 editor.move_up(&MoveUp, window, cx);
23235 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23236 });
23237
23238 let breakpoints = editor.update(cx, |editor, cx| {
23239 editor
23240 .breakpoint_store()
23241 .as_ref()
23242 .unwrap()
23243 .read(cx)
23244 .all_source_breakpoints(cx)
23245 });
23246
23247 assert_eq!(1, breakpoints.len());
23248 assert_breakpoint(
23249 &breakpoints,
23250 &abs_path,
23251 vec![
23252 (0, Breakpoint::new_standard()),
23253 (2, Breakpoint::new_standard()),
23254 (3, Breakpoint::new_standard()),
23255 ],
23256 );
23257
23258 editor.update_in(cx, |editor, window, cx| {
23259 editor.move_to_beginning(&MoveToBeginning, window, cx);
23260 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23261 editor.move_to_end(&MoveToEnd, window, cx);
23262 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23263 // Disabling a breakpoint that doesn't exist should do nothing
23264 editor.move_up(&MoveUp, window, cx);
23265 editor.move_up(&MoveUp, window, cx);
23266 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23267 });
23268
23269 let breakpoints = editor.update(cx, |editor, cx| {
23270 editor
23271 .breakpoint_store()
23272 .as_ref()
23273 .unwrap()
23274 .read(cx)
23275 .all_source_breakpoints(cx)
23276 });
23277
23278 let disable_breakpoint = {
23279 let mut bp = Breakpoint::new_standard();
23280 bp.state = BreakpointState::Disabled;
23281 bp
23282 };
23283
23284 assert_eq!(1, breakpoints.len());
23285 assert_breakpoint(
23286 &breakpoints,
23287 &abs_path,
23288 vec![
23289 (0, disable_breakpoint.clone()),
23290 (2, Breakpoint::new_standard()),
23291 (3, disable_breakpoint.clone()),
23292 ],
23293 );
23294
23295 editor.update_in(cx, |editor, window, cx| {
23296 editor.move_to_beginning(&MoveToBeginning, window, cx);
23297 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23298 editor.move_to_end(&MoveToEnd, window, cx);
23299 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23300 editor.move_up(&MoveUp, window, cx);
23301 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23302 });
23303
23304 let breakpoints = editor.update(cx, |editor, cx| {
23305 editor
23306 .breakpoint_store()
23307 .as_ref()
23308 .unwrap()
23309 .read(cx)
23310 .all_source_breakpoints(cx)
23311 });
23312
23313 assert_eq!(1, breakpoints.len());
23314 assert_breakpoint(
23315 &breakpoints,
23316 &abs_path,
23317 vec![
23318 (0, Breakpoint::new_standard()),
23319 (2, disable_breakpoint),
23320 (3, Breakpoint::new_standard()),
23321 ],
23322 );
23323}
23324
23325#[gpui::test]
23326async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23327 init_test(cx, |_| {});
23328 let capabilities = lsp::ServerCapabilities {
23329 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23330 prepare_provider: Some(true),
23331 work_done_progress_options: Default::default(),
23332 })),
23333 ..Default::default()
23334 };
23335 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23336
23337 cx.set_state(indoc! {"
23338 struct Fˇoo {}
23339 "});
23340
23341 cx.update_editor(|editor, _, cx| {
23342 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23343 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23344 editor.highlight_background::<DocumentHighlightRead>(
23345 &[highlight_range],
23346 |theme| theme.colors().editor_document_highlight_read_background,
23347 cx,
23348 );
23349 });
23350
23351 let mut prepare_rename_handler = cx
23352 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23353 move |_, _, _| async move {
23354 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23355 start: lsp::Position {
23356 line: 0,
23357 character: 7,
23358 },
23359 end: lsp::Position {
23360 line: 0,
23361 character: 10,
23362 },
23363 })))
23364 },
23365 );
23366 let prepare_rename_task = cx
23367 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23368 .expect("Prepare rename was not started");
23369 prepare_rename_handler.next().await.unwrap();
23370 prepare_rename_task.await.expect("Prepare rename failed");
23371
23372 let mut rename_handler =
23373 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23374 let edit = lsp::TextEdit {
23375 range: lsp::Range {
23376 start: lsp::Position {
23377 line: 0,
23378 character: 7,
23379 },
23380 end: lsp::Position {
23381 line: 0,
23382 character: 10,
23383 },
23384 },
23385 new_text: "FooRenamed".to_string(),
23386 };
23387 Ok(Some(lsp::WorkspaceEdit::new(
23388 // Specify the same edit twice
23389 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23390 )))
23391 });
23392 let rename_task = cx
23393 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23394 .expect("Confirm rename was not started");
23395 rename_handler.next().await.unwrap();
23396 rename_task.await.expect("Confirm rename failed");
23397 cx.run_until_parked();
23398
23399 // Despite two edits, only one is actually applied as those are identical
23400 cx.assert_editor_state(indoc! {"
23401 struct FooRenamedˇ {}
23402 "});
23403}
23404
23405#[gpui::test]
23406async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23407 init_test(cx, |_| {});
23408 // These capabilities indicate that the server does not support prepare rename.
23409 let capabilities = lsp::ServerCapabilities {
23410 rename_provider: Some(lsp::OneOf::Left(true)),
23411 ..Default::default()
23412 };
23413 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23414
23415 cx.set_state(indoc! {"
23416 struct Fˇoo {}
23417 "});
23418
23419 cx.update_editor(|editor, _window, cx| {
23420 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23421 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23422 editor.highlight_background::<DocumentHighlightRead>(
23423 &[highlight_range],
23424 |theme| theme.colors().editor_document_highlight_read_background,
23425 cx,
23426 );
23427 });
23428
23429 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23430 .expect("Prepare rename was not started")
23431 .await
23432 .expect("Prepare rename failed");
23433
23434 let mut rename_handler =
23435 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23436 let edit = lsp::TextEdit {
23437 range: lsp::Range {
23438 start: lsp::Position {
23439 line: 0,
23440 character: 7,
23441 },
23442 end: lsp::Position {
23443 line: 0,
23444 character: 10,
23445 },
23446 },
23447 new_text: "FooRenamed".to_string(),
23448 };
23449 Ok(Some(lsp::WorkspaceEdit::new(
23450 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23451 )))
23452 });
23453 let rename_task = cx
23454 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23455 .expect("Confirm rename was not started");
23456 rename_handler.next().await.unwrap();
23457 rename_task.await.expect("Confirm rename failed");
23458 cx.run_until_parked();
23459
23460 // Correct range is renamed, as `surrounding_word` is used to find it.
23461 cx.assert_editor_state(indoc! {"
23462 struct FooRenamedˇ {}
23463 "});
23464}
23465
23466#[gpui::test]
23467async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23468 init_test(cx, |_| {});
23469 let mut cx = EditorTestContext::new(cx).await;
23470
23471 let language = Arc::new(
23472 Language::new(
23473 LanguageConfig::default(),
23474 Some(tree_sitter_html::LANGUAGE.into()),
23475 )
23476 .with_brackets_query(
23477 r#"
23478 ("<" @open "/>" @close)
23479 ("</" @open ">" @close)
23480 ("<" @open ">" @close)
23481 ("\"" @open "\"" @close)
23482 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23483 "#,
23484 )
23485 .unwrap(),
23486 );
23487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23488
23489 cx.set_state(indoc! {"
23490 <span>ˇ</span>
23491 "});
23492 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23493 cx.assert_editor_state(indoc! {"
23494 <span>
23495 ˇ
23496 </span>
23497 "});
23498
23499 cx.set_state(indoc! {"
23500 <span><span></span>ˇ</span>
23501 "});
23502 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23503 cx.assert_editor_state(indoc! {"
23504 <span><span></span>
23505 ˇ</span>
23506 "});
23507
23508 cx.set_state(indoc! {"
23509 <span>ˇ
23510 </span>
23511 "});
23512 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23513 cx.assert_editor_state(indoc! {"
23514 <span>
23515 ˇ
23516 </span>
23517 "});
23518}
23519
23520#[gpui::test(iterations = 10)]
23521async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23522 init_test(cx, |_| {});
23523
23524 let fs = FakeFs::new(cx.executor());
23525 fs.insert_tree(
23526 path!("/dir"),
23527 json!({
23528 "a.ts": "a",
23529 }),
23530 )
23531 .await;
23532
23533 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23534 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23535 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23536
23537 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23538 language_registry.add(Arc::new(Language::new(
23539 LanguageConfig {
23540 name: "TypeScript".into(),
23541 matcher: LanguageMatcher {
23542 path_suffixes: vec!["ts".to_string()],
23543 ..Default::default()
23544 },
23545 ..Default::default()
23546 },
23547 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23548 )));
23549 let mut fake_language_servers = language_registry.register_fake_lsp(
23550 "TypeScript",
23551 FakeLspAdapter {
23552 capabilities: lsp::ServerCapabilities {
23553 code_lens_provider: Some(lsp::CodeLensOptions {
23554 resolve_provider: Some(true),
23555 }),
23556 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23557 commands: vec!["_the/command".to_string()],
23558 ..lsp::ExecuteCommandOptions::default()
23559 }),
23560 ..lsp::ServerCapabilities::default()
23561 },
23562 ..FakeLspAdapter::default()
23563 },
23564 );
23565
23566 let editor = workspace
23567 .update(cx, |workspace, window, cx| {
23568 workspace.open_abs_path(
23569 PathBuf::from(path!("/dir/a.ts")),
23570 OpenOptions::default(),
23571 window,
23572 cx,
23573 )
23574 })
23575 .unwrap()
23576 .await
23577 .unwrap()
23578 .downcast::<Editor>()
23579 .unwrap();
23580 cx.executor().run_until_parked();
23581
23582 let fake_server = fake_language_servers.next().await.unwrap();
23583
23584 let buffer = editor.update(cx, |editor, cx| {
23585 editor
23586 .buffer()
23587 .read(cx)
23588 .as_singleton()
23589 .expect("have opened a single file by path")
23590 });
23591
23592 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23593 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23594 drop(buffer_snapshot);
23595 let actions = cx
23596 .update_window(*workspace, |_, window, cx| {
23597 project.code_actions(&buffer, anchor..anchor, window, cx)
23598 })
23599 .unwrap();
23600
23601 fake_server
23602 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23603 Ok(Some(vec![
23604 lsp::CodeLens {
23605 range: lsp::Range::default(),
23606 command: Some(lsp::Command {
23607 title: "Code lens command".to_owned(),
23608 command: "_the/command".to_owned(),
23609 arguments: None,
23610 }),
23611 data: None,
23612 },
23613 lsp::CodeLens {
23614 range: lsp::Range::default(),
23615 command: Some(lsp::Command {
23616 title: "Command not in capabilities".to_owned(),
23617 command: "not in capabilities".to_owned(),
23618 arguments: None,
23619 }),
23620 data: None,
23621 },
23622 lsp::CodeLens {
23623 range: lsp::Range {
23624 start: lsp::Position {
23625 line: 1,
23626 character: 1,
23627 },
23628 end: lsp::Position {
23629 line: 1,
23630 character: 1,
23631 },
23632 },
23633 command: Some(lsp::Command {
23634 title: "Command not in range".to_owned(),
23635 command: "_the/command".to_owned(),
23636 arguments: None,
23637 }),
23638 data: None,
23639 },
23640 ]))
23641 })
23642 .next()
23643 .await;
23644
23645 let actions = actions.await.unwrap();
23646 assert_eq!(
23647 actions.len(),
23648 1,
23649 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23650 );
23651 let action = actions[0].clone();
23652 let apply = project.update(cx, |project, cx| {
23653 project.apply_code_action(buffer.clone(), action, true, cx)
23654 });
23655
23656 // Resolving the code action does not populate its edits. In absence of
23657 // edits, we must execute the given command.
23658 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23659 |mut lens, _| async move {
23660 let lens_command = lens.command.as_mut().expect("should have a command");
23661 assert_eq!(lens_command.title, "Code lens command");
23662 lens_command.arguments = Some(vec![json!("the-argument")]);
23663 Ok(lens)
23664 },
23665 );
23666
23667 // While executing the command, the language server sends the editor
23668 // a `workspaceEdit` request.
23669 fake_server
23670 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23671 let fake = fake_server.clone();
23672 move |params, _| {
23673 assert_eq!(params.command, "_the/command");
23674 let fake = fake.clone();
23675 async move {
23676 fake.server
23677 .request::<lsp::request::ApplyWorkspaceEdit>(
23678 lsp::ApplyWorkspaceEditParams {
23679 label: None,
23680 edit: lsp::WorkspaceEdit {
23681 changes: Some(
23682 [(
23683 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23684 vec![lsp::TextEdit {
23685 range: lsp::Range::new(
23686 lsp::Position::new(0, 0),
23687 lsp::Position::new(0, 0),
23688 ),
23689 new_text: "X".into(),
23690 }],
23691 )]
23692 .into_iter()
23693 .collect(),
23694 ),
23695 ..lsp::WorkspaceEdit::default()
23696 },
23697 },
23698 )
23699 .await
23700 .into_response()
23701 .unwrap();
23702 Ok(Some(json!(null)))
23703 }
23704 }
23705 })
23706 .next()
23707 .await;
23708
23709 // Applying the code lens command returns a project transaction containing the edits
23710 // sent by the language server in its `workspaceEdit` request.
23711 let transaction = apply.await.unwrap();
23712 assert!(transaction.0.contains_key(&buffer));
23713 buffer.update(cx, |buffer, cx| {
23714 assert_eq!(buffer.text(), "Xa");
23715 buffer.undo(cx);
23716 assert_eq!(buffer.text(), "a");
23717 });
23718
23719 let actions_after_edits = cx
23720 .update_window(*workspace, |_, window, cx| {
23721 project.code_actions(&buffer, anchor..anchor, window, cx)
23722 })
23723 .unwrap()
23724 .await
23725 .unwrap();
23726 assert_eq!(
23727 actions, actions_after_edits,
23728 "For the same selection, same code lens actions should be returned"
23729 );
23730
23731 let _responses =
23732 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23733 panic!("No more code lens requests are expected");
23734 });
23735 editor.update_in(cx, |editor, window, cx| {
23736 editor.select_all(&SelectAll, window, cx);
23737 });
23738 cx.executor().run_until_parked();
23739 let new_actions = cx
23740 .update_window(*workspace, |_, window, cx| {
23741 project.code_actions(&buffer, anchor..anchor, window, cx)
23742 })
23743 .unwrap()
23744 .await
23745 .unwrap();
23746 assert_eq!(
23747 actions, new_actions,
23748 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23749 );
23750}
23751
23752#[gpui::test]
23753async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23754 init_test(cx, |_| {});
23755
23756 let fs = FakeFs::new(cx.executor());
23757 let main_text = r#"fn main() {
23758println!("1");
23759println!("2");
23760println!("3");
23761println!("4");
23762println!("5");
23763}"#;
23764 let lib_text = "mod foo {}";
23765 fs.insert_tree(
23766 path!("/a"),
23767 json!({
23768 "lib.rs": lib_text,
23769 "main.rs": main_text,
23770 }),
23771 )
23772 .await;
23773
23774 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23775 let (workspace, cx) =
23776 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23777 let worktree_id = workspace.update(cx, |workspace, cx| {
23778 workspace.project().update(cx, |project, cx| {
23779 project.worktrees(cx).next().unwrap().read(cx).id()
23780 })
23781 });
23782
23783 let expected_ranges = vec![
23784 Point::new(0, 0)..Point::new(0, 0),
23785 Point::new(1, 0)..Point::new(1, 1),
23786 Point::new(2, 0)..Point::new(2, 2),
23787 Point::new(3, 0)..Point::new(3, 3),
23788 ];
23789
23790 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23791 let editor_1 = workspace
23792 .update_in(cx, |workspace, window, cx| {
23793 workspace.open_path(
23794 (worktree_id, rel_path("main.rs")),
23795 Some(pane_1.downgrade()),
23796 true,
23797 window,
23798 cx,
23799 )
23800 })
23801 .unwrap()
23802 .await
23803 .downcast::<Editor>()
23804 .unwrap();
23805 pane_1.update(cx, |pane, cx| {
23806 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23807 open_editor.update(cx, |editor, cx| {
23808 assert_eq!(
23809 editor.display_text(cx),
23810 main_text,
23811 "Original main.rs text on initial open",
23812 );
23813 assert_eq!(
23814 editor
23815 .selections
23816 .all::<Point>(&editor.display_snapshot(cx))
23817 .into_iter()
23818 .map(|s| s.range())
23819 .collect::<Vec<_>>(),
23820 vec![Point::zero()..Point::zero()],
23821 "Default selections on initial open",
23822 );
23823 })
23824 });
23825 editor_1.update_in(cx, |editor, window, cx| {
23826 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23827 s.select_ranges(expected_ranges.clone());
23828 });
23829 });
23830
23831 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23832 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23833 });
23834 let editor_2 = workspace
23835 .update_in(cx, |workspace, window, cx| {
23836 workspace.open_path(
23837 (worktree_id, rel_path("main.rs")),
23838 Some(pane_2.downgrade()),
23839 true,
23840 window,
23841 cx,
23842 )
23843 })
23844 .unwrap()
23845 .await
23846 .downcast::<Editor>()
23847 .unwrap();
23848 pane_2.update(cx, |pane, cx| {
23849 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23850 open_editor.update(cx, |editor, cx| {
23851 assert_eq!(
23852 editor.display_text(cx),
23853 main_text,
23854 "Original main.rs text on initial open in another panel",
23855 );
23856 assert_eq!(
23857 editor
23858 .selections
23859 .all::<Point>(&editor.display_snapshot(cx))
23860 .into_iter()
23861 .map(|s| s.range())
23862 .collect::<Vec<_>>(),
23863 vec![Point::zero()..Point::zero()],
23864 "Default selections on initial open in another panel",
23865 );
23866 })
23867 });
23868
23869 editor_2.update_in(cx, |editor, window, cx| {
23870 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23871 });
23872
23873 let _other_editor_1 = workspace
23874 .update_in(cx, |workspace, window, cx| {
23875 workspace.open_path(
23876 (worktree_id, rel_path("lib.rs")),
23877 Some(pane_1.downgrade()),
23878 true,
23879 window,
23880 cx,
23881 )
23882 })
23883 .unwrap()
23884 .await
23885 .downcast::<Editor>()
23886 .unwrap();
23887 pane_1
23888 .update_in(cx, |pane, window, cx| {
23889 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23890 })
23891 .await
23892 .unwrap();
23893 drop(editor_1);
23894 pane_1.update(cx, |pane, cx| {
23895 pane.active_item()
23896 .unwrap()
23897 .downcast::<Editor>()
23898 .unwrap()
23899 .update(cx, |editor, cx| {
23900 assert_eq!(
23901 editor.display_text(cx),
23902 lib_text,
23903 "Other file should be open and active",
23904 );
23905 });
23906 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23907 });
23908
23909 let _other_editor_2 = workspace
23910 .update_in(cx, |workspace, window, cx| {
23911 workspace.open_path(
23912 (worktree_id, rel_path("lib.rs")),
23913 Some(pane_2.downgrade()),
23914 true,
23915 window,
23916 cx,
23917 )
23918 })
23919 .unwrap()
23920 .await
23921 .downcast::<Editor>()
23922 .unwrap();
23923 pane_2
23924 .update_in(cx, |pane, window, cx| {
23925 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23926 })
23927 .await
23928 .unwrap();
23929 drop(editor_2);
23930 pane_2.update(cx, |pane, cx| {
23931 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23932 open_editor.update(cx, |editor, cx| {
23933 assert_eq!(
23934 editor.display_text(cx),
23935 lib_text,
23936 "Other file should be open and active in another panel too",
23937 );
23938 });
23939 assert_eq!(
23940 pane.items().count(),
23941 1,
23942 "No other editors should be open in another pane",
23943 );
23944 });
23945
23946 let _editor_1_reopened = workspace
23947 .update_in(cx, |workspace, window, cx| {
23948 workspace.open_path(
23949 (worktree_id, rel_path("main.rs")),
23950 Some(pane_1.downgrade()),
23951 true,
23952 window,
23953 cx,
23954 )
23955 })
23956 .unwrap()
23957 .await
23958 .downcast::<Editor>()
23959 .unwrap();
23960 let _editor_2_reopened = workspace
23961 .update_in(cx, |workspace, window, cx| {
23962 workspace.open_path(
23963 (worktree_id, rel_path("main.rs")),
23964 Some(pane_2.downgrade()),
23965 true,
23966 window,
23967 cx,
23968 )
23969 })
23970 .unwrap()
23971 .await
23972 .downcast::<Editor>()
23973 .unwrap();
23974 pane_1.update(cx, |pane, cx| {
23975 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23976 open_editor.update(cx, |editor, cx| {
23977 assert_eq!(
23978 editor.display_text(cx),
23979 main_text,
23980 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23981 );
23982 assert_eq!(
23983 editor
23984 .selections
23985 .all::<Point>(&editor.display_snapshot(cx))
23986 .into_iter()
23987 .map(|s| s.range())
23988 .collect::<Vec<_>>(),
23989 expected_ranges,
23990 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23991 );
23992 })
23993 });
23994 pane_2.update(cx, |pane, cx| {
23995 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23996 open_editor.update(cx, |editor, cx| {
23997 assert_eq!(
23998 editor.display_text(cx),
23999 r#"fn main() {
24000⋯rintln!("1");
24001⋯intln!("2");
24002⋯ntln!("3");
24003println!("4");
24004println!("5");
24005}"#,
24006 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24007 );
24008 assert_eq!(
24009 editor
24010 .selections
24011 .all::<Point>(&editor.display_snapshot(cx))
24012 .into_iter()
24013 .map(|s| s.range())
24014 .collect::<Vec<_>>(),
24015 vec![Point::zero()..Point::zero()],
24016 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24017 );
24018 })
24019 });
24020}
24021
24022#[gpui::test]
24023async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24024 init_test(cx, |_| {});
24025
24026 let fs = FakeFs::new(cx.executor());
24027 let main_text = r#"fn main() {
24028println!("1");
24029println!("2");
24030println!("3");
24031println!("4");
24032println!("5");
24033}"#;
24034 let lib_text = "mod foo {}";
24035 fs.insert_tree(
24036 path!("/a"),
24037 json!({
24038 "lib.rs": lib_text,
24039 "main.rs": main_text,
24040 }),
24041 )
24042 .await;
24043
24044 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24045 let (workspace, cx) =
24046 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24047 let worktree_id = workspace.update(cx, |workspace, cx| {
24048 workspace.project().update(cx, |project, cx| {
24049 project.worktrees(cx).next().unwrap().read(cx).id()
24050 })
24051 });
24052
24053 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24054 let editor = workspace
24055 .update_in(cx, |workspace, window, cx| {
24056 workspace.open_path(
24057 (worktree_id, rel_path("main.rs")),
24058 Some(pane.downgrade()),
24059 true,
24060 window,
24061 cx,
24062 )
24063 })
24064 .unwrap()
24065 .await
24066 .downcast::<Editor>()
24067 .unwrap();
24068 pane.update(cx, |pane, cx| {
24069 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24070 open_editor.update(cx, |editor, cx| {
24071 assert_eq!(
24072 editor.display_text(cx),
24073 main_text,
24074 "Original main.rs text on initial open",
24075 );
24076 })
24077 });
24078 editor.update_in(cx, |editor, window, cx| {
24079 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24080 });
24081
24082 cx.update_global(|store: &mut SettingsStore, cx| {
24083 store.update_user_settings(cx, |s| {
24084 s.workspace.restore_on_file_reopen = Some(false);
24085 });
24086 });
24087 editor.update_in(cx, |editor, window, cx| {
24088 editor.fold_ranges(
24089 vec![
24090 Point::new(1, 0)..Point::new(1, 1),
24091 Point::new(2, 0)..Point::new(2, 2),
24092 Point::new(3, 0)..Point::new(3, 3),
24093 ],
24094 false,
24095 window,
24096 cx,
24097 );
24098 });
24099 pane.update_in(cx, |pane, window, cx| {
24100 pane.close_all_items(&CloseAllItems::default(), window, cx)
24101 })
24102 .await
24103 .unwrap();
24104 pane.update(cx, |pane, _| {
24105 assert!(pane.active_item().is_none());
24106 });
24107 cx.update_global(|store: &mut SettingsStore, cx| {
24108 store.update_user_settings(cx, |s| {
24109 s.workspace.restore_on_file_reopen = Some(true);
24110 });
24111 });
24112
24113 let _editor_reopened = workspace
24114 .update_in(cx, |workspace, window, cx| {
24115 workspace.open_path(
24116 (worktree_id, rel_path("main.rs")),
24117 Some(pane.downgrade()),
24118 true,
24119 window,
24120 cx,
24121 )
24122 })
24123 .unwrap()
24124 .await
24125 .downcast::<Editor>()
24126 .unwrap();
24127 pane.update(cx, |pane, cx| {
24128 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24129 open_editor.update(cx, |editor, cx| {
24130 assert_eq!(
24131 editor.display_text(cx),
24132 main_text,
24133 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24134 );
24135 })
24136 });
24137}
24138
24139#[gpui::test]
24140async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24141 struct EmptyModalView {
24142 focus_handle: gpui::FocusHandle,
24143 }
24144 impl EventEmitter<DismissEvent> for EmptyModalView {}
24145 impl Render for EmptyModalView {
24146 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24147 div()
24148 }
24149 }
24150 impl Focusable for EmptyModalView {
24151 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24152 self.focus_handle.clone()
24153 }
24154 }
24155 impl workspace::ModalView for EmptyModalView {}
24156 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24157 EmptyModalView {
24158 focus_handle: cx.focus_handle(),
24159 }
24160 }
24161
24162 init_test(cx, |_| {});
24163
24164 let fs = FakeFs::new(cx.executor());
24165 let project = Project::test(fs, [], cx).await;
24166 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24167 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24168 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24169 let editor = cx.new_window_entity(|window, cx| {
24170 Editor::new(
24171 EditorMode::full(),
24172 buffer,
24173 Some(project.clone()),
24174 window,
24175 cx,
24176 )
24177 });
24178 workspace
24179 .update(cx, |workspace, window, cx| {
24180 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24181 })
24182 .unwrap();
24183 editor.update_in(cx, |editor, window, cx| {
24184 editor.open_context_menu(&OpenContextMenu, window, cx);
24185 assert!(editor.mouse_context_menu.is_some());
24186 });
24187 workspace
24188 .update(cx, |workspace, window, cx| {
24189 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24190 })
24191 .unwrap();
24192 cx.read(|cx| {
24193 assert!(editor.read(cx).mouse_context_menu.is_none());
24194 });
24195}
24196
24197fn set_linked_edit_ranges(
24198 opening: (Point, Point),
24199 closing: (Point, Point),
24200 editor: &mut Editor,
24201 cx: &mut Context<Editor>,
24202) {
24203 let Some((buffer, _)) = editor
24204 .buffer
24205 .read(cx)
24206 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24207 else {
24208 panic!("Failed to get buffer for selection position");
24209 };
24210 let buffer = buffer.read(cx);
24211 let buffer_id = buffer.remote_id();
24212 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24213 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24214 let mut linked_ranges = HashMap::default();
24215 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24216 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24217}
24218
24219#[gpui::test]
24220async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24221 init_test(cx, |_| {});
24222
24223 let fs = FakeFs::new(cx.executor());
24224 fs.insert_file(path!("/file.html"), Default::default())
24225 .await;
24226
24227 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24228
24229 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24230 let html_language = Arc::new(Language::new(
24231 LanguageConfig {
24232 name: "HTML".into(),
24233 matcher: LanguageMatcher {
24234 path_suffixes: vec!["html".to_string()],
24235 ..LanguageMatcher::default()
24236 },
24237 brackets: BracketPairConfig {
24238 pairs: vec![BracketPair {
24239 start: "<".into(),
24240 end: ">".into(),
24241 close: true,
24242 ..Default::default()
24243 }],
24244 ..Default::default()
24245 },
24246 ..Default::default()
24247 },
24248 Some(tree_sitter_html::LANGUAGE.into()),
24249 ));
24250 language_registry.add(html_language);
24251 let mut fake_servers = language_registry.register_fake_lsp(
24252 "HTML",
24253 FakeLspAdapter {
24254 capabilities: lsp::ServerCapabilities {
24255 completion_provider: Some(lsp::CompletionOptions {
24256 resolve_provider: Some(true),
24257 ..Default::default()
24258 }),
24259 ..Default::default()
24260 },
24261 ..Default::default()
24262 },
24263 );
24264
24265 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24266 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24267
24268 let worktree_id = workspace
24269 .update(cx, |workspace, _window, cx| {
24270 workspace.project().update(cx, |project, cx| {
24271 project.worktrees(cx).next().unwrap().read(cx).id()
24272 })
24273 })
24274 .unwrap();
24275 project
24276 .update(cx, |project, cx| {
24277 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24278 })
24279 .await
24280 .unwrap();
24281 let editor = workspace
24282 .update(cx, |workspace, window, cx| {
24283 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24284 })
24285 .unwrap()
24286 .await
24287 .unwrap()
24288 .downcast::<Editor>()
24289 .unwrap();
24290
24291 let fake_server = fake_servers.next().await.unwrap();
24292 editor.update_in(cx, |editor, window, cx| {
24293 editor.set_text("<ad></ad>", window, cx);
24294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24295 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24296 });
24297 set_linked_edit_ranges(
24298 (Point::new(0, 1), Point::new(0, 3)),
24299 (Point::new(0, 6), Point::new(0, 8)),
24300 editor,
24301 cx,
24302 );
24303 });
24304 let mut completion_handle =
24305 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24306 Ok(Some(lsp::CompletionResponse::Array(vec![
24307 lsp::CompletionItem {
24308 label: "head".to_string(),
24309 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24310 lsp::InsertReplaceEdit {
24311 new_text: "head".to_string(),
24312 insert: lsp::Range::new(
24313 lsp::Position::new(0, 1),
24314 lsp::Position::new(0, 3),
24315 ),
24316 replace: lsp::Range::new(
24317 lsp::Position::new(0, 1),
24318 lsp::Position::new(0, 3),
24319 ),
24320 },
24321 )),
24322 ..Default::default()
24323 },
24324 ])))
24325 });
24326 editor.update_in(cx, |editor, window, cx| {
24327 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24328 });
24329 cx.run_until_parked();
24330 completion_handle.next().await.unwrap();
24331 editor.update(cx, |editor, _| {
24332 assert!(
24333 editor.context_menu_visible(),
24334 "Completion menu should be visible"
24335 );
24336 });
24337 editor.update_in(cx, |editor, window, cx| {
24338 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24339 });
24340 cx.executor().run_until_parked();
24341 editor.update(cx, |editor, cx| {
24342 assert_eq!(editor.text(cx), "<head></head>");
24343 });
24344}
24345
24346#[gpui::test]
24347async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24348 init_test(cx, |_| {});
24349
24350 let mut cx = EditorTestContext::new(cx).await;
24351 let language = Arc::new(Language::new(
24352 LanguageConfig {
24353 name: "TSX".into(),
24354 matcher: LanguageMatcher {
24355 path_suffixes: vec!["tsx".to_string()],
24356 ..LanguageMatcher::default()
24357 },
24358 brackets: BracketPairConfig {
24359 pairs: vec![BracketPair {
24360 start: "<".into(),
24361 end: ">".into(),
24362 close: true,
24363 ..Default::default()
24364 }],
24365 ..Default::default()
24366 },
24367 linked_edit_characters: HashSet::from_iter(['.']),
24368 ..Default::default()
24369 },
24370 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24371 ));
24372 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24373
24374 // Test typing > does not extend linked pair
24375 cx.set_state("<divˇ<div></div>");
24376 cx.update_editor(|editor, _, cx| {
24377 set_linked_edit_ranges(
24378 (Point::new(0, 1), Point::new(0, 4)),
24379 (Point::new(0, 11), Point::new(0, 14)),
24380 editor,
24381 cx,
24382 );
24383 });
24384 cx.update_editor(|editor, window, cx| {
24385 editor.handle_input(">", window, cx);
24386 });
24387 cx.assert_editor_state("<div>ˇ<div></div>");
24388
24389 // Test typing . do extend linked pair
24390 cx.set_state("<Animatedˇ></Animated>");
24391 cx.update_editor(|editor, _, cx| {
24392 set_linked_edit_ranges(
24393 (Point::new(0, 1), Point::new(0, 9)),
24394 (Point::new(0, 12), Point::new(0, 20)),
24395 editor,
24396 cx,
24397 );
24398 });
24399 cx.update_editor(|editor, window, cx| {
24400 editor.handle_input(".", window, cx);
24401 });
24402 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24403 cx.update_editor(|editor, _, cx| {
24404 set_linked_edit_ranges(
24405 (Point::new(0, 1), Point::new(0, 10)),
24406 (Point::new(0, 13), Point::new(0, 21)),
24407 editor,
24408 cx,
24409 );
24410 });
24411 cx.update_editor(|editor, window, cx| {
24412 editor.handle_input("V", window, cx);
24413 });
24414 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24415}
24416
24417#[gpui::test]
24418async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24419 init_test(cx, |_| {});
24420
24421 let fs = FakeFs::new(cx.executor());
24422 fs.insert_tree(
24423 path!("/root"),
24424 json!({
24425 "a": {
24426 "main.rs": "fn main() {}",
24427 },
24428 "foo": {
24429 "bar": {
24430 "external_file.rs": "pub mod external {}",
24431 }
24432 }
24433 }),
24434 )
24435 .await;
24436
24437 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24438 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24439 language_registry.add(rust_lang());
24440 let _fake_servers = language_registry.register_fake_lsp(
24441 "Rust",
24442 FakeLspAdapter {
24443 ..FakeLspAdapter::default()
24444 },
24445 );
24446 let (workspace, cx) =
24447 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24448 let worktree_id = workspace.update(cx, |workspace, cx| {
24449 workspace.project().update(cx, |project, cx| {
24450 project.worktrees(cx).next().unwrap().read(cx).id()
24451 })
24452 });
24453
24454 let assert_language_servers_count =
24455 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24456 project.update(cx, |project, cx| {
24457 let current = project
24458 .lsp_store()
24459 .read(cx)
24460 .as_local()
24461 .unwrap()
24462 .language_servers
24463 .len();
24464 assert_eq!(expected, current, "{context}");
24465 });
24466 };
24467
24468 assert_language_servers_count(
24469 0,
24470 "No servers should be running before any file is open",
24471 cx,
24472 );
24473 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24474 let main_editor = workspace
24475 .update_in(cx, |workspace, window, cx| {
24476 workspace.open_path(
24477 (worktree_id, rel_path("main.rs")),
24478 Some(pane.downgrade()),
24479 true,
24480 window,
24481 cx,
24482 )
24483 })
24484 .unwrap()
24485 .await
24486 .downcast::<Editor>()
24487 .unwrap();
24488 pane.update(cx, |pane, cx| {
24489 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24490 open_editor.update(cx, |editor, cx| {
24491 assert_eq!(
24492 editor.display_text(cx),
24493 "fn main() {}",
24494 "Original main.rs text on initial open",
24495 );
24496 });
24497 assert_eq!(open_editor, main_editor);
24498 });
24499 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24500
24501 let external_editor = workspace
24502 .update_in(cx, |workspace, window, cx| {
24503 workspace.open_abs_path(
24504 PathBuf::from("/root/foo/bar/external_file.rs"),
24505 OpenOptions::default(),
24506 window,
24507 cx,
24508 )
24509 })
24510 .await
24511 .expect("opening external file")
24512 .downcast::<Editor>()
24513 .expect("downcasted external file's open element to editor");
24514 pane.update(cx, |pane, cx| {
24515 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24516 open_editor.update(cx, |editor, cx| {
24517 assert_eq!(
24518 editor.display_text(cx),
24519 "pub mod external {}",
24520 "External file is open now",
24521 );
24522 });
24523 assert_eq!(open_editor, external_editor);
24524 });
24525 assert_language_servers_count(
24526 1,
24527 "Second, external, *.rs file should join the existing server",
24528 cx,
24529 );
24530
24531 pane.update_in(cx, |pane, window, cx| {
24532 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24533 })
24534 .await
24535 .unwrap();
24536 pane.update_in(cx, |pane, window, cx| {
24537 pane.navigate_backward(&Default::default(), window, cx);
24538 });
24539 cx.run_until_parked();
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 "pub mod external {}",
24546 "External file is open now",
24547 );
24548 });
24549 });
24550 assert_language_servers_count(
24551 1,
24552 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24553 cx,
24554 );
24555
24556 cx.update(|_, cx| {
24557 workspace::reload(cx);
24558 });
24559 assert_language_servers_count(
24560 1,
24561 "After reloading the worktree with local and external files opened, only one project should be started",
24562 cx,
24563 );
24564}
24565
24566#[gpui::test]
24567async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24568 init_test(cx, |_| {});
24569
24570 let mut cx = EditorTestContext::new(cx).await;
24571 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24572 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24573
24574 // test cursor move to start of each line on tab
24575 // for `if`, `elif`, `else`, `while`, `with` and `for`
24576 cx.set_state(indoc! {"
24577 def main():
24578 ˇ for item in items:
24579 ˇ while item.active:
24580 ˇ if item.value > 10:
24581 ˇ continue
24582 ˇ elif item.value < 0:
24583 ˇ break
24584 ˇ else:
24585 ˇ with item.context() as ctx:
24586 ˇ yield count
24587 ˇ else:
24588 ˇ log('while else')
24589 ˇ else:
24590 ˇ log('for else')
24591 "});
24592 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24593 cx.assert_editor_state(indoc! {"
24594 def main():
24595 ˇfor item in items:
24596 ˇwhile item.active:
24597 ˇif item.value > 10:
24598 ˇcontinue
24599 ˇelif item.value < 0:
24600 ˇbreak
24601 ˇelse:
24602 ˇwith item.context() as ctx:
24603 ˇyield count
24604 ˇelse:
24605 ˇlog('while else')
24606 ˇelse:
24607 ˇlog('for else')
24608 "});
24609 // test relative indent is preserved when tab
24610 // for `if`, `elif`, `else`, `while`, `with` and `for`
24611 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24612 cx.assert_editor_state(indoc! {"
24613 def main():
24614 ˇfor item in items:
24615 ˇwhile item.active:
24616 ˇif item.value > 10:
24617 ˇcontinue
24618 ˇelif item.value < 0:
24619 ˇbreak
24620 ˇelse:
24621 ˇwith item.context() as ctx:
24622 ˇyield count
24623 ˇelse:
24624 ˇlog('while else')
24625 ˇelse:
24626 ˇlog('for else')
24627 "});
24628
24629 // test cursor move to start of each line on tab
24630 // for `try`, `except`, `else`, `finally`, `match` and `def`
24631 cx.set_state(indoc! {"
24632 def main():
24633 ˇ try:
24634 ˇ fetch()
24635 ˇ except ValueError:
24636 ˇ handle_error()
24637 ˇ else:
24638 ˇ match value:
24639 ˇ case _:
24640 ˇ finally:
24641 ˇ def status():
24642 ˇ return 0
24643 "});
24644 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24645 cx.assert_editor_state(indoc! {"
24646 def main():
24647 ˇtry:
24648 ˇfetch()
24649 ˇexcept ValueError:
24650 ˇhandle_error()
24651 ˇelse:
24652 ˇmatch value:
24653 ˇcase _:
24654 ˇfinally:
24655 ˇdef status():
24656 ˇreturn 0
24657 "});
24658 // test relative indent is preserved when tab
24659 // for `try`, `except`, `else`, `finally`, `match` and `def`
24660 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24661 cx.assert_editor_state(indoc! {"
24662 def main():
24663 ˇtry:
24664 ˇfetch()
24665 ˇexcept ValueError:
24666 ˇhandle_error()
24667 ˇelse:
24668 ˇmatch value:
24669 ˇcase _:
24670 ˇfinally:
24671 ˇdef status():
24672 ˇreturn 0
24673 "});
24674}
24675
24676#[gpui::test]
24677async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24678 init_test(cx, |_| {});
24679
24680 let mut cx = EditorTestContext::new(cx).await;
24681 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24682 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24683
24684 // test `else` auto outdents when typed inside `if` block
24685 cx.set_state(indoc! {"
24686 def main():
24687 if i == 2:
24688 return
24689 ˇ
24690 "});
24691 cx.update_editor(|editor, window, cx| {
24692 editor.handle_input("else:", window, cx);
24693 });
24694 cx.assert_editor_state(indoc! {"
24695 def main():
24696 if i == 2:
24697 return
24698 else:ˇ
24699 "});
24700
24701 // test `except` auto outdents when typed inside `try` block
24702 cx.set_state(indoc! {"
24703 def main():
24704 try:
24705 i = 2
24706 ˇ
24707 "});
24708 cx.update_editor(|editor, window, cx| {
24709 editor.handle_input("except:", window, cx);
24710 });
24711 cx.assert_editor_state(indoc! {"
24712 def main():
24713 try:
24714 i = 2
24715 except:ˇ
24716 "});
24717
24718 // test `else` auto outdents when typed inside `except` block
24719 cx.set_state(indoc! {"
24720 def main():
24721 try:
24722 i = 2
24723 except:
24724 j = 2
24725 ˇ
24726 "});
24727 cx.update_editor(|editor, window, cx| {
24728 editor.handle_input("else:", window, cx);
24729 });
24730 cx.assert_editor_state(indoc! {"
24731 def main():
24732 try:
24733 i = 2
24734 except:
24735 j = 2
24736 else:ˇ
24737 "});
24738
24739 // test `finally` auto outdents when typed inside `else` block
24740 cx.set_state(indoc! {"
24741 def main():
24742 try:
24743 i = 2
24744 except:
24745 j = 2
24746 else:
24747 k = 2
24748 ˇ
24749 "});
24750 cx.update_editor(|editor, window, cx| {
24751 editor.handle_input("finally:", window, cx);
24752 });
24753 cx.assert_editor_state(indoc! {"
24754 def main():
24755 try:
24756 i = 2
24757 except:
24758 j = 2
24759 else:
24760 k = 2
24761 finally:ˇ
24762 "});
24763
24764 // test `else` does not outdents when typed inside `except` block right after for block
24765 cx.set_state(indoc! {"
24766 def main():
24767 try:
24768 i = 2
24769 except:
24770 for i in range(n):
24771 pass
24772 ˇ
24773 "});
24774 cx.update_editor(|editor, window, cx| {
24775 editor.handle_input("else:", window, cx);
24776 });
24777 cx.assert_editor_state(indoc! {"
24778 def main():
24779 try:
24780 i = 2
24781 except:
24782 for i in range(n):
24783 pass
24784 else:ˇ
24785 "});
24786
24787 // test `finally` auto outdents when typed inside `else` block right after for block
24788 cx.set_state(indoc! {"
24789 def main():
24790 try:
24791 i = 2
24792 except:
24793 j = 2
24794 else:
24795 for i in range(n):
24796 pass
24797 ˇ
24798 "});
24799 cx.update_editor(|editor, window, cx| {
24800 editor.handle_input("finally:", window, cx);
24801 });
24802 cx.assert_editor_state(indoc! {"
24803 def main():
24804 try:
24805 i = 2
24806 except:
24807 j = 2
24808 else:
24809 for i in range(n):
24810 pass
24811 finally:ˇ
24812 "});
24813
24814 // test `except` outdents to inner "try" block
24815 cx.set_state(indoc! {"
24816 def main():
24817 try:
24818 i = 2
24819 if i == 2:
24820 try:
24821 i = 3
24822 ˇ
24823 "});
24824 cx.update_editor(|editor, window, cx| {
24825 editor.handle_input("except:", window, cx);
24826 });
24827 cx.assert_editor_state(indoc! {"
24828 def main():
24829 try:
24830 i = 2
24831 if i == 2:
24832 try:
24833 i = 3
24834 except:ˇ
24835 "});
24836
24837 // test `except` outdents to outer "try" block
24838 cx.set_state(indoc! {"
24839 def main():
24840 try:
24841 i = 2
24842 if i == 2:
24843 try:
24844 i = 3
24845 ˇ
24846 "});
24847 cx.update_editor(|editor, window, cx| {
24848 editor.handle_input("except:", window, cx);
24849 });
24850 cx.assert_editor_state(indoc! {"
24851 def main():
24852 try:
24853 i = 2
24854 if i == 2:
24855 try:
24856 i = 3
24857 except:ˇ
24858 "});
24859
24860 // test `else` stays at correct indent when typed after `for` block
24861 cx.set_state(indoc! {"
24862 def main():
24863 for i in range(10):
24864 if i == 3:
24865 break
24866 ˇ
24867 "});
24868 cx.update_editor(|editor, window, cx| {
24869 editor.handle_input("else:", window, cx);
24870 });
24871 cx.assert_editor_state(indoc! {"
24872 def main():
24873 for i in range(10):
24874 if i == 3:
24875 break
24876 else:ˇ
24877 "});
24878
24879 // test does not outdent on typing after line with square brackets
24880 cx.set_state(indoc! {"
24881 def f() -> list[str]:
24882 ˇ
24883 "});
24884 cx.update_editor(|editor, window, cx| {
24885 editor.handle_input("a", window, cx);
24886 });
24887 cx.assert_editor_state(indoc! {"
24888 def f() -> list[str]:
24889 aˇ
24890 "});
24891
24892 // test does not outdent on typing : after case keyword
24893 cx.set_state(indoc! {"
24894 match 1:
24895 caseˇ
24896 "});
24897 cx.update_editor(|editor, window, cx| {
24898 editor.handle_input(":", window, cx);
24899 });
24900 cx.assert_editor_state(indoc! {"
24901 match 1:
24902 case:ˇ
24903 "});
24904}
24905
24906#[gpui::test]
24907async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24908 init_test(cx, |_| {});
24909 update_test_language_settings(cx, |settings| {
24910 settings.defaults.extend_comment_on_newline = Some(false);
24911 });
24912 let mut cx = EditorTestContext::new(cx).await;
24913 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24914 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24915
24916 // test correct indent after newline on comment
24917 cx.set_state(indoc! {"
24918 # COMMENT:ˇ
24919 "});
24920 cx.update_editor(|editor, window, cx| {
24921 editor.newline(&Newline, window, cx);
24922 });
24923 cx.assert_editor_state(indoc! {"
24924 # COMMENT:
24925 ˇ
24926 "});
24927
24928 // test correct indent after newline in brackets
24929 cx.set_state(indoc! {"
24930 {ˇ}
24931 "});
24932 cx.update_editor(|editor, window, cx| {
24933 editor.newline(&Newline, window, cx);
24934 });
24935 cx.run_until_parked();
24936 cx.assert_editor_state(indoc! {"
24937 {
24938 ˇ
24939 }
24940 "});
24941
24942 cx.set_state(indoc! {"
24943 (ˇ)
24944 "});
24945 cx.update_editor(|editor, window, cx| {
24946 editor.newline(&Newline, window, cx);
24947 });
24948 cx.run_until_parked();
24949 cx.assert_editor_state(indoc! {"
24950 (
24951 ˇ
24952 )
24953 "});
24954
24955 // do not indent after empty lists or dictionaries
24956 cx.set_state(indoc! {"
24957 a = []ˇ
24958 "});
24959 cx.update_editor(|editor, window, cx| {
24960 editor.newline(&Newline, window, cx);
24961 });
24962 cx.run_until_parked();
24963 cx.assert_editor_state(indoc! {"
24964 a = []
24965 ˇ
24966 "});
24967}
24968
24969#[gpui::test]
24970async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24971 init_test(cx, |_| {});
24972
24973 let mut cx = EditorTestContext::new(cx).await;
24974 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24976
24977 // test cursor move to start of each line on tab
24978 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24979 cx.set_state(indoc! {"
24980 function main() {
24981 ˇ for item in $items; do
24982 ˇ while [ -n \"$item\" ]; do
24983 ˇ if [ \"$value\" -gt 10 ]; then
24984 ˇ continue
24985 ˇ elif [ \"$value\" -lt 0 ]; then
24986 ˇ break
24987 ˇ else
24988 ˇ echo \"$item\"
24989 ˇ fi
24990 ˇ done
24991 ˇ done
24992 ˇ}
24993 "});
24994 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24995 cx.assert_editor_state(indoc! {"
24996 function main() {
24997 ˇfor item in $items; do
24998 ˇwhile [ -n \"$item\" ]; do
24999 ˇif [ \"$value\" -gt 10 ]; then
25000 ˇcontinue
25001 ˇelif [ \"$value\" -lt 0 ]; then
25002 ˇbreak
25003 ˇelse
25004 ˇecho \"$item\"
25005 ˇfi
25006 ˇdone
25007 ˇdone
25008 ˇ}
25009 "});
25010 // test relative indent is preserved when tab
25011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25012 cx.assert_editor_state(indoc! {"
25013 function main() {
25014 ˇfor item in $items; do
25015 ˇwhile [ -n \"$item\" ]; do
25016 ˇif [ \"$value\" -gt 10 ]; then
25017 ˇcontinue
25018 ˇelif [ \"$value\" -lt 0 ]; then
25019 ˇbreak
25020 ˇelse
25021 ˇecho \"$item\"
25022 ˇfi
25023 ˇdone
25024 ˇdone
25025 ˇ}
25026 "});
25027
25028 // test cursor move to start of each line on tab
25029 // for `case` statement with patterns
25030 cx.set_state(indoc! {"
25031 function handle() {
25032 ˇ case \"$1\" in
25033 ˇ start)
25034 ˇ echo \"a\"
25035 ˇ ;;
25036 ˇ stop)
25037 ˇ echo \"b\"
25038 ˇ ;;
25039 ˇ *)
25040 ˇ echo \"c\"
25041 ˇ ;;
25042 ˇ esac
25043 ˇ}
25044 "});
25045 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25046 cx.assert_editor_state(indoc! {"
25047 function handle() {
25048 ˇcase \"$1\" in
25049 ˇstart)
25050 ˇecho \"a\"
25051 ˇ;;
25052 ˇstop)
25053 ˇecho \"b\"
25054 ˇ;;
25055 ˇ*)
25056 ˇecho \"c\"
25057 ˇ;;
25058 ˇesac
25059 ˇ}
25060 "});
25061}
25062
25063#[gpui::test]
25064async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25065 init_test(cx, |_| {});
25066
25067 let mut cx = EditorTestContext::new(cx).await;
25068 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25069 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25070
25071 // test indents on comment insert
25072 cx.set_state(indoc! {"
25073 function main() {
25074 ˇ for item in $items; do
25075 ˇ while [ -n \"$item\" ]; do
25076 ˇ if [ \"$value\" -gt 10 ]; then
25077 ˇ continue
25078 ˇ elif [ \"$value\" -lt 0 ]; then
25079 ˇ break
25080 ˇ else
25081 ˇ echo \"$item\"
25082 ˇ fi
25083 ˇ done
25084 ˇ done
25085 ˇ}
25086 "});
25087 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25088 cx.assert_editor_state(indoc! {"
25089 function main() {
25090 #ˇ for item in $items; do
25091 #ˇ while [ -n \"$item\" ]; do
25092 #ˇ if [ \"$value\" -gt 10 ]; then
25093 #ˇ continue
25094 #ˇ elif [ \"$value\" -lt 0 ]; then
25095 #ˇ break
25096 #ˇ else
25097 #ˇ echo \"$item\"
25098 #ˇ fi
25099 #ˇ done
25100 #ˇ done
25101 #ˇ}
25102 "});
25103}
25104
25105#[gpui::test]
25106async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25107 init_test(cx, |_| {});
25108
25109 let mut cx = EditorTestContext::new(cx).await;
25110 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25111 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25112
25113 // test `else` auto outdents when typed inside `if` block
25114 cx.set_state(indoc! {"
25115 if [ \"$1\" = \"test\" ]; then
25116 echo \"foo bar\"
25117 ˇ
25118 "});
25119 cx.update_editor(|editor, window, cx| {
25120 editor.handle_input("else", window, cx);
25121 });
25122 cx.assert_editor_state(indoc! {"
25123 if [ \"$1\" = \"test\" ]; then
25124 echo \"foo bar\"
25125 elseˇ
25126 "});
25127
25128 // test `elif` auto outdents when typed inside `if` block
25129 cx.set_state(indoc! {"
25130 if [ \"$1\" = \"test\" ]; then
25131 echo \"foo bar\"
25132 ˇ
25133 "});
25134 cx.update_editor(|editor, window, cx| {
25135 editor.handle_input("elif", window, cx);
25136 });
25137 cx.assert_editor_state(indoc! {"
25138 if [ \"$1\" = \"test\" ]; then
25139 echo \"foo bar\"
25140 elifˇ
25141 "});
25142
25143 // test `fi` auto outdents when typed inside `else` block
25144 cx.set_state(indoc! {"
25145 if [ \"$1\" = \"test\" ]; then
25146 echo \"foo bar\"
25147 else
25148 echo \"bar baz\"
25149 ˇ
25150 "});
25151 cx.update_editor(|editor, window, cx| {
25152 editor.handle_input("fi", window, cx);
25153 });
25154 cx.assert_editor_state(indoc! {"
25155 if [ \"$1\" = \"test\" ]; then
25156 echo \"foo bar\"
25157 else
25158 echo \"bar baz\"
25159 fiˇ
25160 "});
25161
25162 // test `done` auto outdents when typed inside `while` block
25163 cx.set_state(indoc! {"
25164 while read line; do
25165 echo \"$line\"
25166 ˇ
25167 "});
25168 cx.update_editor(|editor, window, cx| {
25169 editor.handle_input("done", window, cx);
25170 });
25171 cx.assert_editor_state(indoc! {"
25172 while read line; do
25173 echo \"$line\"
25174 doneˇ
25175 "});
25176
25177 // test `done` auto outdents when typed inside `for` block
25178 cx.set_state(indoc! {"
25179 for file in *.txt; do
25180 cat \"$file\"
25181 ˇ
25182 "});
25183 cx.update_editor(|editor, window, cx| {
25184 editor.handle_input("done", window, cx);
25185 });
25186 cx.assert_editor_state(indoc! {"
25187 for file in *.txt; do
25188 cat \"$file\"
25189 doneˇ
25190 "});
25191
25192 // test `esac` auto outdents when typed inside `case` block
25193 cx.set_state(indoc! {"
25194 case \"$1\" in
25195 start)
25196 echo \"foo bar\"
25197 ;;
25198 stop)
25199 echo \"bar baz\"
25200 ;;
25201 ˇ
25202 "});
25203 cx.update_editor(|editor, window, cx| {
25204 editor.handle_input("esac", window, cx);
25205 });
25206 cx.assert_editor_state(indoc! {"
25207 case \"$1\" in
25208 start)
25209 echo \"foo bar\"
25210 ;;
25211 stop)
25212 echo \"bar baz\"
25213 ;;
25214 esacˇ
25215 "});
25216
25217 // test `*)` auto outdents when typed inside `case` block
25218 cx.set_state(indoc! {"
25219 case \"$1\" in
25220 start)
25221 echo \"foo bar\"
25222 ;;
25223 ˇ
25224 "});
25225 cx.update_editor(|editor, window, cx| {
25226 editor.handle_input("*)", window, cx);
25227 });
25228 cx.assert_editor_state(indoc! {"
25229 case \"$1\" in
25230 start)
25231 echo \"foo bar\"
25232 ;;
25233 *)ˇ
25234 "});
25235
25236 // test `fi` outdents to correct level with nested if blocks
25237 cx.set_state(indoc! {"
25238 if [ \"$1\" = \"test\" ]; then
25239 echo \"outer if\"
25240 if [ \"$2\" = \"debug\" ]; then
25241 echo \"inner if\"
25242 ˇ
25243 "});
25244 cx.update_editor(|editor, window, cx| {
25245 editor.handle_input("fi", window, cx);
25246 });
25247 cx.assert_editor_state(indoc! {"
25248 if [ \"$1\" = \"test\" ]; then
25249 echo \"outer if\"
25250 if [ \"$2\" = \"debug\" ]; then
25251 echo \"inner if\"
25252 fiˇ
25253 "});
25254}
25255
25256#[gpui::test]
25257async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25258 init_test(cx, |_| {});
25259 update_test_language_settings(cx, |settings| {
25260 settings.defaults.extend_comment_on_newline = Some(false);
25261 });
25262 let mut cx = EditorTestContext::new(cx).await;
25263 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25264 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25265
25266 // test correct indent after newline on comment
25267 cx.set_state(indoc! {"
25268 # COMMENT:ˇ
25269 "});
25270 cx.update_editor(|editor, window, cx| {
25271 editor.newline(&Newline, window, cx);
25272 });
25273 cx.assert_editor_state(indoc! {"
25274 # COMMENT:
25275 ˇ
25276 "});
25277
25278 // test correct indent after newline after `then`
25279 cx.set_state(indoc! {"
25280
25281 if [ \"$1\" = \"test\" ]; thenˇ
25282 "});
25283 cx.update_editor(|editor, window, cx| {
25284 editor.newline(&Newline, window, cx);
25285 });
25286 cx.run_until_parked();
25287 cx.assert_editor_state(indoc! {"
25288
25289 if [ \"$1\" = \"test\" ]; then
25290 ˇ
25291 "});
25292
25293 // test correct indent after newline after `else`
25294 cx.set_state(indoc! {"
25295 if [ \"$1\" = \"test\" ]; then
25296 elseˇ
25297 "});
25298 cx.update_editor(|editor, window, cx| {
25299 editor.newline(&Newline, window, cx);
25300 });
25301 cx.run_until_parked();
25302 cx.assert_editor_state(indoc! {"
25303 if [ \"$1\" = \"test\" ]; then
25304 else
25305 ˇ
25306 "});
25307
25308 // test correct indent after newline after `elif`
25309 cx.set_state(indoc! {"
25310 if [ \"$1\" = \"test\" ]; then
25311 elifˇ
25312 "});
25313 cx.update_editor(|editor, window, cx| {
25314 editor.newline(&Newline, window, cx);
25315 });
25316 cx.run_until_parked();
25317 cx.assert_editor_state(indoc! {"
25318 if [ \"$1\" = \"test\" ]; then
25319 elif
25320 ˇ
25321 "});
25322
25323 // test correct indent after newline after `do`
25324 cx.set_state(indoc! {"
25325 for file in *.txt; doˇ
25326 "});
25327 cx.update_editor(|editor, window, cx| {
25328 editor.newline(&Newline, window, cx);
25329 });
25330 cx.run_until_parked();
25331 cx.assert_editor_state(indoc! {"
25332 for file in *.txt; do
25333 ˇ
25334 "});
25335
25336 // test correct indent after newline after case pattern
25337 cx.set_state(indoc! {"
25338 case \"$1\" in
25339 start)ˇ
25340 "});
25341 cx.update_editor(|editor, window, cx| {
25342 editor.newline(&Newline, window, cx);
25343 });
25344 cx.run_until_parked();
25345 cx.assert_editor_state(indoc! {"
25346 case \"$1\" in
25347 start)
25348 ˇ
25349 "});
25350
25351 // test correct indent after newline after case pattern
25352 cx.set_state(indoc! {"
25353 case \"$1\" in
25354 start)
25355 ;;
25356 *)ˇ
25357 "});
25358 cx.update_editor(|editor, window, cx| {
25359 editor.newline(&Newline, window, cx);
25360 });
25361 cx.run_until_parked();
25362 cx.assert_editor_state(indoc! {"
25363 case \"$1\" in
25364 start)
25365 ;;
25366 *)
25367 ˇ
25368 "});
25369
25370 // test correct indent after newline after function opening brace
25371 cx.set_state(indoc! {"
25372 function test() {ˇ}
25373 "});
25374 cx.update_editor(|editor, window, cx| {
25375 editor.newline(&Newline, window, cx);
25376 });
25377 cx.run_until_parked();
25378 cx.assert_editor_state(indoc! {"
25379 function test() {
25380 ˇ
25381 }
25382 "});
25383
25384 // test no extra indent after semicolon on same line
25385 cx.set_state(indoc! {"
25386 echo \"test\";ˇ
25387 "});
25388 cx.update_editor(|editor, window, cx| {
25389 editor.newline(&Newline, window, cx);
25390 });
25391 cx.run_until_parked();
25392 cx.assert_editor_state(indoc! {"
25393 echo \"test\";
25394 ˇ
25395 "});
25396}
25397
25398fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25399 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25400 point..point
25401}
25402
25403#[track_caller]
25404fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25405 let (text, ranges) = marked_text_ranges(marked_text, true);
25406 assert_eq!(editor.text(cx), text);
25407 assert_eq!(
25408 editor.selections.ranges(&editor.display_snapshot(cx)),
25409 ranges,
25410 "Assert selections are {}",
25411 marked_text
25412 );
25413}
25414
25415pub fn handle_signature_help_request(
25416 cx: &mut EditorLspTestContext,
25417 mocked_response: lsp::SignatureHelp,
25418) -> impl Future<Output = ()> + use<> {
25419 let mut request =
25420 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25421 let mocked_response = mocked_response.clone();
25422 async move { Ok(Some(mocked_response)) }
25423 });
25424
25425 async move {
25426 request.next().await;
25427 }
25428}
25429
25430#[track_caller]
25431pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25432 cx.update_editor(|editor, _, _| {
25433 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25434 let entries = menu.entries.borrow();
25435 let entries = entries
25436 .iter()
25437 .map(|entry| entry.string.as_str())
25438 .collect::<Vec<_>>();
25439 assert_eq!(entries, expected);
25440 } else {
25441 panic!("Expected completions menu");
25442 }
25443 });
25444}
25445
25446/// Handle completion request passing a marked string specifying where the completion
25447/// should be triggered from using '|' character, what range should be replaced, and what completions
25448/// should be returned using '<' and '>' to delimit the range.
25449///
25450/// Also see `handle_completion_request_with_insert_and_replace`.
25451#[track_caller]
25452pub fn handle_completion_request(
25453 marked_string: &str,
25454 completions: Vec<&'static str>,
25455 is_incomplete: bool,
25456 counter: Arc<AtomicUsize>,
25457 cx: &mut EditorLspTestContext,
25458) -> impl Future<Output = ()> {
25459 let complete_from_marker: TextRangeMarker = '|'.into();
25460 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25461 let (_, mut marked_ranges) = marked_text_ranges_by(
25462 marked_string,
25463 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25464 );
25465
25466 let complete_from_position =
25467 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25468 let replace_range =
25469 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25470
25471 let mut request =
25472 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25473 let completions = completions.clone();
25474 counter.fetch_add(1, atomic::Ordering::Release);
25475 async move {
25476 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25477 assert_eq!(
25478 params.text_document_position.position,
25479 complete_from_position
25480 );
25481 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25482 is_incomplete,
25483 item_defaults: None,
25484 items: completions
25485 .iter()
25486 .map(|completion_text| lsp::CompletionItem {
25487 label: completion_text.to_string(),
25488 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25489 range: replace_range,
25490 new_text: completion_text.to_string(),
25491 })),
25492 ..Default::default()
25493 })
25494 .collect(),
25495 })))
25496 }
25497 });
25498
25499 async move {
25500 request.next().await;
25501 }
25502}
25503
25504/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25505/// given instead, which also contains an `insert` range.
25506///
25507/// This function uses markers to define ranges:
25508/// - `|` marks the cursor position
25509/// - `<>` marks the replace range
25510/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25511pub fn handle_completion_request_with_insert_and_replace(
25512 cx: &mut EditorLspTestContext,
25513 marked_string: &str,
25514 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25515 counter: Arc<AtomicUsize>,
25516) -> impl Future<Output = ()> {
25517 let complete_from_marker: TextRangeMarker = '|'.into();
25518 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25519 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25520
25521 let (_, mut marked_ranges) = marked_text_ranges_by(
25522 marked_string,
25523 vec![
25524 complete_from_marker.clone(),
25525 replace_range_marker.clone(),
25526 insert_range_marker.clone(),
25527 ],
25528 );
25529
25530 let complete_from_position =
25531 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25532 let replace_range =
25533 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25534
25535 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25536 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25537 _ => lsp::Range {
25538 start: replace_range.start,
25539 end: complete_from_position,
25540 },
25541 };
25542
25543 let mut request =
25544 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25545 let completions = completions.clone();
25546 counter.fetch_add(1, atomic::Ordering::Release);
25547 async move {
25548 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25549 assert_eq!(
25550 params.text_document_position.position, complete_from_position,
25551 "marker `|` position doesn't match",
25552 );
25553 Ok(Some(lsp::CompletionResponse::Array(
25554 completions
25555 .iter()
25556 .map(|(label, new_text)| lsp::CompletionItem {
25557 label: label.to_string(),
25558 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25559 lsp::InsertReplaceEdit {
25560 insert: insert_range,
25561 replace: replace_range,
25562 new_text: new_text.to_string(),
25563 },
25564 )),
25565 ..Default::default()
25566 })
25567 .collect(),
25568 )))
25569 }
25570 });
25571
25572 async move {
25573 request.next().await;
25574 }
25575}
25576
25577fn handle_resolve_completion_request(
25578 cx: &mut EditorLspTestContext,
25579 edits: Option<Vec<(&'static str, &'static str)>>,
25580) -> impl Future<Output = ()> {
25581 let edits = edits.map(|edits| {
25582 edits
25583 .iter()
25584 .map(|(marked_string, new_text)| {
25585 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25586 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25587 lsp::TextEdit::new(replace_range, new_text.to_string())
25588 })
25589 .collect::<Vec<_>>()
25590 });
25591
25592 let mut request =
25593 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25594 let edits = edits.clone();
25595 async move {
25596 Ok(lsp::CompletionItem {
25597 additional_text_edits: edits,
25598 ..Default::default()
25599 })
25600 }
25601 });
25602
25603 async move {
25604 request.next().await;
25605 }
25606}
25607
25608pub(crate) fn update_test_language_settings(
25609 cx: &mut TestAppContext,
25610 f: impl Fn(&mut AllLanguageSettingsContent),
25611) {
25612 cx.update(|cx| {
25613 SettingsStore::update_global(cx, |store, cx| {
25614 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25615 });
25616 });
25617}
25618
25619pub(crate) fn update_test_project_settings(
25620 cx: &mut TestAppContext,
25621 f: impl Fn(&mut ProjectSettingsContent),
25622) {
25623 cx.update(|cx| {
25624 SettingsStore::update_global(cx, |store, cx| {
25625 store.update_user_settings(cx, |settings| f(&mut settings.project));
25626 });
25627 });
25628}
25629
25630pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25631 cx.update(|cx| {
25632 assets::Assets.load_test_fonts(cx);
25633 let store = SettingsStore::test(cx);
25634 cx.set_global(store);
25635 theme::init(theme::LoadThemes::JustBase, cx);
25636 release_channel::init(SemanticVersion::default(), cx);
25637 client::init_settings(cx);
25638 language::init(cx);
25639 Project::init_settings(cx);
25640 workspace::init_settings(cx);
25641 crate::init(cx);
25642 });
25643 zlog::init_test();
25644 update_test_language_settings(cx, f);
25645}
25646
25647#[track_caller]
25648fn assert_hunk_revert(
25649 not_reverted_text_with_selections: &str,
25650 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25651 expected_reverted_text_with_selections: &str,
25652 base_text: &str,
25653 cx: &mut EditorLspTestContext,
25654) {
25655 cx.set_state(not_reverted_text_with_selections);
25656 cx.set_head_text(base_text);
25657 cx.executor().run_until_parked();
25658
25659 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25660 let snapshot = editor.snapshot(window, cx);
25661 let reverted_hunk_statuses = snapshot
25662 .buffer_snapshot()
25663 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25664 .map(|hunk| hunk.status().kind)
25665 .collect::<Vec<_>>();
25666
25667 editor.git_restore(&Default::default(), window, cx);
25668 reverted_hunk_statuses
25669 });
25670 cx.executor().run_until_parked();
25671 cx.assert_editor_state(expected_reverted_text_with_selections);
25672 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25673}
25674
25675#[gpui::test(iterations = 10)]
25676async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25677 init_test(cx, |_| {});
25678
25679 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25680 let counter = diagnostic_requests.clone();
25681
25682 let fs = FakeFs::new(cx.executor());
25683 fs.insert_tree(
25684 path!("/a"),
25685 json!({
25686 "first.rs": "fn main() { let a = 5; }",
25687 "second.rs": "// Test file",
25688 }),
25689 )
25690 .await;
25691
25692 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25694 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25695
25696 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25697 language_registry.add(rust_lang());
25698 let mut fake_servers = language_registry.register_fake_lsp(
25699 "Rust",
25700 FakeLspAdapter {
25701 capabilities: lsp::ServerCapabilities {
25702 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25703 lsp::DiagnosticOptions {
25704 identifier: None,
25705 inter_file_dependencies: true,
25706 workspace_diagnostics: true,
25707 work_done_progress_options: Default::default(),
25708 },
25709 )),
25710 ..Default::default()
25711 },
25712 ..Default::default()
25713 },
25714 );
25715
25716 let editor = workspace
25717 .update(cx, |workspace, window, cx| {
25718 workspace.open_abs_path(
25719 PathBuf::from(path!("/a/first.rs")),
25720 OpenOptions::default(),
25721 window,
25722 cx,
25723 )
25724 })
25725 .unwrap()
25726 .await
25727 .unwrap()
25728 .downcast::<Editor>()
25729 .unwrap();
25730 let fake_server = fake_servers.next().await.unwrap();
25731 let server_id = fake_server.server.server_id();
25732 let mut first_request = fake_server
25733 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25734 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25735 let result_id = Some(new_result_id.to_string());
25736 assert_eq!(
25737 params.text_document.uri,
25738 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25739 );
25740 async move {
25741 Ok(lsp::DocumentDiagnosticReportResult::Report(
25742 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25743 related_documents: None,
25744 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25745 items: Vec::new(),
25746 result_id,
25747 },
25748 }),
25749 ))
25750 }
25751 });
25752
25753 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25754 project.update(cx, |project, cx| {
25755 let buffer_id = editor
25756 .read(cx)
25757 .buffer()
25758 .read(cx)
25759 .as_singleton()
25760 .expect("created a singleton buffer")
25761 .read(cx)
25762 .remote_id();
25763 let buffer_result_id = project
25764 .lsp_store()
25765 .read(cx)
25766 .result_id(server_id, buffer_id, cx);
25767 assert_eq!(expected, buffer_result_id);
25768 });
25769 };
25770
25771 ensure_result_id(None, cx);
25772 cx.executor().advance_clock(Duration::from_millis(60));
25773 cx.executor().run_until_parked();
25774 assert_eq!(
25775 diagnostic_requests.load(atomic::Ordering::Acquire),
25776 1,
25777 "Opening file should trigger diagnostic request"
25778 );
25779 first_request
25780 .next()
25781 .await
25782 .expect("should have sent the first diagnostics pull request");
25783 ensure_result_id(Some("1".to_string()), cx);
25784
25785 // Editing should trigger diagnostics
25786 editor.update_in(cx, |editor, window, cx| {
25787 editor.handle_input("2", window, cx)
25788 });
25789 cx.executor().advance_clock(Duration::from_millis(60));
25790 cx.executor().run_until_parked();
25791 assert_eq!(
25792 diagnostic_requests.load(atomic::Ordering::Acquire),
25793 2,
25794 "Editing should trigger diagnostic request"
25795 );
25796 ensure_result_id(Some("2".to_string()), cx);
25797
25798 // Moving cursor should not trigger diagnostic request
25799 editor.update_in(cx, |editor, window, cx| {
25800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25801 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25802 });
25803 });
25804 cx.executor().advance_clock(Duration::from_millis(60));
25805 cx.executor().run_until_parked();
25806 assert_eq!(
25807 diagnostic_requests.load(atomic::Ordering::Acquire),
25808 2,
25809 "Cursor movement should not trigger diagnostic request"
25810 );
25811 ensure_result_id(Some("2".to_string()), cx);
25812 // Multiple rapid edits should be debounced
25813 for _ in 0..5 {
25814 editor.update_in(cx, |editor, window, cx| {
25815 editor.handle_input("x", window, cx)
25816 });
25817 }
25818 cx.executor().advance_clock(Duration::from_millis(60));
25819 cx.executor().run_until_parked();
25820
25821 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25822 assert!(
25823 final_requests <= 4,
25824 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25825 );
25826 ensure_result_id(Some(final_requests.to_string()), cx);
25827}
25828
25829#[gpui::test]
25830async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25831 // Regression test for issue #11671
25832 // Previously, adding a cursor after moving multiple cursors would reset
25833 // the cursor count instead of adding to the existing cursors.
25834 init_test(cx, |_| {});
25835 let mut cx = EditorTestContext::new(cx).await;
25836
25837 // Create a simple buffer with cursor at start
25838 cx.set_state(indoc! {"
25839 ˇaaaa
25840 bbbb
25841 cccc
25842 dddd
25843 eeee
25844 ffff
25845 gggg
25846 hhhh"});
25847
25848 // Add 2 cursors below (so we have 3 total)
25849 cx.update_editor(|editor, window, cx| {
25850 editor.add_selection_below(&Default::default(), window, cx);
25851 editor.add_selection_below(&Default::default(), window, cx);
25852 });
25853
25854 // Verify we have 3 cursors
25855 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25856 assert_eq!(
25857 initial_count, 3,
25858 "Should have 3 cursors after adding 2 below"
25859 );
25860
25861 // Move down one line
25862 cx.update_editor(|editor, window, cx| {
25863 editor.move_down(&MoveDown, window, cx);
25864 });
25865
25866 // Add another cursor below
25867 cx.update_editor(|editor, window, cx| {
25868 editor.add_selection_below(&Default::default(), window, cx);
25869 });
25870
25871 // Should now have 4 cursors (3 original + 1 new)
25872 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25873 assert_eq!(
25874 final_count, 4,
25875 "Should have 4 cursors after moving and adding another"
25876 );
25877}
25878
25879#[gpui::test]
25880async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25881 init_test(cx, |_| {});
25882
25883 let mut cx = EditorTestContext::new(cx).await;
25884
25885 cx.set_state(indoc!(
25886 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25887 Second line here"#
25888 ));
25889
25890 cx.update_editor(|editor, window, cx| {
25891 // Enable soft wrapping with a narrow width to force soft wrapping and
25892 // confirm that more than 2 rows are being displayed.
25893 editor.set_wrap_width(Some(100.0.into()), cx);
25894 assert!(editor.display_text(cx).lines().count() > 2);
25895
25896 editor.add_selection_below(
25897 &AddSelectionBelow {
25898 skip_soft_wrap: true,
25899 },
25900 window,
25901 cx,
25902 );
25903
25904 assert_eq!(
25905 editor.selections.display_ranges(cx),
25906 &[
25907 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25908 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25909 ]
25910 );
25911
25912 editor.add_selection_above(
25913 &AddSelectionAbove {
25914 skip_soft_wrap: true,
25915 },
25916 window,
25917 cx,
25918 );
25919
25920 assert_eq!(
25921 editor.selections.display_ranges(cx),
25922 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25923 );
25924
25925 editor.add_selection_below(
25926 &AddSelectionBelow {
25927 skip_soft_wrap: false,
25928 },
25929 window,
25930 cx,
25931 );
25932
25933 assert_eq!(
25934 editor.selections.display_ranges(cx),
25935 &[
25936 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25937 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25938 ]
25939 );
25940
25941 editor.add_selection_above(
25942 &AddSelectionAbove {
25943 skip_soft_wrap: false,
25944 },
25945 window,
25946 cx,
25947 );
25948
25949 assert_eq!(
25950 editor.selections.display_ranges(cx),
25951 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25952 );
25953 });
25954}
25955
25956#[gpui::test(iterations = 10)]
25957async fn test_document_colors(cx: &mut TestAppContext) {
25958 let expected_color = Rgba {
25959 r: 0.33,
25960 g: 0.33,
25961 b: 0.33,
25962 a: 0.33,
25963 };
25964
25965 init_test(cx, |_| {});
25966
25967 let fs = FakeFs::new(cx.executor());
25968 fs.insert_tree(
25969 path!("/a"),
25970 json!({
25971 "first.rs": "fn main() { let a = 5; }",
25972 }),
25973 )
25974 .await;
25975
25976 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25977 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25978 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25979
25980 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25981 language_registry.add(rust_lang());
25982 let mut fake_servers = language_registry.register_fake_lsp(
25983 "Rust",
25984 FakeLspAdapter {
25985 capabilities: lsp::ServerCapabilities {
25986 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25987 ..lsp::ServerCapabilities::default()
25988 },
25989 name: "rust-analyzer",
25990 ..FakeLspAdapter::default()
25991 },
25992 );
25993 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25994 "Rust",
25995 FakeLspAdapter {
25996 capabilities: lsp::ServerCapabilities {
25997 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25998 ..lsp::ServerCapabilities::default()
25999 },
26000 name: "not-rust-analyzer",
26001 ..FakeLspAdapter::default()
26002 },
26003 );
26004
26005 let editor = workspace
26006 .update(cx, |workspace, window, cx| {
26007 workspace.open_abs_path(
26008 PathBuf::from(path!("/a/first.rs")),
26009 OpenOptions::default(),
26010 window,
26011 cx,
26012 )
26013 })
26014 .unwrap()
26015 .await
26016 .unwrap()
26017 .downcast::<Editor>()
26018 .unwrap();
26019 let fake_language_server = fake_servers.next().await.unwrap();
26020 let fake_language_server_without_capabilities =
26021 fake_servers_without_capabilities.next().await.unwrap();
26022 let requests_made = Arc::new(AtomicUsize::new(0));
26023 let closure_requests_made = Arc::clone(&requests_made);
26024 let mut color_request_handle = fake_language_server
26025 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26026 let requests_made = Arc::clone(&closure_requests_made);
26027 async move {
26028 assert_eq!(
26029 params.text_document.uri,
26030 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26031 );
26032 requests_made.fetch_add(1, atomic::Ordering::Release);
26033 Ok(vec![
26034 lsp::ColorInformation {
26035 range: lsp::Range {
26036 start: lsp::Position {
26037 line: 0,
26038 character: 0,
26039 },
26040 end: lsp::Position {
26041 line: 0,
26042 character: 1,
26043 },
26044 },
26045 color: lsp::Color {
26046 red: 0.33,
26047 green: 0.33,
26048 blue: 0.33,
26049 alpha: 0.33,
26050 },
26051 },
26052 lsp::ColorInformation {
26053 range: lsp::Range {
26054 start: lsp::Position {
26055 line: 0,
26056 character: 0,
26057 },
26058 end: lsp::Position {
26059 line: 0,
26060 character: 1,
26061 },
26062 },
26063 color: lsp::Color {
26064 red: 0.33,
26065 green: 0.33,
26066 blue: 0.33,
26067 alpha: 0.33,
26068 },
26069 },
26070 ])
26071 }
26072 });
26073
26074 let _handle = fake_language_server_without_capabilities
26075 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26076 panic!("Should not be called");
26077 });
26078 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26079 color_request_handle.next().await.unwrap();
26080 cx.run_until_parked();
26081 assert_eq!(
26082 1,
26083 requests_made.load(atomic::Ordering::Acquire),
26084 "Should query for colors once per editor open"
26085 );
26086 editor.update_in(cx, |editor, _, cx| {
26087 assert_eq!(
26088 vec![expected_color],
26089 extract_color_inlays(editor, cx),
26090 "Should have an initial inlay"
26091 );
26092 });
26093
26094 // opening another file in a split should not influence the LSP query counter
26095 workspace
26096 .update(cx, |workspace, window, cx| {
26097 assert_eq!(
26098 workspace.panes().len(),
26099 1,
26100 "Should have one pane with one editor"
26101 );
26102 workspace.move_item_to_pane_in_direction(
26103 &MoveItemToPaneInDirection {
26104 direction: SplitDirection::Right,
26105 focus: false,
26106 clone: true,
26107 },
26108 window,
26109 cx,
26110 );
26111 })
26112 .unwrap();
26113 cx.run_until_parked();
26114 workspace
26115 .update(cx, |workspace, _, cx| {
26116 let panes = workspace.panes();
26117 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26118 for pane in panes {
26119 let editor = pane
26120 .read(cx)
26121 .active_item()
26122 .and_then(|item| item.downcast::<Editor>())
26123 .expect("Should have opened an editor in each split");
26124 let editor_file = editor
26125 .read(cx)
26126 .buffer()
26127 .read(cx)
26128 .as_singleton()
26129 .expect("test deals with singleton buffers")
26130 .read(cx)
26131 .file()
26132 .expect("test buffese should have a file")
26133 .path();
26134 assert_eq!(
26135 editor_file.as_ref(),
26136 rel_path("first.rs"),
26137 "Both editors should be opened for the same file"
26138 )
26139 }
26140 })
26141 .unwrap();
26142
26143 cx.executor().advance_clock(Duration::from_millis(500));
26144 let save = editor.update_in(cx, |editor, window, cx| {
26145 editor.move_to_end(&MoveToEnd, window, cx);
26146 editor.handle_input("dirty", window, cx);
26147 editor.save(
26148 SaveOptions {
26149 format: true,
26150 autosave: true,
26151 },
26152 project.clone(),
26153 window,
26154 cx,
26155 )
26156 });
26157 save.await.unwrap();
26158
26159 color_request_handle.next().await.unwrap();
26160 cx.run_until_parked();
26161 assert_eq!(
26162 2,
26163 requests_made.load(atomic::Ordering::Acquire),
26164 "Should query for colors once per save (deduplicated) and once per formatting after save"
26165 );
26166
26167 drop(editor);
26168 let close = workspace
26169 .update(cx, |workspace, window, cx| {
26170 workspace.active_pane().update(cx, |pane, cx| {
26171 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26172 })
26173 })
26174 .unwrap();
26175 close.await.unwrap();
26176 let close = workspace
26177 .update(cx, |workspace, window, cx| {
26178 workspace.active_pane().update(cx, |pane, cx| {
26179 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26180 })
26181 })
26182 .unwrap();
26183 close.await.unwrap();
26184 assert_eq!(
26185 2,
26186 requests_made.load(atomic::Ordering::Acquire),
26187 "After saving and closing all editors, no extra requests should be made"
26188 );
26189 workspace
26190 .update(cx, |workspace, _, cx| {
26191 assert!(
26192 workspace.active_item(cx).is_none(),
26193 "Should close all editors"
26194 )
26195 })
26196 .unwrap();
26197
26198 workspace
26199 .update(cx, |workspace, window, cx| {
26200 workspace.active_pane().update(cx, |pane, cx| {
26201 pane.navigate_backward(&workspace::GoBack, window, cx);
26202 })
26203 })
26204 .unwrap();
26205 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26206 cx.run_until_parked();
26207 let editor = workspace
26208 .update(cx, |workspace, _, cx| {
26209 workspace
26210 .active_item(cx)
26211 .expect("Should have reopened the editor again after navigating back")
26212 .downcast::<Editor>()
26213 .expect("Should be an editor")
26214 })
26215 .unwrap();
26216
26217 assert_eq!(
26218 2,
26219 requests_made.load(atomic::Ordering::Acquire),
26220 "Cache should be reused on buffer close and reopen"
26221 );
26222 editor.update(cx, |editor, cx| {
26223 assert_eq!(
26224 vec![expected_color],
26225 extract_color_inlays(editor, cx),
26226 "Should have an initial inlay"
26227 );
26228 });
26229
26230 drop(color_request_handle);
26231 let closure_requests_made = Arc::clone(&requests_made);
26232 let mut empty_color_request_handle = fake_language_server
26233 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26234 let requests_made = Arc::clone(&closure_requests_made);
26235 async move {
26236 assert_eq!(
26237 params.text_document.uri,
26238 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26239 );
26240 requests_made.fetch_add(1, atomic::Ordering::Release);
26241 Ok(Vec::new())
26242 }
26243 });
26244 let save = editor.update_in(cx, |editor, window, cx| {
26245 editor.move_to_end(&MoveToEnd, window, cx);
26246 editor.handle_input("dirty_again", window, cx);
26247 editor.save(
26248 SaveOptions {
26249 format: false,
26250 autosave: true,
26251 },
26252 project.clone(),
26253 window,
26254 cx,
26255 )
26256 });
26257 save.await.unwrap();
26258
26259 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26260 empty_color_request_handle.next().await.unwrap();
26261 cx.run_until_parked();
26262 assert_eq!(
26263 3,
26264 requests_made.load(atomic::Ordering::Acquire),
26265 "Should query for colors once per save only, as formatting was not requested"
26266 );
26267 editor.update(cx, |editor, cx| {
26268 assert_eq!(
26269 Vec::<Rgba>::new(),
26270 extract_color_inlays(editor, cx),
26271 "Should clear all colors when the server returns an empty response"
26272 );
26273 });
26274}
26275
26276#[gpui::test]
26277async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26278 init_test(cx, |_| {});
26279 let (editor, cx) = cx.add_window_view(Editor::single_line);
26280 editor.update_in(cx, |editor, window, cx| {
26281 editor.set_text("oops\n\nwow\n", window, cx)
26282 });
26283 cx.run_until_parked();
26284 editor.update(cx, |editor, cx| {
26285 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26286 });
26287 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26288 cx.run_until_parked();
26289 editor.update(cx, |editor, cx| {
26290 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26291 });
26292}
26293
26294#[gpui::test]
26295async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26296 init_test(cx, |_| {});
26297
26298 cx.update(|cx| {
26299 register_project_item::<Editor>(cx);
26300 });
26301
26302 let fs = FakeFs::new(cx.executor());
26303 fs.insert_tree("/root1", json!({})).await;
26304 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26305 .await;
26306
26307 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26308 let (workspace, cx) =
26309 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26310
26311 let worktree_id = project.update(cx, |project, cx| {
26312 project.worktrees(cx).next().unwrap().read(cx).id()
26313 });
26314
26315 let handle = workspace
26316 .update_in(cx, |workspace, window, cx| {
26317 let project_path = (worktree_id, rel_path("one.pdf"));
26318 workspace.open_path(project_path, None, true, window, cx)
26319 })
26320 .await
26321 .unwrap();
26322
26323 assert_eq!(
26324 handle.to_any().entity_type(),
26325 TypeId::of::<InvalidItemView>()
26326 );
26327}
26328
26329#[gpui::test]
26330async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26331 init_test(cx, |_| {});
26332
26333 let language = Arc::new(Language::new(
26334 LanguageConfig::default(),
26335 Some(tree_sitter_rust::LANGUAGE.into()),
26336 ));
26337
26338 // Test hierarchical sibling navigation
26339 let text = r#"
26340 fn outer() {
26341 if condition {
26342 let a = 1;
26343 }
26344 let b = 2;
26345 }
26346
26347 fn another() {
26348 let c = 3;
26349 }
26350 "#;
26351
26352 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26353 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26354 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26355
26356 // Wait for parsing to complete
26357 editor
26358 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26359 .await;
26360
26361 editor.update_in(cx, |editor, window, cx| {
26362 // Start by selecting "let a = 1;" inside the if block
26363 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26364 s.select_display_ranges([
26365 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26366 ]);
26367 });
26368
26369 let initial_selection = editor.selections.display_ranges(cx);
26370 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26371
26372 // Test select next sibling - should move up levels to find the next sibling
26373 // Since "let a = 1;" has no siblings in the if block, it should move up
26374 // to find "let b = 2;" which is a sibling of the if block
26375 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26376 let next_selection = editor.selections.display_ranges(cx);
26377
26378 // Should have a selection and it should be different from the initial
26379 assert_eq!(
26380 next_selection.len(),
26381 1,
26382 "Should have one selection after next"
26383 );
26384 assert_ne!(
26385 next_selection[0], initial_selection[0],
26386 "Next sibling selection should be different"
26387 );
26388
26389 // Test hierarchical navigation by going to the end of the current function
26390 // and trying to navigate to the next function
26391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26392 s.select_display_ranges([
26393 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26394 ]);
26395 });
26396
26397 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26398 let function_next_selection = editor.selections.display_ranges(cx);
26399
26400 // Should move to the next function
26401 assert_eq!(
26402 function_next_selection.len(),
26403 1,
26404 "Should have one selection after function next"
26405 );
26406
26407 // Test select previous sibling navigation
26408 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26409 let prev_selection = editor.selections.display_ranges(cx);
26410
26411 // Should have a selection and it should be different
26412 assert_eq!(
26413 prev_selection.len(),
26414 1,
26415 "Should have one selection after prev"
26416 );
26417 assert_ne!(
26418 prev_selection[0], function_next_selection[0],
26419 "Previous sibling selection should be different from next"
26420 );
26421 });
26422}
26423
26424#[gpui::test]
26425async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26426 init_test(cx, |_| {});
26427
26428 let mut cx = EditorTestContext::new(cx).await;
26429 cx.set_state(
26430 "let ˇvariable = 42;
26431let another = variable + 1;
26432let result = variable * 2;",
26433 );
26434
26435 // Set up document highlights manually (simulating LSP response)
26436 cx.update_editor(|editor, _window, cx| {
26437 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26438
26439 // Create highlights for "variable" occurrences
26440 let highlight_ranges = [
26441 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26442 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26443 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26444 ];
26445
26446 let anchor_ranges: Vec<_> = highlight_ranges
26447 .iter()
26448 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26449 .collect();
26450
26451 editor.highlight_background::<DocumentHighlightRead>(
26452 &anchor_ranges,
26453 |theme| theme.colors().editor_document_highlight_read_background,
26454 cx,
26455 );
26456 });
26457
26458 // Go to next highlight - should move to second "variable"
26459 cx.update_editor(|editor, window, cx| {
26460 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26461 });
26462 cx.assert_editor_state(
26463 "let variable = 42;
26464let another = ˇvariable + 1;
26465let result = variable * 2;",
26466 );
26467
26468 // Go to next highlight - should move to third "variable"
26469 cx.update_editor(|editor, window, cx| {
26470 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26471 });
26472 cx.assert_editor_state(
26473 "let variable = 42;
26474let another = variable + 1;
26475let result = ˇvariable * 2;",
26476 );
26477
26478 // Go to next highlight - should stay at third "variable" (no wrap-around)
26479 cx.update_editor(|editor, window, cx| {
26480 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26481 });
26482 cx.assert_editor_state(
26483 "let variable = 42;
26484let another = variable + 1;
26485let result = ˇvariable * 2;",
26486 );
26487
26488 // Now test going backwards from third position
26489 cx.update_editor(|editor, window, cx| {
26490 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26491 });
26492 cx.assert_editor_state(
26493 "let variable = 42;
26494let another = ˇvariable + 1;
26495let result = variable * 2;",
26496 );
26497
26498 // Go to previous highlight - should move to first "variable"
26499 cx.update_editor(|editor, window, cx| {
26500 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26501 });
26502 cx.assert_editor_state(
26503 "let ˇvariable = 42;
26504let another = variable + 1;
26505let result = variable * 2;",
26506 );
26507
26508 // Go to previous highlight - should stay on first "variable"
26509 cx.update_editor(|editor, window, cx| {
26510 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26511 });
26512 cx.assert_editor_state(
26513 "let ˇvariable = 42;
26514let another = variable + 1;
26515let result = variable * 2;",
26516 );
26517}
26518
26519#[gpui::test]
26520async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26521 cx: &mut gpui::TestAppContext,
26522) {
26523 init_test(cx, |_| {});
26524
26525 let url = "https://zed.dev";
26526
26527 let markdown_language = Arc::new(Language::new(
26528 LanguageConfig {
26529 name: "Markdown".into(),
26530 ..LanguageConfig::default()
26531 },
26532 None,
26533 ));
26534
26535 let mut cx = EditorTestContext::new(cx).await;
26536 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26537 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26538
26539 cx.update_editor(|editor, window, cx| {
26540 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26541 editor.paste(&Paste, window, cx);
26542 });
26543
26544 cx.assert_editor_state(&format!(
26545 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26546 ));
26547}
26548
26549#[gpui::test]
26550async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26551 cx: &mut gpui::TestAppContext,
26552) {
26553 init_test(cx, |_| {});
26554
26555 let url = "https://zed.dev";
26556
26557 let markdown_language = Arc::new(Language::new(
26558 LanguageConfig {
26559 name: "Markdown".into(),
26560 ..LanguageConfig::default()
26561 },
26562 None,
26563 ));
26564
26565 let mut cx = EditorTestContext::new(cx).await;
26566 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26567 cx.set_state(&format!(
26568 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26569 ));
26570
26571 cx.update_editor(|editor, window, cx| {
26572 editor.copy(&Copy, window, cx);
26573 });
26574
26575 cx.set_state(&format!(
26576 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26577 ));
26578
26579 cx.update_editor(|editor, window, cx| {
26580 editor.paste(&Paste, window, cx);
26581 });
26582
26583 cx.assert_editor_state(&format!(
26584 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26585 ));
26586}
26587
26588#[gpui::test]
26589async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26590 cx: &mut gpui::TestAppContext,
26591) {
26592 init_test(cx, |_| {});
26593
26594 let url = "https://zed.dev";
26595
26596 let markdown_language = Arc::new(Language::new(
26597 LanguageConfig {
26598 name: "Markdown".into(),
26599 ..LanguageConfig::default()
26600 },
26601 None,
26602 ));
26603
26604 let mut cx = EditorTestContext::new(cx).await;
26605 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26606 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26607
26608 cx.update_editor(|editor, window, cx| {
26609 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26610 editor.paste(&Paste, window, cx);
26611 });
26612
26613 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26614}
26615
26616#[gpui::test]
26617async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26618 cx: &mut gpui::TestAppContext,
26619) {
26620 init_test(cx, |_| {});
26621
26622 let text = "Awesome";
26623
26624 let markdown_language = Arc::new(Language::new(
26625 LanguageConfig {
26626 name: "Markdown".into(),
26627 ..LanguageConfig::default()
26628 },
26629 None,
26630 ));
26631
26632 let mut cx = EditorTestContext::new(cx).await;
26633 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26634 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26635
26636 cx.update_editor(|editor, window, cx| {
26637 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26638 editor.paste(&Paste, window, cx);
26639 });
26640
26641 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26642}
26643
26644#[gpui::test]
26645async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26646 cx: &mut gpui::TestAppContext,
26647) {
26648 init_test(cx, |_| {});
26649
26650 let url = "https://zed.dev";
26651
26652 let markdown_language = Arc::new(Language::new(
26653 LanguageConfig {
26654 name: "Rust".into(),
26655 ..LanguageConfig::default()
26656 },
26657 None,
26658 ));
26659
26660 let mut cx = EditorTestContext::new(cx).await;
26661 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26662 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26663
26664 cx.update_editor(|editor, window, cx| {
26665 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26666 editor.paste(&Paste, window, cx);
26667 });
26668
26669 cx.assert_editor_state(&format!(
26670 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26671 ));
26672}
26673
26674#[gpui::test]
26675async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26676 cx: &mut TestAppContext,
26677) {
26678 init_test(cx, |_| {});
26679
26680 let url = "https://zed.dev";
26681
26682 let markdown_language = Arc::new(Language::new(
26683 LanguageConfig {
26684 name: "Markdown".into(),
26685 ..LanguageConfig::default()
26686 },
26687 None,
26688 ));
26689
26690 let (editor, cx) = cx.add_window_view(|window, cx| {
26691 let multi_buffer = MultiBuffer::build_multi(
26692 [
26693 ("this will embed -> link", vec![Point::row_range(0..1)]),
26694 ("this will replace -> link", vec![Point::row_range(0..1)]),
26695 ],
26696 cx,
26697 );
26698 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26699 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26700 s.select_ranges(vec![
26701 Point::new(0, 19)..Point::new(0, 23),
26702 Point::new(1, 21)..Point::new(1, 25),
26703 ])
26704 });
26705 let first_buffer_id = multi_buffer
26706 .read(cx)
26707 .excerpt_buffer_ids()
26708 .into_iter()
26709 .next()
26710 .unwrap();
26711 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26712 first_buffer.update(cx, |buffer, cx| {
26713 buffer.set_language(Some(markdown_language.clone()), cx);
26714 });
26715
26716 editor
26717 });
26718 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26719
26720 cx.update_editor(|editor, window, cx| {
26721 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26722 editor.paste(&Paste, window, cx);
26723 });
26724
26725 cx.assert_editor_state(&format!(
26726 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26727 ));
26728}
26729
26730#[gpui::test]
26731async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26732 init_test(cx, |_| {});
26733
26734 let fs = FakeFs::new(cx.executor());
26735 fs.insert_tree(
26736 path!("/project"),
26737 json!({
26738 "first.rs": "# First Document\nSome content here.",
26739 "second.rs": "Plain text content for second file.",
26740 }),
26741 )
26742 .await;
26743
26744 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26745 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26746 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26747
26748 let language = rust_lang();
26749 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26750 language_registry.add(language.clone());
26751 let mut fake_servers = language_registry.register_fake_lsp(
26752 "Rust",
26753 FakeLspAdapter {
26754 ..FakeLspAdapter::default()
26755 },
26756 );
26757
26758 let buffer1 = project
26759 .update(cx, |project, cx| {
26760 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26761 })
26762 .await
26763 .unwrap();
26764 let buffer2 = project
26765 .update(cx, |project, cx| {
26766 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26767 })
26768 .await
26769 .unwrap();
26770
26771 let multi_buffer = cx.new(|cx| {
26772 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26773 multi_buffer.set_excerpts_for_path(
26774 PathKey::for_buffer(&buffer1, cx),
26775 buffer1.clone(),
26776 [Point::zero()..buffer1.read(cx).max_point()],
26777 3,
26778 cx,
26779 );
26780 multi_buffer.set_excerpts_for_path(
26781 PathKey::for_buffer(&buffer2, cx),
26782 buffer2.clone(),
26783 [Point::zero()..buffer1.read(cx).max_point()],
26784 3,
26785 cx,
26786 );
26787 multi_buffer
26788 });
26789
26790 let (editor, cx) = cx.add_window_view(|window, cx| {
26791 Editor::new(
26792 EditorMode::full(),
26793 multi_buffer,
26794 Some(project.clone()),
26795 window,
26796 cx,
26797 )
26798 });
26799
26800 let fake_language_server = fake_servers.next().await.unwrap();
26801
26802 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26803
26804 let save = editor.update_in(cx, |editor, window, cx| {
26805 assert!(editor.is_dirty(cx));
26806
26807 editor.save(
26808 SaveOptions {
26809 format: true,
26810 autosave: true,
26811 },
26812 project,
26813 window,
26814 cx,
26815 )
26816 });
26817 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26818 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26819 let mut done_edit_rx = Some(done_edit_rx);
26820 let mut start_edit_tx = Some(start_edit_tx);
26821
26822 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26823 start_edit_tx.take().unwrap().send(()).unwrap();
26824 let done_edit_rx = done_edit_rx.take().unwrap();
26825 async move {
26826 done_edit_rx.await.unwrap();
26827 Ok(None)
26828 }
26829 });
26830
26831 start_edit_rx.await.unwrap();
26832 buffer2
26833 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26834 .unwrap();
26835
26836 done_edit_tx.send(()).unwrap();
26837
26838 save.await.unwrap();
26839 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26840}
26841
26842#[track_caller]
26843fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26844 editor
26845 .all_inlays(cx)
26846 .into_iter()
26847 .filter_map(|inlay| inlay.get_color())
26848 .map(Rgba::from)
26849 .collect()
26850}
26851
26852#[gpui::test]
26853fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26854 init_test(cx, |_| {});
26855
26856 let editor = cx.add_window(|window, cx| {
26857 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26858 build_editor(buffer, window, cx)
26859 });
26860
26861 editor
26862 .update(cx, |editor, window, cx| {
26863 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26864 s.select_display_ranges([
26865 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26866 ])
26867 });
26868
26869 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26870
26871 assert_eq!(
26872 editor.display_text(cx),
26873 "line1\nline2\nline2",
26874 "Duplicating last line upward should create duplicate above, not on same line"
26875 );
26876
26877 assert_eq!(
26878 editor.selections.display_ranges(cx),
26879 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26880 "Selection should move to the duplicated line"
26881 );
26882 })
26883 .unwrap();
26884}
26885
26886#[gpui::test]
26887async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26888 init_test(cx, |_| {});
26889
26890 let mut cx = EditorTestContext::new(cx).await;
26891
26892 cx.set_state("line1\nline2ˇ");
26893
26894 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26895
26896 let clipboard_text = cx
26897 .read_from_clipboard()
26898 .and_then(|item| item.text().as_deref().map(str::to_string));
26899
26900 assert_eq!(
26901 clipboard_text,
26902 Some("line2\n".to_string()),
26903 "Copying a line without trailing newline should include a newline"
26904 );
26905
26906 cx.set_state("line1\nˇ");
26907
26908 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26909
26910 cx.assert_editor_state("line1\nline2\nˇ");
26911}
26912
26913#[gpui::test]
26914async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26915 init_test(cx, |_| {});
26916
26917 let mut cx = EditorTestContext::new(cx).await;
26918
26919 cx.set_state("line1\nline2ˇ");
26920 cx.update_editor(|e, window, cx| {
26921 e.set_mode(EditorMode::SingleLine);
26922 assert!(e.key_context(window, cx).contains("end_of_input"));
26923 });
26924 cx.set_state("ˇline1\nline2");
26925 cx.update_editor(|e, window, cx| {
26926 assert!(!e.key_context(window, cx).contains("end_of_input"));
26927 });
26928 cx.set_state("line1ˇ\nline2");
26929 cx.update_editor(|e, window, cx| {
26930 assert!(!e.key_context(window, cx).contains("end_of_input"));
26931 });
26932}
26933
26934#[gpui::test]
26935async fn test_next_prev_reference(cx: &mut TestAppContext) {
26936 const CYCLE_POSITIONS: &[&'static str] = &[
26937 indoc! {"
26938 fn foo() {
26939 let ˇabc = 123;
26940 let x = abc + 1;
26941 let y = abc + 2;
26942 let z = abc + 2;
26943 }
26944 "},
26945 indoc! {"
26946 fn foo() {
26947 let abc = 123;
26948 let x = ˇabc + 1;
26949 let y = abc + 2;
26950 let z = abc + 2;
26951 }
26952 "},
26953 indoc! {"
26954 fn foo() {
26955 let abc = 123;
26956 let x = abc + 1;
26957 let y = ˇabc + 2;
26958 let z = abc + 2;
26959 }
26960 "},
26961 indoc! {"
26962 fn foo() {
26963 let abc = 123;
26964 let x = abc + 1;
26965 let y = abc + 2;
26966 let z = ˇabc + 2;
26967 }
26968 "},
26969 ];
26970
26971 init_test(cx, |_| {});
26972
26973 let mut cx = EditorLspTestContext::new_rust(
26974 lsp::ServerCapabilities {
26975 references_provider: Some(lsp::OneOf::Left(true)),
26976 ..Default::default()
26977 },
26978 cx,
26979 )
26980 .await;
26981
26982 // importantly, the cursor is in the middle
26983 cx.set_state(indoc! {"
26984 fn foo() {
26985 let aˇbc = 123;
26986 let x = abc + 1;
26987 let y = abc + 2;
26988 let z = abc + 2;
26989 }
26990 "});
26991
26992 let reference_ranges = [
26993 lsp::Position::new(1, 8),
26994 lsp::Position::new(2, 12),
26995 lsp::Position::new(3, 12),
26996 lsp::Position::new(4, 12),
26997 ]
26998 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
26999
27000 cx.lsp
27001 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27002 Ok(Some(
27003 reference_ranges
27004 .map(|range| lsp::Location {
27005 uri: params.text_document_position.text_document.uri.clone(),
27006 range,
27007 })
27008 .to_vec(),
27009 ))
27010 });
27011
27012 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27013 cx.update_editor(|editor, window, cx| {
27014 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27015 })
27016 .unwrap()
27017 .await
27018 .unwrap()
27019 };
27020
27021 _move(Direction::Next, 1, &mut cx).await;
27022 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27023
27024 _move(Direction::Next, 1, &mut cx).await;
27025 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27026
27027 _move(Direction::Next, 1, &mut cx).await;
27028 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27029
27030 // loops back to the start
27031 _move(Direction::Next, 1, &mut cx).await;
27032 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27033
27034 // loops back to the end
27035 _move(Direction::Prev, 1, &mut cx).await;
27036 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27037
27038 _move(Direction::Prev, 1, &mut cx).await;
27039 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27040
27041 _move(Direction::Prev, 1, &mut cx).await;
27042 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27043
27044 _move(Direction::Prev, 1, &mut cx).await;
27045 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27046
27047 _move(Direction::Next, 3, &mut cx).await;
27048 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27049
27050 _move(Direction::Prev, 2, &mut cx).await;
27051 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27052}