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_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11142 init_test(cx, |_| {});
11143
11144 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11145 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11146 assert_eq!(editor.text(cx), expected_text);
11147 assert_eq!(
11148 editor
11149 .selections
11150 .ranges::<usize>(&editor.display_snapshot(cx)),
11151 selection_ranges
11152 );
11153 }
11154
11155 let (text, insertion_ranges) = marked_text_ranges(
11156 indoc! {"
11157 ˇ
11158 "},
11159 false,
11160 );
11161
11162 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11163 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11164
11165 _ = editor.update_in(cx, |editor, window, cx| {
11166 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11167
11168 editor
11169 .insert_snippet(&insertion_ranges, snippet, window, cx)
11170 .unwrap();
11171
11172 assert_state(
11173 editor,
11174 cx,
11175 indoc! {"
11176 type «» = ;•
11177 "},
11178 );
11179
11180 assert!(
11181 editor.context_menu_visible(),
11182 "Context menu should be visible for placeholder choices"
11183 );
11184
11185 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11186
11187 assert_state(
11188 editor,
11189 cx,
11190 indoc! {"
11191 type = «»;•
11192 "},
11193 );
11194
11195 assert!(
11196 !editor.context_menu_visible(),
11197 "Context menu should be hidden after moving to next tabstop"
11198 );
11199
11200 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11201
11202 assert_state(
11203 editor,
11204 cx,
11205 indoc! {"
11206 type = ; ˇ
11207 "},
11208 );
11209
11210 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11211
11212 assert_state(
11213 editor,
11214 cx,
11215 indoc! {"
11216 type = ; ˇ
11217 "},
11218 );
11219 });
11220
11221 _ = editor.update_in(cx, |editor, window, cx| {
11222 editor.select_all(&SelectAll, window, cx);
11223 editor.backspace(&Backspace, window, cx);
11224
11225 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11226 let insertion_ranges = editor
11227 .selections
11228 .all(&editor.display_snapshot(cx))
11229 .iter()
11230 .map(|s| s.range())
11231 .collect::<Vec<_>>();
11232
11233 editor
11234 .insert_snippet(&insertion_ranges, snippet, window, cx)
11235 .unwrap();
11236
11237 assert_state(editor, cx, "fn «» = value;•");
11238
11239 assert!(
11240 editor.context_menu_visible(),
11241 "Context menu should be visible for placeholder choices"
11242 );
11243
11244 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11245
11246 assert_state(editor, cx, "fn = «valueˇ»;•");
11247
11248 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11249
11250 assert_state(editor, cx, "fn «» = value;•");
11251
11252 assert!(
11253 editor.context_menu_visible(),
11254 "Context menu should be visible again after returning to first tabstop"
11255 );
11256
11257 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11258
11259 assert_state(editor, cx, "fn «» = value;•");
11260 });
11261}
11262
11263#[gpui::test]
11264async fn test_snippets(cx: &mut TestAppContext) {
11265 init_test(cx, |_| {});
11266
11267 let mut cx = EditorTestContext::new(cx).await;
11268
11269 cx.set_state(indoc! {"
11270 a.ˇ b
11271 a.ˇ b
11272 a.ˇ b
11273 "});
11274
11275 cx.update_editor(|editor, window, cx| {
11276 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11277 let insertion_ranges = editor
11278 .selections
11279 .all(&editor.display_snapshot(cx))
11280 .iter()
11281 .map(|s| s.range())
11282 .collect::<Vec<_>>();
11283 editor
11284 .insert_snippet(&insertion_ranges, snippet, window, cx)
11285 .unwrap();
11286 });
11287
11288 cx.assert_editor_state(indoc! {"
11289 a.f(«oneˇ», two, «threeˇ») b
11290 a.f(«oneˇ», two, «threeˇ») b
11291 a.f(«oneˇ», two, «threeˇ») b
11292 "});
11293
11294 // Can't move earlier than the first tab stop
11295 cx.update_editor(|editor, window, cx| {
11296 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11297 });
11298 cx.assert_editor_state(indoc! {"
11299 a.f(«oneˇ», two, «threeˇ») b
11300 a.f(«oneˇ», two, «threeˇ») b
11301 a.f(«oneˇ», two, «threeˇ») b
11302 "});
11303
11304 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11305 cx.assert_editor_state(indoc! {"
11306 a.f(one, «twoˇ», three) b
11307 a.f(one, «twoˇ», three) b
11308 a.f(one, «twoˇ», three) b
11309 "});
11310
11311 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11312 cx.assert_editor_state(indoc! {"
11313 a.f(«oneˇ», two, «threeˇ») b
11314 a.f(«oneˇ», two, «threeˇ») b
11315 a.f(«oneˇ», two, «threeˇ») b
11316 "});
11317
11318 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11319 cx.assert_editor_state(indoc! {"
11320 a.f(one, «twoˇ», three) b
11321 a.f(one, «twoˇ», three) b
11322 a.f(one, «twoˇ», three) b
11323 "});
11324 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11325 cx.assert_editor_state(indoc! {"
11326 a.f(one, two, three)ˇ b
11327 a.f(one, two, three)ˇ b
11328 a.f(one, two, three)ˇ b
11329 "});
11330
11331 // As soon as the last tab stop is reached, snippet state is gone
11332 cx.update_editor(|editor, window, cx| {
11333 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11334 });
11335 cx.assert_editor_state(indoc! {"
11336 a.f(one, two, three)ˇ b
11337 a.f(one, two, three)ˇ b
11338 a.f(one, two, three)ˇ b
11339 "});
11340}
11341
11342#[gpui::test]
11343async fn test_snippet_indentation(cx: &mut TestAppContext) {
11344 init_test(cx, |_| {});
11345
11346 let mut cx = EditorTestContext::new(cx).await;
11347
11348 cx.update_editor(|editor, window, cx| {
11349 let snippet = Snippet::parse(indoc! {"
11350 /*
11351 * Multiline comment with leading indentation
11352 *
11353 * $1
11354 */
11355 $0"})
11356 .unwrap();
11357 let insertion_ranges = editor
11358 .selections
11359 .all(&editor.display_snapshot(cx))
11360 .iter()
11361 .map(|s| s.range())
11362 .collect::<Vec<_>>();
11363 editor
11364 .insert_snippet(&insertion_ranges, snippet, window, cx)
11365 .unwrap();
11366 });
11367
11368 cx.assert_editor_state(indoc! {"
11369 /*
11370 * Multiline comment with leading indentation
11371 *
11372 * ˇ
11373 */
11374 "});
11375
11376 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11377 cx.assert_editor_state(indoc! {"
11378 /*
11379 * Multiline comment with leading indentation
11380 *
11381 *•
11382 */
11383 ˇ"});
11384}
11385
11386#[gpui::test]
11387async fn test_document_format_during_save(cx: &mut TestAppContext) {
11388 init_test(cx, |_| {});
11389
11390 let fs = FakeFs::new(cx.executor());
11391 fs.insert_file(path!("/file.rs"), Default::default()).await;
11392
11393 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11394
11395 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11396 language_registry.add(rust_lang());
11397 let mut fake_servers = language_registry.register_fake_lsp(
11398 "Rust",
11399 FakeLspAdapter {
11400 capabilities: lsp::ServerCapabilities {
11401 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11402 ..Default::default()
11403 },
11404 ..Default::default()
11405 },
11406 );
11407
11408 let buffer = project
11409 .update(cx, |project, cx| {
11410 project.open_local_buffer(path!("/file.rs"), cx)
11411 })
11412 .await
11413 .unwrap();
11414
11415 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11416 let (editor, cx) = cx.add_window_view(|window, cx| {
11417 build_editor_with_project(project.clone(), buffer, window, cx)
11418 });
11419 editor.update_in(cx, |editor, window, cx| {
11420 editor.set_text("one\ntwo\nthree\n", window, cx)
11421 });
11422 assert!(cx.read(|cx| editor.is_dirty(cx)));
11423
11424 cx.executor().start_waiting();
11425 let fake_server = fake_servers.next().await.unwrap();
11426
11427 {
11428 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11429 move |params, _| async move {
11430 assert_eq!(
11431 params.text_document.uri,
11432 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11433 );
11434 assert_eq!(params.options.tab_size, 4);
11435 Ok(Some(vec![lsp::TextEdit::new(
11436 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11437 ", ".to_string(),
11438 )]))
11439 },
11440 );
11441 let save = editor
11442 .update_in(cx, |editor, window, cx| {
11443 editor.save(
11444 SaveOptions {
11445 format: true,
11446 autosave: false,
11447 },
11448 project.clone(),
11449 window,
11450 cx,
11451 )
11452 })
11453 .unwrap();
11454 cx.executor().start_waiting();
11455 save.await;
11456
11457 assert_eq!(
11458 editor.update(cx, |editor, cx| editor.text(cx)),
11459 "one, two\nthree\n"
11460 );
11461 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11462 }
11463
11464 {
11465 editor.update_in(cx, |editor, window, cx| {
11466 editor.set_text("one\ntwo\nthree\n", window, cx)
11467 });
11468 assert!(cx.read(|cx| editor.is_dirty(cx)));
11469
11470 // Ensure we can still save even if formatting hangs.
11471 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11472 move |params, _| async move {
11473 assert_eq!(
11474 params.text_document.uri,
11475 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11476 );
11477 futures::future::pending::<()>().await;
11478 unreachable!()
11479 },
11480 );
11481 let save = editor
11482 .update_in(cx, |editor, window, cx| {
11483 editor.save(
11484 SaveOptions {
11485 format: true,
11486 autosave: false,
11487 },
11488 project.clone(),
11489 window,
11490 cx,
11491 )
11492 })
11493 .unwrap();
11494 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11495 cx.executor().start_waiting();
11496 save.await;
11497 assert_eq!(
11498 editor.update(cx, |editor, cx| editor.text(cx)),
11499 "one\ntwo\nthree\n"
11500 );
11501 }
11502
11503 // Set rust language override and assert overridden tabsize is sent to language server
11504 update_test_language_settings(cx, |settings| {
11505 settings.languages.0.insert(
11506 "Rust".into(),
11507 LanguageSettingsContent {
11508 tab_size: NonZeroU32::new(8),
11509 ..Default::default()
11510 },
11511 );
11512 });
11513
11514 {
11515 editor.update_in(cx, |editor, window, cx| {
11516 editor.set_text("somehting_new\n", window, cx)
11517 });
11518 assert!(cx.read(|cx| editor.is_dirty(cx)));
11519 let _formatting_request_signal = fake_server
11520 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11521 assert_eq!(
11522 params.text_document.uri,
11523 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11524 );
11525 assert_eq!(params.options.tab_size, 8);
11526 Ok(Some(vec![]))
11527 });
11528 let save = editor
11529 .update_in(cx, |editor, window, cx| {
11530 editor.save(
11531 SaveOptions {
11532 format: true,
11533 autosave: false,
11534 },
11535 project.clone(),
11536 window,
11537 cx,
11538 )
11539 })
11540 .unwrap();
11541 cx.executor().start_waiting();
11542 save.await;
11543 }
11544}
11545
11546#[gpui::test]
11547async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11548 init_test(cx, |settings| {
11549 settings.defaults.ensure_final_newline_on_save = Some(false);
11550 });
11551
11552 let fs = FakeFs::new(cx.executor());
11553 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11554
11555 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11556
11557 let buffer = project
11558 .update(cx, |project, cx| {
11559 project.open_local_buffer(path!("/file.txt"), cx)
11560 })
11561 .await
11562 .unwrap();
11563
11564 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11565 let (editor, cx) = cx.add_window_view(|window, cx| {
11566 build_editor_with_project(project.clone(), buffer, window, cx)
11567 });
11568 editor.update_in(cx, |editor, window, cx| {
11569 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11570 s.select_ranges([0..0])
11571 });
11572 });
11573 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11574
11575 editor.update_in(cx, |editor, window, cx| {
11576 editor.handle_input("\n", window, cx)
11577 });
11578 cx.run_until_parked();
11579 save(&editor, &project, cx).await;
11580 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11581
11582 editor.update_in(cx, |editor, window, cx| {
11583 editor.undo(&Default::default(), window, cx);
11584 });
11585 save(&editor, &project, cx).await;
11586 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11587
11588 editor.update_in(cx, |editor, window, cx| {
11589 editor.redo(&Default::default(), window, cx);
11590 });
11591 cx.run_until_parked();
11592 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11593
11594 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11595 let save = editor
11596 .update_in(cx, |editor, window, cx| {
11597 editor.save(
11598 SaveOptions {
11599 format: true,
11600 autosave: false,
11601 },
11602 project.clone(),
11603 window,
11604 cx,
11605 )
11606 })
11607 .unwrap();
11608 cx.executor().start_waiting();
11609 save.await;
11610 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11611 }
11612}
11613
11614#[gpui::test]
11615async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11616 init_test(cx, |_| {});
11617
11618 let cols = 4;
11619 let rows = 10;
11620 let sample_text_1 = sample_text(rows, cols, 'a');
11621 assert_eq!(
11622 sample_text_1,
11623 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11624 );
11625 let sample_text_2 = sample_text(rows, cols, 'l');
11626 assert_eq!(
11627 sample_text_2,
11628 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11629 );
11630 let sample_text_3 = sample_text(rows, cols, 'v');
11631 assert_eq!(
11632 sample_text_3,
11633 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11634 );
11635
11636 let fs = FakeFs::new(cx.executor());
11637 fs.insert_tree(
11638 path!("/a"),
11639 json!({
11640 "main.rs": sample_text_1,
11641 "other.rs": sample_text_2,
11642 "lib.rs": sample_text_3,
11643 }),
11644 )
11645 .await;
11646
11647 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11648 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11649 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11650
11651 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11652 language_registry.add(rust_lang());
11653 let mut fake_servers = language_registry.register_fake_lsp(
11654 "Rust",
11655 FakeLspAdapter {
11656 capabilities: lsp::ServerCapabilities {
11657 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11658 ..Default::default()
11659 },
11660 ..Default::default()
11661 },
11662 );
11663
11664 let worktree = project.update(cx, |project, cx| {
11665 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11666 assert_eq!(worktrees.len(), 1);
11667 worktrees.pop().unwrap()
11668 });
11669 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11670
11671 let buffer_1 = project
11672 .update(cx, |project, cx| {
11673 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11674 })
11675 .await
11676 .unwrap();
11677 let buffer_2 = project
11678 .update(cx, |project, cx| {
11679 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11680 })
11681 .await
11682 .unwrap();
11683 let buffer_3 = project
11684 .update(cx, |project, cx| {
11685 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11686 })
11687 .await
11688 .unwrap();
11689
11690 let multi_buffer = cx.new(|cx| {
11691 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11692 multi_buffer.push_excerpts(
11693 buffer_1.clone(),
11694 [
11695 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11696 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11697 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11698 ],
11699 cx,
11700 );
11701 multi_buffer.push_excerpts(
11702 buffer_2.clone(),
11703 [
11704 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11705 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11706 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11707 ],
11708 cx,
11709 );
11710 multi_buffer.push_excerpts(
11711 buffer_3.clone(),
11712 [
11713 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11714 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11715 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11716 ],
11717 cx,
11718 );
11719 multi_buffer
11720 });
11721 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11722 Editor::new(
11723 EditorMode::full(),
11724 multi_buffer,
11725 Some(project.clone()),
11726 window,
11727 cx,
11728 )
11729 });
11730
11731 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11732 editor.change_selections(
11733 SelectionEffects::scroll(Autoscroll::Next),
11734 window,
11735 cx,
11736 |s| s.select_ranges(Some(1..2)),
11737 );
11738 editor.insert("|one|two|three|", window, cx);
11739 });
11740 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11741 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11742 editor.change_selections(
11743 SelectionEffects::scroll(Autoscroll::Next),
11744 window,
11745 cx,
11746 |s| s.select_ranges(Some(60..70)),
11747 );
11748 editor.insert("|four|five|six|", window, cx);
11749 });
11750 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11751
11752 // First two buffers should be edited, but not the third one.
11753 assert_eq!(
11754 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11755 "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}",
11756 );
11757 buffer_1.update(cx, |buffer, _| {
11758 assert!(buffer.is_dirty());
11759 assert_eq!(
11760 buffer.text(),
11761 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11762 )
11763 });
11764 buffer_2.update(cx, |buffer, _| {
11765 assert!(buffer.is_dirty());
11766 assert_eq!(
11767 buffer.text(),
11768 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11769 )
11770 });
11771 buffer_3.update(cx, |buffer, _| {
11772 assert!(!buffer.is_dirty());
11773 assert_eq!(buffer.text(), sample_text_3,)
11774 });
11775 cx.executor().run_until_parked();
11776
11777 cx.executor().start_waiting();
11778 let save = multi_buffer_editor
11779 .update_in(cx, |editor, window, cx| {
11780 editor.save(
11781 SaveOptions {
11782 format: true,
11783 autosave: false,
11784 },
11785 project.clone(),
11786 window,
11787 cx,
11788 )
11789 })
11790 .unwrap();
11791
11792 let fake_server = fake_servers.next().await.unwrap();
11793 fake_server
11794 .server
11795 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11796 Ok(Some(vec![lsp::TextEdit::new(
11797 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11798 format!("[{} formatted]", params.text_document.uri),
11799 )]))
11800 })
11801 .detach();
11802 save.await;
11803
11804 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11805 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11806 assert_eq!(
11807 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11808 uri!(
11809 "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}"
11810 ),
11811 );
11812 buffer_1.update(cx, |buffer, _| {
11813 assert!(!buffer.is_dirty());
11814 assert_eq!(
11815 buffer.text(),
11816 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11817 )
11818 });
11819 buffer_2.update(cx, |buffer, _| {
11820 assert!(!buffer.is_dirty());
11821 assert_eq!(
11822 buffer.text(),
11823 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11824 )
11825 });
11826 buffer_3.update(cx, |buffer, _| {
11827 assert!(!buffer.is_dirty());
11828 assert_eq!(buffer.text(), sample_text_3,)
11829 });
11830}
11831
11832#[gpui::test]
11833async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11834 init_test(cx, |_| {});
11835
11836 let fs = FakeFs::new(cx.executor());
11837 fs.insert_tree(
11838 path!("/dir"),
11839 json!({
11840 "file1.rs": "fn main() { println!(\"hello\"); }",
11841 "file2.rs": "fn test() { println!(\"test\"); }",
11842 "file3.rs": "fn other() { println!(\"other\"); }\n",
11843 }),
11844 )
11845 .await;
11846
11847 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11848 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11849 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11850
11851 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11852 language_registry.add(rust_lang());
11853
11854 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11855 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11856
11857 // Open three buffers
11858 let buffer_1 = project
11859 .update(cx, |project, cx| {
11860 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11861 })
11862 .await
11863 .unwrap();
11864 let buffer_2 = project
11865 .update(cx, |project, cx| {
11866 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11867 })
11868 .await
11869 .unwrap();
11870 let buffer_3 = project
11871 .update(cx, |project, cx| {
11872 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11873 })
11874 .await
11875 .unwrap();
11876
11877 // Create a multi-buffer with all three buffers
11878 let multi_buffer = cx.new(|cx| {
11879 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11880 multi_buffer.push_excerpts(
11881 buffer_1.clone(),
11882 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11883 cx,
11884 );
11885 multi_buffer.push_excerpts(
11886 buffer_2.clone(),
11887 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11888 cx,
11889 );
11890 multi_buffer.push_excerpts(
11891 buffer_3.clone(),
11892 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11893 cx,
11894 );
11895 multi_buffer
11896 });
11897
11898 let editor = cx.new_window_entity(|window, cx| {
11899 Editor::new(
11900 EditorMode::full(),
11901 multi_buffer,
11902 Some(project.clone()),
11903 window,
11904 cx,
11905 )
11906 });
11907
11908 // Edit only the first buffer
11909 editor.update_in(cx, |editor, window, cx| {
11910 editor.change_selections(
11911 SelectionEffects::scroll(Autoscroll::Next),
11912 window,
11913 cx,
11914 |s| s.select_ranges(Some(10..10)),
11915 );
11916 editor.insert("// edited", window, cx);
11917 });
11918
11919 // Verify that only buffer 1 is dirty
11920 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11921 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11922 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11923
11924 // Get write counts after file creation (files were created with initial content)
11925 // We expect each file to have been written once during creation
11926 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11927 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11928 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11929
11930 // Perform autosave
11931 let save_task = editor.update_in(cx, |editor, window, cx| {
11932 editor.save(
11933 SaveOptions {
11934 format: true,
11935 autosave: true,
11936 },
11937 project.clone(),
11938 window,
11939 cx,
11940 )
11941 });
11942 save_task.await.unwrap();
11943
11944 // Only the dirty buffer should have been saved
11945 assert_eq!(
11946 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11947 1,
11948 "Buffer 1 was dirty, so it should have been written once during autosave"
11949 );
11950 assert_eq!(
11951 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11952 0,
11953 "Buffer 2 was clean, so it should not have been written during autosave"
11954 );
11955 assert_eq!(
11956 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11957 0,
11958 "Buffer 3 was clean, so it should not have been written during autosave"
11959 );
11960
11961 // Verify buffer states after autosave
11962 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11963 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11964 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11965
11966 // Now perform a manual save (format = true)
11967 let save_task = editor.update_in(cx, |editor, window, cx| {
11968 editor.save(
11969 SaveOptions {
11970 format: true,
11971 autosave: false,
11972 },
11973 project.clone(),
11974 window,
11975 cx,
11976 )
11977 });
11978 save_task.await.unwrap();
11979
11980 // During manual save, clean buffers don't get written to disk
11981 // They just get did_save called for language server notifications
11982 assert_eq!(
11983 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11984 1,
11985 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11986 );
11987 assert_eq!(
11988 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11989 0,
11990 "Buffer 2 should not have been written at all"
11991 );
11992 assert_eq!(
11993 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11994 0,
11995 "Buffer 3 should not have been written at all"
11996 );
11997}
11998
11999async fn setup_range_format_test(
12000 cx: &mut TestAppContext,
12001) -> (
12002 Entity<Project>,
12003 Entity<Editor>,
12004 &mut gpui::VisualTestContext,
12005 lsp::FakeLanguageServer,
12006) {
12007 init_test(cx, |_| {});
12008
12009 let fs = FakeFs::new(cx.executor());
12010 fs.insert_file(path!("/file.rs"), Default::default()).await;
12011
12012 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12013
12014 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12015 language_registry.add(rust_lang());
12016 let mut fake_servers = language_registry.register_fake_lsp(
12017 "Rust",
12018 FakeLspAdapter {
12019 capabilities: lsp::ServerCapabilities {
12020 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12021 ..lsp::ServerCapabilities::default()
12022 },
12023 ..FakeLspAdapter::default()
12024 },
12025 );
12026
12027 let buffer = project
12028 .update(cx, |project, cx| {
12029 project.open_local_buffer(path!("/file.rs"), cx)
12030 })
12031 .await
12032 .unwrap();
12033
12034 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12035 let (editor, cx) = cx.add_window_view(|window, cx| {
12036 build_editor_with_project(project.clone(), buffer, window, cx)
12037 });
12038
12039 cx.executor().start_waiting();
12040 let fake_server = fake_servers.next().await.unwrap();
12041
12042 (project, editor, cx, fake_server)
12043}
12044
12045#[gpui::test]
12046async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12047 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12048
12049 editor.update_in(cx, |editor, window, cx| {
12050 editor.set_text("one\ntwo\nthree\n", window, cx)
12051 });
12052 assert!(cx.read(|cx| editor.is_dirty(cx)));
12053
12054 let save = editor
12055 .update_in(cx, |editor, window, cx| {
12056 editor.save(
12057 SaveOptions {
12058 format: true,
12059 autosave: false,
12060 },
12061 project.clone(),
12062 window,
12063 cx,
12064 )
12065 })
12066 .unwrap();
12067 fake_server
12068 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12069 assert_eq!(
12070 params.text_document.uri,
12071 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12072 );
12073 assert_eq!(params.options.tab_size, 4);
12074 Ok(Some(vec![lsp::TextEdit::new(
12075 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12076 ", ".to_string(),
12077 )]))
12078 })
12079 .next()
12080 .await;
12081 cx.executor().start_waiting();
12082 save.await;
12083 assert_eq!(
12084 editor.update(cx, |editor, cx| editor.text(cx)),
12085 "one, two\nthree\n"
12086 );
12087 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12088}
12089
12090#[gpui::test]
12091async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12092 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12093
12094 editor.update_in(cx, |editor, window, cx| {
12095 editor.set_text("one\ntwo\nthree\n", window, cx)
12096 });
12097 assert!(cx.read(|cx| editor.is_dirty(cx)));
12098
12099 // Test that save still works when formatting hangs
12100 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12101 move |params, _| async move {
12102 assert_eq!(
12103 params.text_document.uri,
12104 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12105 );
12106 futures::future::pending::<()>().await;
12107 unreachable!()
12108 },
12109 );
12110 let save = editor
12111 .update_in(cx, |editor, window, cx| {
12112 editor.save(
12113 SaveOptions {
12114 format: true,
12115 autosave: false,
12116 },
12117 project.clone(),
12118 window,
12119 cx,
12120 )
12121 })
12122 .unwrap();
12123 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12124 cx.executor().start_waiting();
12125 save.await;
12126 assert_eq!(
12127 editor.update(cx, |editor, cx| editor.text(cx)),
12128 "one\ntwo\nthree\n"
12129 );
12130 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12131}
12132
12133#[gpui::test]
12134async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12135 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12136
12137 // Buffer starts clean, no formatting should be requested
12138 let save = editor
12139 .update_in(cx, |editor, window, cx| {
12140 editor.save(
12141 SaveOptions {
12142 format: false,
12143 autosave: false,
12144 },
12145 project.clone(),
12146 window,
12147 cx,
12148 )
12149 })
12150 .unwrap();
12151 let _pending_format_request = fake_server
12152 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12153 panic!("Should not be invoked");
12154 })
12155 .next();
12156 cx.executor().start_waiting();
12157 save.await;
12158 cx.run_until_parked();
12159}
12160
12161#[gpui::test]
12162async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12163 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12164
12165 // Set Rust language override and assert overridden tabsize is sent to language server
12166 update_test_language_settings(cx, |settings| {
12167 settings.languages.0.insert(
12168 "Rust".into(),
12169 LanguageSettingsContent {
12170 tab_size: NonZeroU32::new(8),
12171 ..Default::default()
12172 },
12173 );
12174 });
12175
12176 editor.update_in(cx, |editor, window, cx| {
12177 editor.set_text("something_new\n", window, cx)
12178 });
12179 assert!(cx.read(|cx| editor.is_dirty(cx)));
12180 let save = editor
12181 .update_in(cx, |editor, window, cx| {
12182 editor.save(
12183 SaveOptions {
12184 format: true,
12185 autosave: false,
12186 },
12187 project.clone(),
12188 window,
12189 cx,
12190 )
12191 })
12192 .unwrap();
12193 fake_server
12194 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12195 assert_eq!(
12196 params.text_document.uri,
12197 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12198 );
12199 assert_eq!(params.options.tab_size, 8);
12200 Ok(Some(Vec::new()))
12201 })
12202 .next()
12203 .await;
12204 save.await;
12205}
12206
12207#[gpui::test]
12208async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12209 init_test(cx, |settings| {
12210 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12211 settings::LanguageServerFormatterSpecifier::Current,
12212 )))
12213 });
12214
12215 let fs = FakeFs::new(cx.executor());
12216 fs.insert_file(path!("/file.rs"), Default::default()).await;
12217
12218 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12219
12220 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12221 language_registry.add(Arc::new(Language::new(
12222 LanguageConfig {
12223 name: "Rust".into(),
12224 matcher: LanguageMatcher {
12225 path_suffixes: vec!["rs".to_string()],
12226 ..Default::default()
12227 },
12228 ..LanguageConfig::default()
12229 },
12230 Some(tree_sitter_rust::LANGUAGE.into()),
12231 )));
12232 update_test_language_settings(cx, |settings| {
12233 // Enable Prettier formatting for the same buffer, and ensure
12234 // LSP is called instead of Prettier.
12235 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12236 });
12237 let mut fake_servers = language_registry.register_fake_lsp(
12238 "Rust",
12239 FakeLspAdapter {
12240 capabilities: lsp::ServerCapabilities {
12241 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12242 ..Default::default()
12243 },
12244 ..Default::default()
12245 },
12246 );
12247
12248 let buffer = project
12249 .update(cx, |project, cx| {
12250 project.open_local_buffer(path!("/file.rs"), cx)
12251 })
12252 .await
12253 .unwrap();
12254
12255 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12256 let (editor, cx) = cx.add_window_view(|window, cx| {
12257 build_editor_with_project(project.clone(), buffer, window, cx)
12258 });
12259 editor.update_in(cx, |editor, window, cx| {
12260 editor.set_text("one\ntwo\nthree\n", window, cx)
12261 });
12262
12263 cx.executor().start_waiting();
12264 let fake_server = fake_servers.next().await.unwrap();
12265
12266 let format = editor
12267 .update_in(cx, |editor, window, cx| {
12268 editor.perform_format(
12269 project.clone(),
12270 FormatTrigger::Manual,
12271 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12272 window,
12273 cx,
12274 )
12275 })
12276 .unwrap();
12277 fake_server
12278 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12279 assert_eq!(
12280 params.text_document.uri,
12281 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12282 );
12283 assert_eq!(params.options.tab_size, 4);
12284 Ok(Some(vec![lsp::TextEdit::new(
12285 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12286 ", ".to_string(),
12287 )]))
12288 })
12289 .next()
12290 .await;
12291 cx.executor().start_waiting();
12292 format.await;
12293 assert_eq!(
12294 editor.update(cx, |editor, cx| editor.text(cx)),
12295 "one, two\nthree\n"
12296 );
12297
12298 editor.update_in(cx, |editor, window, cx| {
12299 editor.set_text("one\ntwo\nthree\n", window, cx)
12300 });
12301 // Ensure we don't lock if formatting hangs.
12302 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12303 move |params, _| async move {
12304 assert_eq!(
12305 params.text_document.uri,
12306 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12307 );
12308 futures::future::pending::<()>().await;
12309 unreachable!()
12310 },
12311 );
12312 let format = editor
12313 .update_in(cx, |editor, window, cx| {
12314 editor.perform_format(
12315 project,
12316 FormatTrigger::Manual,
12317 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12318 window,
12319 cx,
12320 )
12321 })
12322 .unwrap();
12323 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12324 cx.executor().start_waiting();
12325 format.await;
12326 assert_eq!(
12327 editor.update(cx, |editor, cx| editor.text(cx)),
12328 "one\ntwo\nthree\n"
12329 );
12330}
12331
12332#[gpui::test]
12333async fn test_multiple_formatters(cx: &mut TestAppContext) {
12334 init_test(cx, |settings| {
12335 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12336 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12337 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12338 Formatter::CodeAction("code-action-1".into()),
12339 Formatter::CodeAction("code-action-2".into()),
12340 ]))
12341 });
12342
12343 let fs = FakeFs::new(cx.executor());
12344 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12345 .await;
12346
12347 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12348 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12349 language_registry.add(rust_lang());
12350
12351 let mut fake_servers = language_registry.register_fake_lsp(
12352 "Rust",
12353 FakeLspAdapter {
12354 capabilities: lsp::ServerCapabilities {
12355 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12356 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12357 commands: vec!["the-command-for-code-action-1".into()],
12358 ..Default::default()
12359 }),
12360 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12361 ..Default::default()
12362 },
12363 ..Default::default()
12364 },
12365 );
12366
12367 let buffer = project
12368 .update(cx, |project, cx| {
12369 project.open_local_buffer(path!("/file.rs"), cx)
12370 })
12371 .await
12372 .unwrap();
12373
12374 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12375 let (editor, cx) = cx.add_window_view(|window, cx| {
12376 build_editor_with_project(project.clone(), buffer, window, cx)
12377 });
12378
12379 cx.executor().start_waiting();
12380
12381 let fake_server = fake_servers.next().await.unwrap();
12382 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12383 move |_params, _| async move {
12384 Ok(Some(vec![lsp::TextEdit::new(
12385 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12386 "applied-formatting\n".to_string(),
12387 )]))
12388 },
12389 );
12390 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12391 move |params, _| async move {
12392 let requested_code_actions = params.context.only.expect("Expected code action request");
12393 assert_eq!(requested_code_actions.len(), 1);
12394
12395 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12396 let code_action = match requested_code_actions[0].as_str() {
12397 "code-action-1" => lsp::CodeAction {
12398 kind: Some("code-action-1".into()),
12399 edit: Some(lsp::WorkspaceEdit::new(
12400 [(
12401 uri,
12402 vec![lsp::TextEdit::new(
12403 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12404 "applied-code-action-1-edit\n".to_string(),
12405 )],
12406 )]
12407 .into_iter()
12408 .collect(),
12409 )),
12410 command: Some(lsp::Command {
12411 command: "the-command-for-code-action-1".into(),
12412 ..Default::default()
12413 }),
12414 ..Default::default()
12415 },
12416 "code-action-2" => lsp::CodeAction {
12417 kind: Some("code-action-2".into()),
12418 edit: Some(lsp::WorkspaceEdit::new(
12419 [(
12420 uri,
12421 vec![lsp::TextEdit::new(
12422 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12423 "applied-code-action-2-edit\n".to_string(),
12424 )],
12425 )]
12426 .into_iter()
12427 .collect(),
12428 )),
12429 ..Default::default()
12430 },
12431 req => panic!("Unexpected code action request: {:?}", req),
12432 };
12433 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12434 code_action,
12435 )]))
12436 },
12437 );
12438
12439 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12440 move |params, _| async move { Ok(params) }
12441 });
12442
12443 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12444 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12445 let fake = fake_server.clone();
12446 let lock = command_lock.clone();
12447 move |params, _| {
12448 assert_eq!(params.command, "the-command-for-code-action-1");
12449 let fake = fake.clone();
12450 let lock = lock.clone();
12451 async move {
12452 lock.lock().await;
12453 fake.server
12454 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12455 label: None,
12456 edit: lsp::WorkspaceEdit {
12457 changes: Some(
12458 [(
12459 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12460 vec![lsp::TextEdit {
12461 range: lsp::Range::new(
12462 lsp::Position::new(0, 0),
12463 lsp::Position::new(0, 0),
12464 ),
12465 new_text: "applied-code-action-1-command\n".into(),
12466 }],
12467 )]
12468 .into_iter()
12469 .collect(),
12470 ),
12471 ..Default::default()
12472 },
12473 })
12474 .await
12475 .into_response()
12476 .unwrap();
12477 Ok(Some(json!(null)))
12478 }
12479 }
12480 });
12481
12482 cx.executor().start_waiting();
12483 editor
12484 .update_in(cx, |editor, window, cx| {
12485 editor.perform_format(
12486 project.clone(),
12487 FormatTrigger::Manual,
12488 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12489 window,
12490 cx,
12491 )
12492 })
12493 .unwrap()
12494 .await;
12495 editor.update(cx, |editor, cx| {
12496 assert_eq!(
12497 editor.text(cx),
12498 r#"
12499 applied-code-action-2-edit
12500 applied-code-action-1-command
12501 applied-code-action-1-edit
12502 applied-formatting
12503 one
12504 two
12505 three
12506 "#
12507 .unindent()
12508 );
12509 });
12510
12511 editor.update_in(cx, |editor, window, cx| {
12512 editor.undo(&Default::default(), window, cx);
12513 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12514 });
12515
12516 // Perform a manual edit while waiting for an LSP command
12517 // that's being run as part of a formatting code action.
12518 let lock_guard = command_lock.lock().await;
12519 let format = editor
12520 .update_in(cx, |editor, window, cx| {
12521 editor.perform_format(
12522 project.clone(),
12523 FormatTrigger::Manual,
12524 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12525 window,
12526 cx,
12527 )
12528 })
12529 .unwrap();
12530 cx.run_until_parked();
12531 editor.update(cx, |editor, cx| {
12532 assert_eq!(
12533 editor.text(cx),
12534 r#"
12535 applied-code-action-1-edit
12536 applied-formatting
12537 one
12538 two
12539 three
12540 "#
12541 .unindent()
12542 );
12543
12544 editor.buffer.update(cx, |buffer, cx| {
12545 let ix = buffer.len(cx);
12546 buffer.edit([(ix..ix, "edited\n")], None, cx);
12547 });
12548 });
12549
12550 // Allow the LSP command to proceed. Because the buffer was edited,
12551 // the second code action will not be run.
12552 drop(lock_guard);
12553 format.await;
12554 editor.update_in(cx, |editor, window, cx| {
12555 assert_eq!(
12556 editor.text(cx),
12557 r#"
12558 applied-code-action-1-command
12559 applied-code-action-1-edit
12560 applied-formatting
12561 one
12562 two
12563 three
12564 edited
12565 "#
12566 .unindent()
12567 );
12568
12569 // The manual edit is undone first, because it is the last thing the user did
12570 // (even though the command completed afterwards).
12571 editor.undo(&Default::default(), window, cx);
12572 assert_eq!(
12573 editor.text(cx),
12574 r#"
12575 applied-code-action-1-command
12576 applied-code-action-1-edit
12577 applied-formatting
12578 one
12579 two
12580 three
12581 "#
12582 .unindent()
12583 );
12584
12585 // All the formatting (including the command, which completed after the manual edit)
12586 // is undone together.
12587 editor.undo(&Default::default(), window, cx);
12588 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12589 });
12590}
12591
12592#[gpui::test]
12593async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12594 init_test(cx, |settings| {
12595 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12596 settings::LanguageServerFormatterSpecifier::Current,
12597 )]))
12598 });
12599
12600 let fs = FakeFs::new(cx.executor());
12601 fs.insert_file(path!("/file.ts"), Default::default()).await;
12602
12603 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12604
12605 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12606 language_registry.add(Arc::new(Language::new(
12607 LanguageConfig {
12608 name: "TypeScript".into(),
12609 matcher: LanguageMatcher {
12610 path_suffixes: vec!["ts".to_string()],
12611 ..Default::default()
12612 },
12613 ..LanguageConfig::default()
12614 },
12615 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12616 )));
12617 update_test_language_settings(cx, |settings| {
12618 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12619 });
12620 let mut fake_servers = language_registry.register_fake_lsp(
12621 "TypeScript",
12622 FakeLspAdapter {
12623 capabilities: lsp::ServerCapabilities {
12624 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12625 ..Default::default()
12626 },
12627 ..Default::default()
12628 },
12629 );
12630
12631 let buffer = project
12632 .update(cx, |project, cx| {
12633 project.open_local_buffer(path!("/file.ts"), cx)
12634 })
12635 .await
12636 .unwrap();
12637
12638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12639 let (editor, cx) = cx.add_window_view(|window, cx| {
12640 build_editor_with_project(project.clone(), buffer, window, cx)
12641 });
12642 editor.update_in(cx, |editor, window, cx| {
12643 editor.set_text(
12644 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12645 window,
12646 cx,
12647 )
12648 });
12649
12650 cx.executor().start_waiting();
12651 let fake_server = fake_servers.next().await.unwrap();
12652
12653 let format = editor
12654 .update_in(cx, |editor, window, cx| {
12655 editor.perform_code_action_kind(
12656 project.clone(),
12657 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12658 window,
12659 cx,
12660 )
12661 })
12662 .unwrap();
12663 fake_server
12664 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12665 assert_eq!(
12666 params.text_document.uri,
12667 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12668 );
12669 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12670 lsp::CodeAction {
12671 title: "Organize Imports".to_string(),
12672 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12673 edit: Some(lsp::WorkspaceEdit {
12674 changes: Some(
12675 [(
12676 params.text_document.uri.clone(),
12677 vec![lsp::TextEdit::new(
12678 lsp::Range::new(
12679 lsp::Position::new(1, 0),
12680 lsp::Position::new(2, 0),
12681 ),
12682 "".to_string(),
12683 )],
12684 )]
12685 .into_iter()
12686 .collect(),
12687 ),
12688 ..Default::default()
12689 }),
12690 ..Default::default()
12691 },
12692 )]))
12693 })
12694 .next()
12695 .await;
12696 cx.executor().start_waiting();
12697 format.await;
12698 assert_eq!(
12699 editor.update(cx, |editor, cx| editor.text(cx)),
12700 "import { a } from 'module';\n\nconst x = a;\n"
12701 );
12702
12703 editor.update_in(cx, |editor, window, cx| {
12704 editor.set_text(
12705 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12706 window,
12707 cx,
12708 )
12709 });
12710 // Ensure we don't lock if code action hangs.
12711 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12712 move |params, _| async move {
12713 assert_eq!(
12714 params.text_document.uri,
12715 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12716 );
12717 futures::future::pending::<()>().await;
12718 unreachable!()
12719 },
12720 );
12721 let format = editor
12722 .update_in(cx, |editor, window, cx| {
12723 editor.perform_code_action_kind(
12724 project,
12725 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12726 window,
12727 cx,
12728 )
12729 })
12730 .unwrap();
12731 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12732 cx.executor().start_waiting();
12733 format.await;
12734 assert_eq!(
12735 editor.update(cx, |editor, cx| editor.text(cx)),
12736 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12737 );
12738}
12739
12740#[gpui::test]
12741async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12742 init_test(cx, |_| {});
12743
12744 let mut cx = EditorLspTestContext::new_rust(
12745 lsp::ServerCapabilities {
12746 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12747 ..Default::default()
12748 },
12749 cx,
12750 )
12751 .await;
12752
12753 cx.set_state(indoc! {"
12754 one.twoˇ
12755 "});
12756
12757 // The format request takes a long time. When it completes, it inserts
12758 // a newline and an indent before the `.`
12759 cx.lsp
12760 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12761 let executor = cx.background_executor().clone();
12762 async move {
12763 executor.timer(Duration::from_millis(100)).await;
12764 Ok(Some(vec![lsp::TextEdit {
12765 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12766 new_text: "\n ".into(),
12767 }]))
12768 }
12769 });
12770
12771 // Submit a format request.
12772 let format_1 = cx
12773 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12774 .unwrap();
12775 cx.executor().run_until_parked();
12776
12777 // Submit a second format request.
12778 let format_2 = cx
12779 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12780 .unwrap();
12781 cx.executor().run_until_parked();
12782
12783 // Wait for both format requests to complete
12784 cx.executor().advance_clock(Duration::from_millis(200));
12785 cx.executor().start_waiting();
12786 format_1.await.unwrap();
12787 cx.executor().start_waiting();
12788 format_2.await.unwrap();
12789
12790 // The formatting edits only happens once.
12791 cx.assert_editor_state(indoc! {"
12792 one
12793 .twoˇ
12794 "});
12795}
12796
12797#[gpui::test]
12798async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12799 init_test(cx, |settings| {
12800 settings.defaults.formatter = Some(FormatterList::default())
12801 });
12802
12803 let mut cx = EditorLspTestContext::new_rust(
12804 lsp::ServerCapabilities {
12805 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12806 ..Default::default()
12807 },
12808 cx,
12809 )
12810 .await;
12811
12812 // Record which buffer changes have been sent to the language server
12813 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12814 cx.lsp
12815 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12816 let buffer_changes = buffer_changes.clone();
12817 move |params, _| {
12818 buffer_changes.lock().extend(
12819 params
12820 .content_changes
12821 .into_iter()
12822 .map(|e| (e.range.unwrap(), e.text)),
12823 );
12824 }
12825 });
12826 // Handle formatting requests to the language server.
12827 cx.lsp
12828 .set_request_handler::<lsp::request::Formatting, _, _>({
12829 let buffer_changes = buffer_changes.clone();
12830 move |_, _| {
12831 let buffer_changes = buffer_changes.clone();
12832 // Insert blank lines between each line of the buffer.
12833 async move {
12834 // When formatting is requested, trailing whitespace has already been stripped,
12835 // and the trailing newline has already been added.
12836 assert_eq!(
12837 &buffer_changes.lock()[1..],
12838 &[
12839 (
12840 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12841 "".into()
12842 ),
12843 (
12844 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12845 "".into()
12846 ),
12847 (
12848 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12849 "\n".into()
12850 ),
12851 ]
12852 );
12853
12854 Ok(Some(vec![
12855 lsp::TextEdit {
12856 range: lsp::Range::new(
12857 lsp::Position::new(1, 0),
12858 lsp::Position::new(1, 0),
12859 ),
12860 new_text: "\n".into(),
12861 },
12862 lsp::TextEdit {
12863 range: lsp::Range::new(
12864 lsp::Position::new(2, 0),
12865 lsp::Position::new(2, 0),
12866 ),
12867 new_text: "\n".into(),
12868 },
12869 ]))
12870 }
12871 }
12872 });
12873
12874 // Set up a buffer white some trailing whitespace and no trailing newline.
12875 cx.set_state(
12876 &[
12877 "one ", //
12878 "twoˇ", //
12879 "three ", //
12880 "four", //
12881 ]
12882 .join("\n"),
12883 );
12884 cx.run_until_parked();
12885
12886 // Submit a format request.
12887 let format = cx
12888 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12889 .unwrap();
12890
12891 cx.run_until_parked();
12892 // After formatting the buffer, the trailing whitespace is stripped,
12893 // a newline is appended, and the edits provided by the language server
12894 // have been applied.
12895 format.await.unwrap();
12896
12897 cx.assert_editor_state(
12898 &[
12899 "one", //
12900 "", //
12901 "twoˇ", //
12902 "", //
12903 "three", //
12904 "four", //
12905 "", //
12906 ]
12907 .join("\n"),
12908 );
12909
12910 // Undoing the formatting undoes the trailing whitespace removal, the
12911 // trailing newline, and the LSP edits.
12912 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12913 cx.assert_editor_state(
12914 &[
12915 "one ", //
12916 "twoˇ", //
12917 "three ", //
12918 "four", //
12919 ]
12920 .join("\n"),
12921 );
12922}
12923
12924#[gpui::test]
12925async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12926 cx: &mut TestAppContext,
12927) {
12928 init_test(cx, |_| {});
12929
12930 cx.update(|cx| {
12931 cx.update_global::<SettingsStore, _>(|settings, cx| {
12932 settings.update_user_settings(cx, |settings| {
12933 settings.editor.auto_signature_help = Some(true);
12934 });
12935 });
12936 });
12937
12938 let mut cx = EditorLspTestContext::new_rust(
12939 lsp::ServerCapabilities {
12940 signature_help_provider: Some(lsp::SignatureHelpOptions {
12941 ..Default::default()
12942 }),
12943 ..Default::default()
12944 },
12945 cx,
12946 )
12947 .await;
12948
12949 let language = Language::new(
12950 LanguageConfig {
12951 name: "Rust".into(),
12952 brackets: BracketPairConfig {
12953 pairs: vec![
12954 BracketPair {
12955 start: "{".to_string(),
12956 end: "}".to_string(),
12957 close: true,
12958 surround: true,
12959 newline: true,
12960 },
12961 BracketPair {
12962 start: "(".to_string(),
12963 end: ")".to_string(),
12964 close: true,
12965 surround: true,
12966 newline: true,
12967 },
12968 BracketPair {
12969 start: "/*".to_string(),
12970 end: " */".to_string(),
12971 close: true,
12972 surround: true,
12973 newline: true,
12974 },
12975 BracketPair {
12976 start: "[".to_string(),
12977 end: "]".to_string(),
12978 close: false,
12979 surround: false,
12980 newline: true,
12981 },
12982 BracketPair {
12983 start: "\"".to_string(),
12984 end: "\"".to_string(),
12985 close: true,
12986 surround: true,
12987 newline: false,
12988 },
12989 BracketPair {
12990 start: "<".to_string(),
12991 end: ">".to_string(),
12992 close: false,
12993 surround: true,
12994 newline: true,
12995 },
12996 ],
12997 ..Default::default()
12998 },
12999 autoclose_before: "})]".to_string(),
13000 ..Default::default()
13001 },
13002 Some(tree_sitter_rust::LANGUAGE.into()),
13003 );
13004 let language = Arc::new(language);
13005
13006 cx.language_registry().add(language.clone());
13007 cx.update_buffer(|buffer, cx| {
13008 buffer.set_language(Some(language), cx);
13009 });
13010
13011 cx.set_state(
13012 &r#"
13013 fn main() {
13014 sampleˇ
13015 }
13016 "#
13017 .unindent(),
13018 );
13019
13020 cx.update_editor(|editor, window, cx| {
13021 editor.handle_input("(", window, cx);
13022 });
13023 cx.assert_editor_state(
13024 &"
13025 fn main() {
13026 sample(ˇ)
13027 }
13028 "
13029 .unindent(),
13030 );
13031
13032 let mocked_response = lsp::SignatureHelp {
13033 signatures: vec![lsp::SignatureInformation {
13034 label: "fn sample(param1: u8, param2: u8)".to_string(),
13035 documentation: None,
13036 parameters: Some(vec![
13037 lsp::ParameterInformation {
13038 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13039 documentation: None,
13040 },
13041 lsp::ParameterInformation {
13042 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13043 documentation: None,
13044 },
13045 ]),
13046 active_parameter: None,
13047 }],
13048 active_signature: Some(0),
13049 active_parameter: Some(0),
13050 };
13051 handle_signature_help_request(&mut cx, mocked_response).await;
13052
13053 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13054 .await;
13055
13056 cx.editor(|editor, _, _| {
13057 let signature_help_state = editor.signature_help_state.popover().cloned();
13058 let signature = signature_help_state.unwrap();
13059 assert_eq!(
13060 signature.signatures[signature.current_signature].label,
13061 "fn sample(param1: u8, param2: u8)"
13062 );
13063 });
13064}
13065
13066#[gpui::test]
13067async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13068 init_test(cx, |_| {});
13069
13070 cx.update(|cx| {
13071 cx.update_global::<SettingsStore, _>(|settings, cx| {
13072 settings.update_user_settings(cx, |settings| {
13073 settings.editor.auto_signature_help = Some(false);
13074 settings.editor.show_signature_help_after_edits = Some(false);
13075 });
13076 });
13077 });
13078
13079 let mut cx = EditorLspTestContext::new_rust(
13080 lsp::ServerCapabilities {
13081 signature_help_provider: Some(lsp::SignatureHelpOptions {
13082 ..Default::default()
13083 }),
13084 ..Default::default()
13085 },
13086 cx,
13087 )
13088 .await;
13089
13090 let language = Language::new(
13091 LanguageConfig {
13092 name: "Rust".into(),
13093 brackets: BracketPairConfig {
13094 pairs: vec![
13095 BracketPair {
13096 start: "{".to_string(),
13097 end: "}".to_string(),
13098 close: true,
13099 surround: true,
13100 newline: true,
13101 },
13102 BracketPair {
13103 start: "(".to_string(),
13104 end: ")".to_string(),
13105 close: true,
13106 surround: true,
13107 newline: true,
13108 },
13109 BracketPair {
13110 start: "/*".to_string(),
13111 end: " */".to_string(),
13112 close: true,
13113 surround: true,
13114 newline: true,
13115 },
13116 BracketPair {
13117 start: "[".to_string(),
13118 end: "]".to_string(),
13119 close: false,
13120 surround: false,
13121 newline: true,
13122 },
13123 BracketPair {
13124 start: "\"".to_string(),
13125 end: "\"".to_string(),
13126 close: true,
13127 surround: true,
13128 newline: false,
13129 },
13130 BracketPair {
13131 start: "<".to_string(),
13132 end: ">".to_string(),
13133 close: false,
13134 surround: true,
13135 newline: true,
13136 },
13137 ],
13138 ..Default::default()
13139 },
13140 autoclose_before: "})]".to_string(),
13141 ..Default::default()
13142 },
13143 Some(tree_sitter_rust::LANGUAGE.into()),
13144 );
13145 let language = Arc::new(language);
13146
13147 cx.language_registry().add(language.clone());
13148 cx.update_buffer(|buffer, cx| {
13149 buffer.set_language(Some(language), cx);
13150 });
13151
13152 // Ensure that signature_help is not called when no signature help is enabled.
13153 cx.set_state(
13154 &r#"
13155 fn main() {
13156 sampleˇ
13157 }
13158 "#
13159 .unindent(),
13160 );
13161 cx.update_editor(|editor, window, cx| {
13162 editor.handle_input("(", window, cx);
13163 });
13164 cx.assert_editor_state(
13165 &"
13166 fn main() {
13167 sample(ˇ)
13168 }
13169 "
13170 .unindent(),
13171 );
13172 cx.editor(|editor, _, _| {
13173 assert!(editor.signature_help_state.task().is_none());
13174 });
13175
13176 let mocked_response = lsp::SignatureHelp {
13177 signatures: vec![lsp::SignatureInformation {
13178 label: "fn sample(param1: u8, param2: u8)".to_string(),
13179 documentation: None,
13180 parameters: Some(vec![
13181 lsp::ParameterInformation {
13182 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13183 documentation: None,
13184 },
13185 lsp::ParameterInformation {
13186 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13187 documentation: None,
13188 },
13189 ]),
13190 active_parameter: None,
13191 }],
13192 active_signature: Some(0),
13193 active_parameter: Some(0),
13194 };
13195
13196 // Ensure that signature_help is called when enabled afte edits
13197 cx.update(|_, cx| {
13198 cx.update_global::<SettingsStore, _>(|settings, cx| {
13199 settings.update_user_settings(cx, |settings| {
13200 settings.editor.auto_signature_help = Some(false);
13201 settings.editor.show_signature_help_after_edits = Some(true);
13202 });
13203 });
13204 });
13205 cx.set_state(
13206 &r#"
13207 fn main() {
13208 sampleˇ
13209 }
13210 "#
13211 .unindent(),
13212 );
13213 cx.update_editor(|editor, window, cx| {
13214 editor.handle_input("(", window, cx);
13215 });
13216 cx.assert_editor_state(
13217 &"
13218 fn main() {
13219 sample(ˇ)
13220 }
13221 "
13222 .unindent(),
13223 );
13224 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13225 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13226 .await;
13227 cx.update_editor(|editor, _, _| {
13228 let signature_help_state = editor.signature_help_state.popover().cloned();
13229 assert!(signature_help_state.is_some());
13230 let signature = signature_help_state.unwrap();
13231 assert_eq!(
13232 signature.signatures[signature.current_signature].label,
13233 "fn sample(param1: u8, param2: u8)"
13234 );
13235 editor.signature_help_state = SignatureHelpState::default();
13236 });
13237
13238 // Ensure that signature_help is called when auto signature help override is enabled
13239 cx.update(|_, cx| {
13240 cx.update_global::<SettingsStore, _>(|settings, cx| {
13241 settings.update_user_settings(cx, |settings| {
13242 settings.editor.auto_signature_help = Some(true);
13243 settings.editor.show_signature_help_after_edits = Some(false);
13244 });
13245 });
13246 });
13247 cx.set_state(
13248 &r#"
13249 fn main() {
13250 sampleˇ
13251 }
13252 "#
13253 .unindent(),
13254 );
13255 cx.update_editor(|editor, window, cx| {
13256 editor.handle_input("(", window, cx);
13257 });
13258 cx.assert_editor_state(
13259 &"
13260 fn main() {
13261 sample(ˇ)
13262 }
13263 "
13264 .unindent(),
13265 );
13266 handle_signature_help_request(&mut cx, mocked_response).await;
13267 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13268 .await;
13269 cx.editor(|editor, _, _| {
13270 let signature_help_state = editor.signature_help_state.popover().cloned();
13271 assert!(signature_help_state.is_some());
13272 let signature = signature_help_state.unwrap();
13273 assert_eq!(
13274 signature.signatures[signature.current_signature].label,
13275 "fn sample(param1: u8, param2: u8)"
13276 );
13277 });
13278}
13279
13280#[gpui::test]
13281async fn test_signature_help(cx: &mut TestAppContext) {
13282 init_test(cx, |_| {});
13283 cx.update(|cx| {
13284 cx.update_global::<SettingsStore, _>(|settings, cx| {
13285 settings.update_user_settings(cx, |settings| {
13286 settings.editor.auto_signature_help = Some(true);
13287 });
13288 });
13289 });
13290
13291 let mut cx = EditorLspTestContext::new_rust(
13292 lsp::ServerCapabilities {
13293 signature_help_provider: Some(lsp::SignatureHelpOptions {
13294 ..Default::default()
13295 }),
13296 ..Default::default()
13297 },
13298 cx,
13299 )
13300 .await;
13301
13302 // A test that directly calls `show_signature_help`
13303 cx.update_editor(|editor, window, cx| {
13304 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13305 });
13306
13307 let mocked_response = lsp::SignatureHelp {
13308 signatures: vec![lsp::SignatureInformation {
13309 label: "fn sample(param1: u8, param2: u8)".to_string(),
13310 documentation: None,
13311 parameters: Some(vec![
13312 lsp::ParameterInformation {
13313 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13314 documentation: None,
13315 },
13316 lsp::ParameterInformation {
13317 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13318 documentation: None,
13319 },
13320 ]),
13321 active_parameter: None,
13322 }],
13323 active_signature: Some(0),
13324 active_parameter: Some(0),
13325 };
13326 handle_signature_help_request(&mut cx, mocked_response).await;
13327
13328 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13329 .await;
13330
13331 cx.editor(|editor, _, _| {
13332 let signature_help_state = editor.signature_help_state.popover().cloned();
13333 assert!(signature_help_state.is_some());
13334 let signature = signature_help_state.unwrap();
13335 assert_eq!(
13336 signature.signatures[signature.current_signature].label,
13337 "fn sample(param1: u8, param2: u8)"
13338 );
13339 });
13340
13341 // When exiting outside from inside the brackets, `signature_help` is closed.
13342 cx.set_state(indoc! {"
13343 fn main() {
13344 sample(ˇ);
13345 }
13346
13347 fn sample(param1: u8, param2: u8) {}
13348 "});
13349
13350 cx.update_editor(|editor, window, cx| {
13351 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13352 s.select_ranges([0..0])
13353 });
13354 });
13355
13356 let mocked_response = lsp::SignatureHelp {
13357 signatures: Vec::new(),
13358 active_signature: None,
13359 active_parameter: None,
13360 };
13361 handle_signature_help_request(&mut cx, mocked_response).await;
13362
13363 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13364 .await;
13365
13366 cx.editor(|editor, _, _| {
13367 assert!(!editor.signature_help_state.is_shown());
13368 });
13369
13370 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13371 cx.set_state(indoc! {"
13372 fn main() {
13373 sample(ˇ);
13374 }
13375
13376 fn sample(param1: u8, param2: u8) {}
13377 "});
13378
13379 let mocked_response = lsp::SignatureHelp {
13380 signatures: vec![lsp::SignatureInformation {
13381 label: "fn sample(param1: u8, param2: u8)".to_string(),
13382 documentation: None,
13383 parameters: Some(vec![
13384 lsp::ParameterInformation {
13385 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13386 documentation: None,
13387 },
13388 lsp::ParameterInformation {
13389 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13390 documentation: None,
13391 },
13392 ]),
13393 active_parameter: None,
13394 }],
13395 active_signature: Some(0),
13396 active_parameter: Some(0),
13397 };
13398 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13399 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13400 .await;
13401 cx.editor(|editor, _, _| {
13402 assert!(editor.signature_help_state.is_shown());
13403 });
13404
13405 // Restore the popover with more parameter input
13406 cx.set_state(indoc! {"
13407 fn main() {
13408 sample(param1, param2ˇ);
13409 }
13410
13411 fn sample(param1: u8, param2: u8) {}
13412 "});
13413
13414 let mocked_response = lsp::SignatureHelp {
13415 signatures: vec![lsp::SignatureInformation {
13416 label: "fn sample(param1: u8, param2: u8)".to_string(),
13417 documentation: None,
13418 parameters: Some(vec![
13419 lsp::ParameterInformation {
13420 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13421 documentation: None,
13422 },
13423 lsp::ParameterInformation {
13424 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13425 documentation: None,
13426 },
13427 ]),
13428 active_parameter: None,
13429 }],
13430 active_signature: Some(0),
13431 active_parameter: Some(1),
13432 };
13433 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13434 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13435 .await;
13436
13437 // When selecting a range, the popover is gone.
13438 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13439 cx.update_editor(|editor, window, cx| {
13440 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13441 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13442 })
13443 });
13444 cx.assert_editor_state(indoc! {"
13445 fn main() {
13446 sample(param1, «ˇparam2»);
13447 }
13448
13449 fn sample(param1: u8, param2: u8) {}
13450 "});
13451 cx.editor(|editor, _, _| {
13452 assert!(!editor.signature_help_state.is_shown());
13453 });
13454
13455 // When unselecting again, the popover is back if within the brackets.
13456 cx.update_editor(|editor, window, cx| {
13457 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13458 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13459 })
13460 });
13461 cx.assert_editor_state(indoc! {"
13462 fn main() {
13463 sample(param1, ˇparam2);
13464 }
13465
13466 fn sample(param1: u8, param2: u8) {}
13467 "});
13468 handle_signature_help_request(&mut cx, mocked_response).await;
13469 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13470 .await;
13471 cx.editor(|editor, _, _| {
13472 assert!(editor.signature_help_state.is_shown());
13473 });
13474
13475 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13476 cx.update_editor(|editor, window, cx| {
13477 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13478 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13479 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13480 })
13481 });
13482 cx.assert_editor_state(indoc! {"
13483 fn main() {
13484 sample(param1, ˇparam2);
13485 }
13486
13487 fn sample(param1: u8, param2: u8) {}
13488 "});
13489
13490 let mocked_response = lsp::SignatureHelp {
13491 signatures: vec![lsp::SignatureInformation {
13492 label: "fn sample(param1: u8, param2: u8)".to_string(),
13493 documentation: None,
13494 parameters: Some(vec![
13495 lsp::ParameterInformation {
13496 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13497 documentation: None,
13498 },
13499 lsp::ParameterInformation {
13500 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13501 documentation: None,
13502 },
13503 ]),
13504 active_parameter: None,
13505 }],
13506 active_signature: Some(0),
13507 active_parameter: Some(1),
13508 };
13509 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13510 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13511 .await;
13512 cx.update_editor(|editor, _, cx| {
13513 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13514 });
13515 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13516 .await;
13517 cx.update_editor(|editor, window, cx| {
13518 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13519 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13520 })
13521 });
13522 cx.assert_editor_state(indoc! {"
13523 fn main() {
13524 sample(param1, «ˇparam2»);
13525 }
13526
13527 fn sample(param1: u8, param2: u8) {}
13528 "});
13529 cx.update_editor(|editor, window, cx| {
13530 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13531 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13532 })
13533 });
13534 cx.assert_editor_state(indoc! {"
13535 fn main() {
13536 sample(param1, ˇparam2);
13537 }
13538
13539 fn sample(param1: u8, param2: u8) {}
13540 "});
13541 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13542 .await;
13543}
13544
13545#[gpui::test]
13546async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13547 init_test(cx, |_| {});
13548
13549 let mut cx = EditorLspTestContext::new_rust(
13550 lsp::ServerCapabilities {
13551 signature_help_provider: Some(lsp::SignatureHelpOptions {
13552 ..Default::default()
13553 }),
13554 ..Default::default()
13555 },
13556 cx,
13557 )
13558 .await;
13559
13560 cx.set_state(indoc! {"
13561 fn main() {
13562 overloadedˇ
13563 }
13564 "});
13565
13566 cx.update_editor(|editor, window, cx| {
13567 editor.handle_input("(", window, cx);
13568 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13569 });
13570
13571 // Mock response with 3 signatures
13572 let mocked_response = lsp::SignatureHelp {
13573 signatures: vec![
13574 lsp::SignatureInformation {
13575 label: "fn overloaded(x: i32)".to_string(),
13576 documentation: None,
13577 parameters: Some(vec![lsp::ParameterInformation {
13578 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13579 documentation: None,
13580 }]),
13581 active_parameter: None,
13582 },
13583 lsp::SignatureInformation {
13584 label: "fn overloaded(x: i32, y: i32)".to_string(),
13585 documentation: None,
13586 parameters: Some(vec![
13587 lsp::ParameterInformation {
13588 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13589 documentation: None,
13590 },
13591 lsp::ParameterInformation {
13592 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13593 documentation: None,
13594 },
13595 ]),
13596 active_parameter: None,
13597 },
13598 lsp::SignatureInformation {
13599 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13600 documentation: None,
13601 parameters: Some(vec![
13602 lsp::ParameterInformation {
13603 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13604 documentation: None,
13605 },
13606 lsp::ParameterInformation {
13607 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13608 documentation: None,
13609 },
13610 lsp::ParameterInformation {
13611 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13612 documentation: None,
13613 },
13614 ]),
13615 active_parameter: None,
13616 },
13617 ],
13618 active_signature: Some(1),
13619 active_parameter: Some(0),
13620 };
13621 handle_signature_help_request(&mut cx, mocked_response).await;
13622
13623 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13624 .await;
13625
13626 // Verify we have multiple signatures and the right one is selected
13627 cx.editor(|editor, _, _| {
13628 let popover = editor.signature_help_state.popover().cloned().unwrap();
13629 assert_eq!(popover.signatures.len(), 3);
13630 // active_signature was 1, so that should be the current
13631 assert_eq!(popover.current_signature, 1);
13632 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13633 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13634 assert_eq!(
13635 popover.signatures[2].label,
13636 "fn overloaded(x: i32, y: i32, z: i32)"
13637 );
13638 });
13639
13640 // Test navigation functionality
13641 cx.update_editor(|editor, window, cx| {
13642 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13643 });
13644
13645 cx.editor(|editor, _, _| {
13646 let popover = editor.signature_help_state.popover().cloned().unwrap();
13647 assert_eq!(popover.current_signature, 2);
13648 });
13649
13650 // Test wrap around
13651 cx.update_editor(|editor, window, cx| {
13652 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13653 });
13654
13655 cx.editor(|editor, _, _| {
13656 let popover = editor.signature_help_state.popover().cloned().unwrap();
13657 assert_eq!(popover.current_signature, 0);
13658 });
13659
13660 // Test previous navigation
13661 cx.update_editor(|editor, window, cx| {
13662 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13663 });
13664
13665 cx.editor(|editor, _, _| {
13666 let popover = editor.signature_help_state.popover().cloned().unwrap();
13667 assert_eq!(popover.current_signature, 2);
13668 });
13669}
13670
13671#[gpui::test]
13672async fn test_completion_mode(cx: &mut TestAppContext) {
13673 init_test(cx, |_| {});
13674 let mut cx = EditorLspTestContext::new_rust(
13675 lsp::ServerCapabilities {
13676 completion_provider: Some(lsp::CompletionOptions {
13677 resolve_provider: Some(true),
13678 ..Default::default()
13679 }),
13680 ..Default::default()
13681 },
13682 cx,
13683 )
13684 .await;
13685
13686 struct Run {
13687 run_description: &'static str,
13688 initial_state: String,
13689 buffer_marked_text: String,
13690 completion_label: &'static str,
13691 completion_text: &'static str,
13692 expected_with_insert_mode: String,
13693 expected_with_replace_mode: String,
13694 expected_with_replace_subsequence_mode: String,
13695 expected_with_replace_suffix_mode: String,
13696 }
13697
13698 let runs = [
13699 Run {
13700 run_description: "Start of word matches completion text",
13701 initial_state: "before ediˇ after".into(),
13702 buffer_marked_text: "before <edi|> after".into(),
13703 completion_label: "editor",
13704 completion_text: "editor",
13705 expected_with_insert_mode: "before editorˇ after".into(),
13706 expected_with_replace_mode: "before editorˇ after".into(),
13707 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13708 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13709 },
13710 Run {
13711 run_description: "Accept same text at the middle of the word",
13712 initial_state: "before ediˇtor after".into(),
13713 buffer_marked_text: "before <edi|tor> after".into(),
13714 completion_label: "editor",
13715 completion_text: "editor",
13716 expected_with_insert_mode: "before editorˇtor after".into(),
13717 expected_with_replace_mode: "before editorˇ after".into(),
13718 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13719 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13720 },
13721 Run {
13722 run_description: "End of word matches completion text -- cursor at end",
13723 initial_state: "before torˇ after".into(),
13724 buffer_marked_text: "before <tor|> after".into(),
13725 completion_label: "editor",
13726 completion_text: "editor",
13727 expected_with_insert_mode: "before editorˇ after".into(),
13728 expected_with_replace_mode: "before editorˇ after".into(),
13729 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13730 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13731 },
13732 Run {
13733 run_description: "End of word matches completion text -- cursor at start",
13734 initial_state: "before ˇtor after".into(),
13735 buffer_marked_text: "before <|tor> after".into(),
13736 completion_label: "editor",
13737 completion_text: "editor",
13738 expected_with_insert_mode: "before editorˇtor after".into(),
13739 expected_with_replace_mode: "before editorˇ after".into(),
13740 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13741 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13742 },
13743 Run {
13744 run_description: "Prepend text containing whitespace",
13745 initial_state: "pˇfield: bool".into(),
13746 buffer_marked_text: "<p|field>: bool".into(),
13747 completion_label: "pub ",
13748 completion_text: "pub ",
13749 expected_with_insert_mode: "pub ˇfield: bool".into(),
13750 expected_with_replace_mode: "pub ˇ: bool".into(),
13751 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13752 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13753 },
13754 Run {
13755 run_description: "Add element to start of list",
13756 initial_state: "[element_ˇelement_2]".into(),
13757 buffer_marked_text: "[<element_|element_2>]".into(),
13758 completion_label: "element_1",
13759 completion_text: "element_1",
13760 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13761 expected_with_replace_mode: "[element_1ˇ]".into(),
13762 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13763 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13764 },
13765 Run {
13766 run_description: "Add element to start of list -- first and second elements are equal",
13767 initial_state: "[elˇelement]".into(),
13768 buffer_marked_text: "[<el|element>]".into(),
13769 completion_label: "element",
13770 completion_text: "element",
13771 expected_with_insert_mode: "[elementˇelement]".into(),
13772 expected_with_replace_mode: "[elementˇ]".into(),
13773 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13774 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13775 },
13776 Run {
13777 run_description: "Ends with matching suffix",
13778 initial_state: "SubˇError".into(),
13779 buffer_marked_text: "<Sub|Error>".into(),
13780 completion_label: "SubscriptionError",
13781 completion_text: "SubscriptionError",
13782 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13783 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13784 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13785 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13786 },
13787 Run {
13788 run_description: "Suffix is a subsequence -- contiguous",
13789 initial_state: "SubˇErr".into(),
13790 buffer_marked_text: "<Sub|Err>".into(),
13791 completion_label: "SubscriptionError",
13792 completion_text: "SubscriptionError",
13793 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13794 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13795 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13796 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13797 },
13798 Run {
13799 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13800 initial_state: "Suˇscrirr".into(),
13801 buffer_marked_text: "<Su|scrirr>".into(),
13802 completion_label: "SubscriptionError",
13803 completion_text: "SubscriptionError",
13804 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13805 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13806 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13807 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13808 },
13809 Run {
13810 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13811 initial_state: "foo(indˇix)".into(),
13812 buffer_marked_text: "foo(<ind|ix>)".into(),
13813 completion_label: "node_index",
13814 completion_text: "node_index",
13815 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13816 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13817 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13818 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13819 },
13820 Run {
13821 run_description: "Replace range ends before cursor - should extend to cursor",
13822 initial_state: "before editˇo after".into(),
13823 buffer_marked_text: "before <{ed}>it|o after".into(),
13824 completion_label: "editor",
13825 completion_text: "editor",
13826 expected_with_insert_mode: "before editorˇo after".into(),
13827 expected_with_replace_mode: "before editorˇo after".into(),
13828 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13829 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13830 },
13831 Run {
13832 run_description: "Uses label for suffix matching",
13833 initial_state: "before ediˇtor after".into(),
13834 buffer_marked_text: "before <edi|tor> after".into(),
13835 completion_label: "editor",
13836 completion_text: "editor()",
13837 expected_with_insert_mode: "before editor()ˇtor after".into(),
13838 expected_with_replace_mode: "before editor()ˇ after".into(),
13839 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13840 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13841 },
13842 Run {
13843 run_description: "Case insensitive subsequence and suffix matching",
13844 initial_state: "before EDiˇtoR after".into(),
13845 buffer_marked_text: "before <EDi|toR> after".into(),
13846 completion_label: "editor",
13847 completion_text: "editor",
13848 expected_with_insert_mode: "before editorˇtoR after".into(),
13849 expected_with_replace_mode: "before editorˇ after".into(),
13850 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13851 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13852 },
13853 ];
13854
13855 for run in runs {
13856 let run_variations = [
13857 (LspInsertMode::Insert, run.expected_with_insert_mode),
13858 (LspInsertMode::Replace, run.expected_with_replace_mode),
13859 (
13860 LspInsertMode::ReplaceSubsequence,
13861 run.expected_with_replace_subsequence_mode,
13862 ),
13863 (
13864 LspInsertMode::ReplaceSuffix,
13865 run.expected_with_replace_suffix_mode,
13866 ),
13867 ];
13868
13869 for (lsp_insert_mode, expected_text) in run_variations {
13870 eprintln!(
13871 "run = {:?}, mode = {lsp_insert_mode:.?}",
13872 run.run_description,
13873 );
13874
13875 update_test_language_settings(&mut cx, |settings| {
13876 settings.defaults.completions = Some(CompletionSettingsContent {
13877 lsp_insert_mode: Some(lsp_insert_mode),
13878 words: Some(WordsCompletionMode::Disabled),
13879 words_min_length: Some(0),
13880 ..Default::default()
13881 });
13882 });
13883
13884 cx.set_state(&run.initial_state);
13885 cx.update_editor(|editor, window, cx| {
13886 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13887 });
13888
13889 let counter = Arc::new(AtomicUsize::new(0));
13890 handle_completion_request_with_insert_and_replace(
13891 &mut cx,
13892 &run.buffer_marked_text,
13893 vec![(run.completion_label, run.completion_text)],
13894 counter.clone(),
13895 )
13896 .await;
13897 cx.condition(|editor, _| editor.context_menu_visible())
13898 .await;
13899 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13900
13901 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13902 editor
13903 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13904 .unwrap()
13905 });
13906 cx.assert_editor_state(&expected_text);
13907 handle_resolve_completion_request(&mut cx, None).await;
13908 apply_additional_edits.await.unwrap();
13909 }
13910 }
13911}
13912
13913#[gpui::test]
13914async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13915 init_test(cx, |_| {});
13916 let mut cx = EditorLspTestContext::new_rust(
13917 lsp::ServerCapabilities {
13918 completion_provider: Some(lsp::CompletionOptions {
13919 resolve_provider: Some(true),
13920 ..Default::default()
13921 }),
13922 ..Default::default()
13923 },
13924 cx,
13925 )
13926 .await;
13927
13928 let initial_state = "SubˇError";
13929 let buffer_marked_text = "<Sub|Error>";
13930 let completion_text = "SubscriptionError";
13931 let expected_with_insert_mode = "SubscriptionErrorˇError";
13932 let expected_with_replace_mode = "SubscriptionErrorˇ";
13933
13934 update_test_language_settings(&mut cx, |settings| {
13935 settings.defaults.completions = Some(CompletionSettingsContent {
13936 words: Some(WordsCompletionMode::Disabled),
13937 words_min_length: Some(0),
13938 // set the opposite here to ensure that the action is overriding the default behavior
13939 lsp_insert_mode: Some(LspInsertMode::Insert),
13940 ..Default::default()
13941 });
13942 });
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
13949 let counter = Arc::new(AtomicUsize::new(0));
13950 handle_completion_request_with_insert_and_replace(
13951 &mut cx,
13952 buffer_marked_text,
13953 vec![(completion_text, completion_text)],
13954 counter.clone(),
13955 )
13956 .await;
13957 cx.condition(|editor, _| editor.context_menu_visible())
13958 .await;
13959 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13960
13961 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13962 editor
13963 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13964 .unwrap()
13965 });
13966 cx.assert_editor_state(expected_with_replace_mode);
13967 handle_resolve_completion_request(&mut cx, None).await;
13968 apply_additional_edits.await.unwrap();
13969
13970 update_test_language_settings(&mut cx, |settings| {
13971 settings.defaults.completions = Some(CompletionSettingsContent {
13972 words: Some(WordsCompletionMode::Disabled),
13973 words_min_length: Some(0),
13974 // set the opposite here to ensure that the action is overriding the default behavior
13975 lsp_insert_mode: Some(LspInsertMode::Replace),
13976 ..Default::default()
13977 });
13978 });
13979
13980 cx.set_state(initial_state);
13981 cx.update_editor(|editor, window, cx| {
13982 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13983 });
13984 handle_completion_request_with_insert_and_replace(
13985 &mut cx,
13986 buffer_marked_text,
13987 vec![(completion_text, completion_text)],
13988 counter.clone(),
13989 )
13990 .await;
13991 cx.condition(|editor, _| editor.context_menu_visible())
13992 .await;
13993 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13994
13995 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13996 editor
13997 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13998 .unwrap()
13999 });
14000 cx.assert_editor_state(expected_with_insert_mode);
14001 handle_resolve_completion_request(&mut cx, None).await;
14002 apply_additional_edits.await.unwrap();
14003}
14004
14005#[gpui::test]
14006async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14007 init_test(cx, |_| {});
14008 let mut cx = EditorLspTestContext::new_rust(
14009 lsp::ServerCapabilities {
14010 completion_provider: Some(lsp::CompletionOptions {
14011 resolve_provider: Some(true),
14012 ..Default::default()
14013 }),
14014 ..Default::default()
14015 },
14016 cx,
14017 )
14018 .await;
14019
14020 // scenario: surrounding text matches completion text
14021 let completion_text = "to_offset";
14022 let initial_state = indoc! {"
14023 1. buf.to_offˇsuffix
14024 2. buf.to_offˇsuf
14025 3. buf.to_offˇfix
14026 4. buf.to_offˇ
14027 5. into_offˇensive
14028 6. ˇsuffix
14029 7. let ˇ //
14030 8. aaˇzz
14031 9. buf.to_off«zzzzzˇ»suffix
14032 10. buf.«ˇzzzzz»suffix
14033 11. to_off«ˇzzzzz»
14034
14035 buf.to_offˇsuffix // newest cursor
14036 "};
14037 let completion_marked_buffer = indoc! {"
14038 1. buf.to_offsuffix
14039 2. buf.to_offsuf
14040 3. buf.to_offfix
14041 4. buf.to_off
14042 5. into_offensive
14043 6. suffix
14044 7. let //
14045 8. aazz
14046 9. buf.to_offzzzzzsuffix
14047 10. buf.zzzzzsuffix
14048 11. to_offzzzzz
14049
14050 buf.<to_off|suffix> // newest cursor
14051 "};
14052 let expected = indoc! {"
14053 1. buf.to_offsetˇ
14054 2. buf.to_offsetˇsuf
14055 3. buf.to_offsetˇfix
14056 4. buf.to_offsetˇ
14057 5. into_offsetˇensive
14058 6. to_offsetˇsuffix
14059 7. let to_offsetˇ //
14060 8. aato_offsetˇzz
14061 9. buf.to_offsetˇ
14062 10. buf.to_offsetˇsuffix
14063 11. to_offsetˇ
14064
14065 buf.to_offsetˇ // newest cursor
14066 "};
14067 cx.set_state(initial_state);
14068 cx.update_editor(|editor, window, cx| {
14069 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14070 });
14071 handle_completion_request_with_insert_and_replace(
14072 &mut cx,
14073 completion_marked_buffer,
14074 vec![(completion_text, completion_text)],
14075 Arc::new(AtomicUsize::new(0)),
14076 )
14077 .await;
14078 cx.condition(|editor, _| editor.context_menu_visible())
14079 .await;
14080 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14081 editor
14082 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14083 .unwrap()
14084 });
14085 cx.assert_editor_state(expected);
14086 handle_resolve_completion_request(&mut cx, None).await;
14087 apply_additional_edits.await.unwrap();
14088
14089 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14090 let completion_text = "foo_and_bar";
14091 let initial_state = indoc! {"
14092 1. ooanbˇ
14093 2. zooanbˇ
14094 3. ooanbˇz
14095 4. zooanbˇz
14096 5. ooanˇ
14097 6. oanbˇ
14098
14099 ooanbˇ
14100 "};
14101 let completion_marked_buffer = indoc! {"
14102 1. ooanb
14103 2. zooanb
14104 3. ooanbz
14105 4. zooanbz
14106 5. ooan
14107 6. oanb
14108
14109 <ooanb|>
14110 "};
14111 let expected = indoc! {"
14112 1. foo_and_barˇ
14113 2. zfoo_and_barˇ
14114 3. foo_and_barˇz
14115 4. zfoo_and_barˇz
14116 5. ooanfoo_and_barˇ
14117 6. oanbfoo_and_barˇ
14118
14119 foo_and_barˇ
14120 "};
14121 cx.set_state(initial_state);
14122 cx.update_editor(|editor, window, cx| {
14123 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14124 });
14125 handle_completion_request_with_insert_and_replace(
14126 &mut cx,
14127 completion_marked_buffer,
14128 vec![(completion_text, completion_text)],
14129 Arc::new(AtomicUsize::new(0)),
14130 )
14131 .await;
14132 cx.condition(|editor, _| editor.context_menu_visible())
14133 .await;
14134 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14135 editor
14136 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14137 .unwrap()
14138 });
14139 cx.assert_editor_state(expected);
14140 handle_resolve_completion_request(&mut cx, None).await;
14141 apply_additional_edits.await.unwrap();
14142
14143 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14144 // (expects the same as if it was inserted at the end)
14145 let completion_text = "foo_and_bar";
14146 let initial_state = indoc! {"
14147 1. ooˇanb
14148 2. zooˇanb
14149 3. ooˇanbz
14150 4. zooˇanbz
14151
14152 ooˇanb
14153 "};
14154 let completion_marked_buffer = indoc! {"
14155 1. ooanb
14156 2. zooanb
14157 3. ooanbz
14158 4. zooanbz
14159
14160 <oo|anb>
14161 "};
14162 let expected = indoc! {"
14163 1. foo_and_barˇ
14164 2. zfoo_and_barˇ
14165 3. foo_and_barˇz
14166 4. zfoo_and_barˇz
14167
14168 foo_and_barˇ
14169 "};
14170 cx.set_state(initial_state);
14171 cx.update_editor(|editor, window, cx| {
14172 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14173 });
14174 handle_completion_request_with_insert_and_replace(
14175 &mut cx,
14176 completion_marked_buffer,
14177 vec![(completion_text, completion_text)],
14178 Arc::new(AtomicUsize::new(0)),
14179 )
14180 .await;
14181 cx.condition(|editor, _| editor.context_menu_visible())
14182 .await;
14183 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14184 editor
14185 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14186 .unwrap()
14187 });
14188 cx.assert_editor_state(expected);
14189 handle_resolve_completion_request(&mut cx, None).await;
14190 apply_additional_edits.await.unwrap();
14191}
14192
14193// This used to crash
14194#[gpui::test]
14195async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14196 init_test(cx, |_| {});
14197
14198 let buffer_text = indoc! {"
14199 fn main() {
14200 10.satu;
14201
14202 //
14203 // separate cursors so they open in different excerpts (manually reproducible)
14204 //
14205
14206 10.satu20;
14207 }
14208 "};
14209 let multibuffer_text_with_selections = indoc! {"
14210 fn main() {
14211 10.satuˇ;
14212
14213 //
14214
14215 //
14216
14217 10.satuˇ20;
14218 }
14219 "};
14220 let expected_multibuffer = indoc! {"
14221 fn main() {
14222 10.saturating_sub()ˇ;
14223
14224 //
14225
14226 //
14227
14228 10.saturating_sub()ˇ;
14229 }
14230 "};
14231
14232 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14233 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14234
14235 let fs = FakeFs::new(cx.executor());
14236 fs.insert_tree(
14237 path!("/a"),
14238 json!({
14239 "main.rs": buffer_text,
14240 }),
14241 )
14242 .await;
14243
14244 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14245 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14246 language_registry.add(rust_lang());
14247 let mut fake_servers = language_registry.register_fake_lsp(
14248 "Rust",
14249 FakeLspAdapter {
14250 capabilities: lsp::ServerCapabilities {
14251 completion_provider: Some(lsp::CompletionOptions {
14252 resolve_provider: None,
14253 ..lsp::CompletionOptions::default()
14254 }),
14255 ..lsp::ServerCapabilities::default()
14256 },
14257 ..FakeLspAdapter::default()
14258 },
14259 );
14260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14261 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14262 let buffer = project
14263 .update(cx, |project, cx| {
14264 project.open_local_buffer(path!("/a/main.rs"), cx)
14265 })
14266 .await
14267 .unwrap();
14268
14269 let multi_buffer = cx.new(|cx| {
14270 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14271 multi_buffer.push_excerpts(
14272 buffer.clone(),
14273 [ExcerptRange::new(0..first_excerpt_end)],
14274 cx,
14275 );
14276 multi_buffer.push_excerpts(
14277 buffer.clone(),
14278 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14279 cx,
14280 );
14281 multi_buffer
14282 });
14283
14284 let editor = workspace
14285 .update(cx, |_, window, cx| {
14286 cx.new(|cx| {
14287 Editor::new(
14288 EditorMode::Full {
14289 scale_ui_elements_with_buffer_font_size: false,
14290 show_active_line_background: false,
14291 sizing_behavior: SizingBehavior::Default,
14292 },
14293 multi_buffer.clone(),
14294 Some(project.clone()),
14295 window,
14296 cx,
14297 )
14298 })
14299 })
14300 .unwrap();
14301
14302 let pane = workspace
14303 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14304 .unwrap();
14305 pane.update_in(cx, |pane, window, cx| {
14306 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14307 });
14308
14309 let fake_server = fake_servers.next().await.unwrap();
14310
14311 editor.update_in(cx, |editor, window, cx| {
14312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14313 s.select_ranges([
14314 Point::new(1, 11)..Point::new(1, 11),
14315 Point::new(7, 11)..Point::new(7, 11),
14316 ])
14317 });
14318
14319 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14320 });
14321
14322 editor.update_in(cx, |editor, window, cx| {
14323 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14324 });
14325
14326 fake_server
14327 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14328 let completion_item = lsp::CompletionItem {
14329 label: "saturating_sub()".into(),
14330 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14331 lsp::InsertReplaceEdit {
14332 new_text: "saturating_sub()".to_owned(),
14333 insert: lsp::Range::new(
14334 lsp::Position::new(7, 7),
14335 lsp::Position::new(7, 11),
14336 ),
14337 replace: lsp::Range::new(
14338 lsp::Position::new(7, 7),
14339 lsp::Position::new(7, 13),
14340 ),
14341 },
14342 )),
14343 ..lsp::CompletionItem::default()
14344 };
14345
14346 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14347 })
14348 .next()
14349 .await
14350 .unwrap();
14351
14352 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14353 .await;
14354
14355 editor
14356 .update_in(cx, |editor, window, cx| {
14357 editor
14358 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14359 .unwrap()
14360 })
14361 .await
14362 .unwrap();
14363
14364 editor.update(cx, |editor, cx| {
14365 assert_text_with_selections(editor, expected_multibuffer, cx);
14366 })
14367}
14368
14369#[gpui::test]
14370async fn test_completion(cx: &mut TestAppContext) {
14371 init_test(cx, |_| {});
14372
14373 let mut cx = EditorLspTestContext::new_rust(
14374 lsp::ServerCapabilities {
14375 completion_provider: Some(lsp::CompletionOptions {
14376 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14377 resolve_provider: Some(true),
14378 ..Default::default()
14379 }),
14380 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14381 ..Default::default()
14382 },
14383 cx,
14384 )
14385 .await;
14386 let counter = Arc::new(AtomicUsize::new(0));
14387
14388 cx.set_state(indoc! {"
14389 oneˇ
14390 two
14391 three
14392 "});
14393 cx.simulate_keystroke(".");
14394 handle_completion_request(
14395 indoc! {"
14396 one.|<>
14397 two
14398 three
14399 "},
14400 vec!["first_completion", "second_completion"],
14401 true,
14402 counter.clone(),
14403 &mut cx,
14404 )
14405 .await;
14406 cx.condition(|editor, _| editor.context_menu_visible())
14407 .await;
14408 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14409
14410 let _handler = handle_signature_help_request(
14411 &mut cx,
14412 lsp::SignatureHelp {
14413 signatures: vec![lsp::SignatureInformation {
14414 label: "test signature".to_string(),
14415 documentation: None,
14416 parameters: Some(vec![lsp::ParameterInformation {
14417 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14418 documentation: None,
14419 }]),
14420 active_parameter: None,
14421 }],
14422 active_signature: None,
14423 active_parameter: None,
14424 },
14425 );
14426 cx.update_editor(|editor, window, cx| {
14427 assert!(
14428 !editor.signature_help_state.is_shown(),
14429 "No signature help was called for"
14430 );
14431 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14432 });
14433 cx.run_until_parked();
14434 cx.update_editor(|editor, _, _| {
14435 assert!(
14436 !editor.signature_help_state.is_shown(),
14437 "No signature help should be shown when completions menu is open"
14438 );
14439 });
14440
14441 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14442 editor.context_menu_next(&Default::default(), window, cx);
14443 editor
14444 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14445 .unwrap()
14446 });
14447 cx.assert_editor_state(indoc! {"
14448 one.second_completionˇ
14449 two
14450 three
14451 "});
14452
14453 handle_resolve_completion_request(
14454 &mut cx,
14455 Some(vec![
14456 (
14457 //This overlaps with the primary completion edit which is
14458 //misbehavior from the LSP spec, test that we filter it out
14459 indoc! {"
14460 one.second_ˇcompletion
14461 two
14462 threeˇ
14463 "},
14464 "overlapping additional edit",
14465 ),
14466 (
14467 indoc! {"
14468 one.second_completion
14469 two
14470 threeˇ
14471 "},
14472 "\nadditional edit",
14473 ),
14474 ]),
14475 )
14476 .await;
14477 apply_additional_edits.await.unwrap();
14478 cx.assert_editor_state(indoc! {"
14479 one.second_completionˇ
14480 two
14481 three
14482 additional edit
14483 "});
14484
14485 cx.set_state(indoc! {"
14486 one.second_completion
14487 twoˇ
14488 threeˇ
14489 additional edit
14490 "});
14491 cx.simulate_keystroke(" ");
14492 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14493 cx.simulate_keystroke("s");
14494 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14495
14496 cx.assert_editor_state(indoc! {"
14497 one.second_completion
14498 two sˇ
14499 three sˇ
14500 additional edit
14501 "});
14502 handle_completion_request(
14503 indoc! {"
14504 one.second_completion
14505 two s
14506 three <s|>
14507 additional edit
14508 "},
14509 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14510 true,
14511 counter.clone(),
14512 &mut cx,
14513 )
14514 .await;
14515 cx.condition(|editor, _| editor.context_menu_visible())
14516 .await;
14517 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14518
14519 cx.simulate_keystroke("i");
14520
14521 handle_completion_request(
14522 indoc! {"
14523 one.second_completion
14524 two si
14525 three <si|>
14526 additional edit
14527 "},
14528 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14529 true,
14530 counter.clone(),
14531 &mut cx,
14532 )
14533 .await;
14534 cx.condition(|editor, _| editor.context_menu_visible())
14535 .await;
14536 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14537
14538 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14539 editor
14540 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14541 .unwrap()
14542 });
14543 cx.assert_editor_state(indoc! {"
14544 one.second_completion
14545 two sixth_completionˇ
14546 three sixth_completionˇ
14547 additional edit
14548 "});
14549
14550 apply_additional_edits.await.unwrap();
14551
14552 update_test_language_settings(&mut cx, |settings| {
14553 settings.defaults.show_completions_on_input = Some(false);
14554 });
14555 cx.set_state("editorˇ");
14556 cx.simulate_keystroke(".");
14557 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14558 cx.simulate_keystrokes("c l o");
14559 cx.assert_editor_state("editor.cloˇ");
14560 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14561 cx.update_editor(|editor, window, cx| {
14562 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14563 });
14564 handle_completion_request(
14565 "editor.<clo|>",
14566 vec!["close", "clobber"],
14567 true,
14568 counter.clone(),
14569 &mut cx,
14570 )
14571 .await;
14572 cx.condition(|editor, _| editor.context_menu_visible())
14573 .await;
14574 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14575
14576 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14577 editor
14578 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14579 .unwrap()
14580 });
14581 cx.assert_editor_state("editor.clobberˇ");
14582 handle_resolve_completion_request(&mut cx, None).await;
14583 apply_additional_edits.await.unwrap();
14584}
14585
14586#[gpui::test]
14587async fn test_completion_reuse(cx: &mut TestAppContext) {
14588 init_test(cx, |_| {});
14589
14590 let mut cx = EditorLspTestContext::new_rust(
14591 lsp::ServerCapabilities {
14592 completion_provider: Some(lsp::CompletionOptions {
14593 trigger_characters: Some(vec![".".to_string()]),
14594 ..Default::default()
14595 }),
14596 ..Default::default()
14597 },
14598 cx,
14599 )
14600 .await;
14601
14602 let counter = Arc::new(AtomicUsize::new(0));
14603 cx.set_state("objˇ");
14604 cx.simulate_keystroke(".");
14605
14606 // Initial completion request returns complete results
14607 let is_incomplete = false;
14608 handle_completion_request(
14609 "obj.|<>",
14610 vec!["a", "ab", "abc"],
14611 is_incomplete,
14612 counter.clone(),
14613 &mut cx,
14614 )
14615 .await;
14616 cx.run_until_parked();
14617 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14618 cx.assert_editor_state("obj.ˇ");
14619 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14620
14621 // Type "a" - filters existing completions
14622 cx.simulate_keystroke("a");
14623 cx.run_until_parked();
14624 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14625 cx.assert_editor_state("obj.aˇ");
14626 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14627
14628 // Type "b" - filters existing completions
14629 cx.simulate_keystroke("b");
14630 cx.run_until_parked();
14631 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14632 cx.assert_editor_state("obj.abˇ");
14633 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14634
14635 // Type "c" - filters existing completions
14636 cx.simulate_keystroke("c");
14637 cx.run_until_parked();
14638 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14639 cx.assert_editor_state("obj.abcˇ");
14640 check_displayed_completions(vec!["abc"], &mut cx);
14641
14642 // Backspace to delete "c" - filters existing completions
14643 cx.update_editor(|editor, window, cx| {
14644 editor.backspace(&Backspace, window, cx);
14645 });
14646 cx.run_until_parked();
14647 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14648 cx.assert_editor_state("obj.abˇ");
14649 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14650
14651 // Moving cursor to the left dismisses menu.
14652 cx.update_editor(|editor, window, cx| {
14653 editor.move_left(&MoveLeft, window, cx);
14654 });
14655 cx.run_until_parked();
14656 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14657 cx.assert_editor_state("obj.aˇb");
14658 cx.update_editor(|editor, _, _| {
14659 assert_eq!(editor.context_menu_visible(), false);
14660 });
14661
14662 // Type "b" - new request
14663 cx.simulate_keystroke("b");
14664 let is_incomplete = false;
14665 handle_completion_request(
14666 "obj.<ab|>a",
14667 vec!["ab", "abc"],
14668 is_incomplete,
14669 counter.clone(),
14670 &mut cx,
14671 )
14672 .await;
14673 cx.run_until_parked();
14674 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14675 cx.assert_editor_state("obj.abˇb");
14676 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14677
14678 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14679 cx.update_editor(|editor, window, cx| {
14680 editor.backspace(&Backspace, window, cx);
14681 });
14682 let is_incomplete = false;
14683 handle_completion_request(
14684 "obj.<a|>b",
14685 vec!["a", "ab", "abc"],
14686 is_incomplete,
14687 counter.clone(),
14688 &mut cx,
14689 )
14690 .await;
14691 cx.run_until_parked();
14692 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14693 cx.assert_editor_state("obj.aˇb");
14694 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14695
14696 // Backspace to delete "a" - dismisses menu.
14697 cx.update_editor(|editor, window, cx| {
14698 editor.backspace(&Backspace, window, cx);
14699 });
14700 cx.run_until_parked();
14701 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14702 cx.assert_editor_state("obj.ˇb");
14703 cx.update_editor(|editor, _, _| {
14704 assert_eq!(editor.context_menu_visible(), false);
14705 });
14706}
14707
14708#[gpui::test]
14709async fn test_word_completion(cx: &mut TestAppContext) {
14710 let lsp_fetch_timeout_ms = 10;
14711 init_test(cx, |language_settings| {
14712 language_settings.defaults.completions = Some(CompletionSettingsContent {
14713 words_min_length: Some(0),
14714 lsp_fetch_timeout_ms: Some(10),
14715 lsp_insert_mode: Some(LspInsertMode::Insert),
14716 ..Default::default()
14717 });
14718 });
14719
14720 let mut cx = EditorLspTestContext::new_rust(
14721 lsp::ServerCapabilities {
14722 completion_provider: Some(lsp::CompletionOptions {
14723 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14724 ..lsp::CompletionOptions::default()
14725 }),
14726 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14727 ..lsp::ServerCapabilities::default()
14728 },
14729 cx,
14730 )
14731 .await;
14732
14733 let throttle_completions = Arc::new(AtomicBool::new(false));
14734
14735 let lsp_throttle_completions = throttle_completions.clone();
14736 let _completion_requests_handler =
14737 cx.lsp
14738 .server
14739 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14740 let lsp_throttle_completions = lsp_throttle_completions.clone();
14741 let cx = cx.clone();
14742 async move {
14743 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14744 cx.background_executor()
14745 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14746 .await;
14747 }
14748 Ok(Some(lsp::CompletionResponse::Array(vec![
14749 lsp::CompletionItem {
14750 label: "first".into(),
14751 ..lsp::CompletionItem::default()
14752 },
14753 lsp::CompletionItem {
14754 label: "last".into(),
14755 ..lsp::CompletionItem::default()
14756 },
14757 ])))
14758 }
14759 });
14760
14761 cx.set_state(indoc! {"
14762 oneˇ
14763 two
14764 three
14765 "});
14766 cx.simulate_keystroke(".");
14767 cx.executor().run_until_parked();
14768 cx.condition(|editor, _| editor.context_menu_visible())
14769 .await;
14770 cx.update_editor(|editor, window, cx| {
14771 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14772 {
14773 assert_eq!(
14774 completion_menu_entries(menu),
14775 &["first", "last"],
14776 "When LSP server is fast to reply, no fallback word completions are used"
14777 );
14778 } else {
14779 panic!("expected completion menu to be open");
14780 }
14781 editor.cancel(&Cancel, window, cx);
14782 });
14783 cx.executor().run_until_parked();
14784 cx.condition(|editor, _| !editor.context_menu_visible())
14785 .await;
14786
14787 throttle_completions.store(true, atomic::Ordering::Release);
14788 cx.simulate_keystroke(".");
14789 cx.executor()
14790 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14791 cx.executor().run_until_parked();
14792 cx.condition(|editor, _| editor.context_menu_visible())
14793 .await;
14794 cx.update_editor(|editor, _, _| {
14795 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14796 {
14797 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14798 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14799 } else {
14800 panic!("expected completion menu to be open");
14801 }
14802 });
14803}
14804
14805#[gpui::test]
14806async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14807 init_test(cx, |language_settings| {
14808 language_settings.defaults.completions = Some(CompletionSettingsContent {
14809 words: Some(WordsCompletionMode::Enabled),
14810 words_min_length: Some(0),
14811 lsp_insert_mode: Some(LspInsertMode::Insert),
14812 ..Default::default()
14813 });
14814 });
14815
14816 let mut cx = EditorLspTestContext::new_rust(
14817 lsp::ServerCapabilities {
14818 completion_provider: Some(lsp::CompletionOptions {
14819 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14820 ..lsp::CompletionOptions::default()
14821 }),
14822 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14823 ..lsp::ServerCapabilities::default()
14824 },
14825 cx,
14826 )
14827 .await;
14828
14829 let _completion_requests_handler =
14830 cx.lsp
14831 .server
14832 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14833 Ok(Some(lsp::CompletionResponse::Array(vec![
14834 lsp::CompletionItem {
14835 label: "first".into(),
14836 ..lsp::CompletionItem::default()
14837 },
14838 lsp::CompletionItem {
14839 label: "last".into(),
14840 ..lsp::CompletionItem::default()
14841 },
14842 ])))
14843 });
14844
14845 cx.set_state(indoc! {"ˇ
14846 first
14847 last
14848 second
14849 "});
14850 cx.simulate_keystroke(".");
14851 cx.executor().run_until_parked();
14852 cx.condition(|editor, _| editor.context_menu_visible())
14853 .await;
14854 cx.update_editor(|editor, _, _| {
14855 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14856 {
14857 assert_eq!(
14858 completion_menu_entries(menu),
14859 &["first", "last", "second"],
14860 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14861 );
14862 } else {
14863 panic!("expected completion menu to be open");
14864 }
14865 });
14866}
14867
14868#[gpui::test]
14869async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14870 init_test(cx, |language_settings| {
14871 language_settings.defaults.completions = Some(CompletionSettingsContent {
14872 words: Some(WordsCompletionMode::Disabled),
14873 words_min_length: Some(0),
14874 lsp_insert_mode: Some(LspInsertMode::Insert),
14875 ..Default::default()
14876 });
14877 });
14878
14879 let mut cx = EditorLspTestContext::new_rust(
14880 lsp::ServerCapabilities {
14881 completion_provider: Some(lsp::CompletionOptions {
14882 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14883 ..lsp::CompletionOptions::default()
14884 }),
14885 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14886 ..lsp::ServerCapabilities::default()
14887 },
14888 cx,
14889 )
14890 .await;
14891
14892 let _completion_requests_handler =
14893 cx.lsp
14894 .server
14895 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14896 panic!("LSP completions should not be queried when dealing with word completions")
14897 });
14898
14899 cx.set_state(indoc! {"ˇ
14900 first
14901 last
14902 second
14903 "});
14904 cx.update_editor(|editor, window, cx| {
14905 editor.show_word_completions(&ShowWordCompletions, window, cx);
14906 });
14907 cx.executor().run_until_parked();
14908 cx.condition(|editor, _| editor.context_menu_visible())
14909 .await;
14910 cx.update_editor(|editor, _, _| {
14911 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14912 {
14913 assert_eq!(
14914 completion_menu_entries(menu),
14915 &["first", "last", "second"],
14916 "`ShowWordCompletions` action should show word completions"
14917 );
14918 } else {
14919 panic!("expected completion menu to be open");
14920 }
14921 });
14922
14923 cx.simulate_keystroke("l");
14924 cx.executor().run_until_parked();
14925 cx.condition(|editor, _| editor.context_menu_visible())
14926 .await;
14927 cx.update_editor(|editor, _, _| {
14928 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14929 {
14930 assert_eq!(
14931 completion_menu_entries(menu),
14932 &["last"],
14933 "After showing word completions, further editing should filter them and not query the LSP"
14934 );
14935 } else {
14936 panic!("expected completion menu to be open");
14937 }
14938 });
14939}
14940
14941#[gpui::test]
14942async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14943 init_test(cx, |language_settings| {
14944 language_settings.defaults.completions = Some(CompletionSettingsContent {
14945 words_min_length: Some(0),
14946 lsp: Some(false),
14947 lsp_insert_mode: Some(LspInsertMode::Insert),
14948 ..Default::default()
14949 });
14950 });
14951
14952 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14953
14954 cx.set_state(indoc! {"ˇ
14955 0_usize
14956 let
14957 33
14958 4.5f32
14959 "});
14960 cx.update_editor(|editor, window, cx| {
14961 editor.show_completions(&ShowCompletions::default(), window, cx);
14962 });
14963 cx.executor().run_until_parked();
14964 cx.condition(|editor, _| editor.context_menu_visible())
14965 .await;
14966 cx.update_editor(|editor, window, cx| {
14967 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14968 {
14969 assert_eq!(
14970 completion_menu_entries(menu),
14971 &["let"],
14972 "With no digits in the completion query, no digits should be in the word completions"
14973 );
14974 } else {
14975 panic!("expected completion menu to be open");
14976 }
14977 editor.cancel(&Cancel, window, cx);
14978 });
14979
14980 cx.set_state(indoc! {"3ˇ
14981 0_usize
14982 let
14983 3
14984 33.35f32
14985 "});
14986 cx.update_editor(|editor, window, cx| {
14987 editor.show_completions(&ShowCompletions::default(), window, cx);
14988 });
14989 cx.executor().run_until_parked();
14990 cx.condition(|editor, _| editor.context_menu_visible())
14991 .await;
14992 cx.update_editor(|editor, _, _| {
14993 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14994 {
14995 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14996 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14997 } else {
14998 panic!("expected completion menu to be open");
14999 }
15000 });
15001}
15002
15003#[gpui::test]
15004async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15005 init_test(cx, |language_settings| {
15006 language_settings.defaults.completions = Some(CompletionSettingsContent {
15007 words: Some(WordsCompletionMode::Enabled),
15008 words_min_length: Some(3),
15009 lsp_insert_mode: Some(LspInsertMode::Insert),
15010 ..Default::default()
15011 });
15012 });
15013
15014 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15015 cx.set_state(indoc! {"ˇ
15016 wow
15017 wowen
15018 wowser
15019 "});
15020 cx.simulate_keystroke("w");
15021 cx.executor().run_until_parked();
15022 cx.update_editor(|editor, _, _| {
15023 if editor.context_menu.borrow_mut().is_some() {
15024 panic!(
15025 "expected completion menu to be hidden, as words completion threshold is not met"
15026 );
15027 }
15028 });
15029
15030 cx.update_editor(|editor, window, cx| {
15031 editor.show_word_completions(&ShowWordCompletions, window, cx);
15032 });
15033 cx.executor().run_until_parked();
15034 cx.update_editor(|editor, window, cx| {
15035 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15036 {
15037 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");
15038 } else {
15039 panic!("expected completion menu to be open after the word completions are called with an action");
15040 }
15041
15042 editor.cancel(&Cancel, window, cx);
15043 });
15044 cx.update_editor(|editor, _, _| {
15045 if editor.context_menu.borrow_mut().is_some() {
15046 panic!("expected completion menu to be hidden after canceling");
15047 }
15048 });
15049
15050 cx.simulate_keystroke("o");
15051 cx.executor().run_until_parked();
15052 cx.update_editor(|editor, _, _| {
15053 if editor.context_menu.borrow_mut().is_some() {
15054 panic!(
15055 "expected completion menu to be hidden, as words completion threshold is not met still"
15056 );
15057 }
15058 });
15059
15060 cx.simulate_keystroke("w");
15061 cx.executor().run_until_parked();
15062 cx.update_editor(|editor, _, _| {
15063 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15064 {
15065 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15066 } else {
15067 panic!("expected completion menu to be open after the word completions threshold is met");
15068 }
15069 });
15070}
15071
15072#[gpui::test]
15073async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15074 init_test(cx, |language_settings| {
15075 language_settings.defaults.completions = Some(CompletionSettingsContent {
15076 words: Some(WordsCompletionMode::Enabled),
15077 words_min_length: Some(0),
15078 lsp_insert_mode: Some(LspInsertMode::Insert),
15079 ..Default::default()
15080 });
15081 });
15082
15083 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15084 cx.update_editor(|editor, _, _| {
15085 editor.disable_word_completions();
15086 });
15087 cx.set_state(indoc! {"ˇ
15088 wow
15089 wowen
15090 wowser
15091 "});
15092 cx.simulate_keystroke("w");
15093 cx.executor().run_until_parked();
15094 cx.update_editor(|editor, _, _| {
15095 if editor.context_menu.borrow_mut().is_some() {
15096 panic!(
15097 "expected completion menu to be hidden, as words completion are disabled for this editor"
15098 );
15099 }
15100 });
15101
15102 cx.update_editor(|editor, window, cx| {
15103 editor.show_word_completions(&ShowWordCompletions, window, cx);
15104 });
15105 cx.executor().run_until_parked();
15106 cx.update_editor(|editor, _, _| {
15107 if editor.context_menu.borrow_mut().is_some() {
15108 panic!(
15109 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15110 );
15111 }
15112 });
15113}
15114
15115fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15116 let position = || lsp::Position {
15117 line: params.text_document_position.position.line,
15118 character: params.text_document_position.position.character,
15119 };
15120 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15121 range: lsp::Range {
15122 start: position(),
15123 end: position(),
15124 },
15125 new_text: text.to_string(),
15126 }))
15127}
15128
15129#[gpui::test]
15130async fn test_multiline_completion(cx: &mut TestAppContext) {
15131 init_test(cx, |_| {});
15132
15133 let fs = FakeFs::new(cx.executor());
15134 fs.insert_tree(
15135 path!("/a"),
15136 json!({
15137 "main.ts": "a",
15138 }),
15139 )
15140 .await;
15141
15142 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15143 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15144 let typescript_language = Arc::new(Language::new(
15145 LanguageConfig {
15146 name: "TypeScript".into(),
15147 matcher: LanguageMatcher {
15148 path_suffixes: vec!["ts".to_string()],
15149 ..LanguageMatcher::default()
15150 },
15151 line_comments: vec!["// ".into()],
15152 ..LanguageConfig::default()
15153 },
15154 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15155 ));
15156 language_registry.add(typescript_language.clone());
15157 let mut fake_servers = language_registry.register_fake_lsp(
15158 "TypeScript",
15159 FakeLspAdapter {
15160 capabilities: lsp::ServerCapabilities {
15161 completion_provider: Some(lsp::CompletionOptions {
15162 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15163 ..lsp::CompletionOptions::default()
15164 }),
15165 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15166 ..lsp::ServerCapabilities::default()
15167 },
15168 // Emulate vtsls label generation
15169 label_for_completion: Some(Box::new(|item, _| {
15170 let text = if let Some(description) = item
15171 .label_details
15172 .as_ref()
15173 .and_then(|label_details| label_details.description.as_ref())
15174 {
15175 format!("{} {}", item.label, description)
15176 } else if let Some(detail) = &item.detail {
15177 format!("{} {}", item.label, detail)
15178 } else {
15179 item.label.clone()
15180 };
15181 Some(language::CodeLabel::plain(text, None))
15182 })),
15183 ..FakeLspAdapter::default()
15184 },
15185 );
15186 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15187 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15188 let worktree_id = workspace
15189 .update(cx, |workspace, _window, cx| {
15190 workspace.project().update(cx, |project, cx| {
15191 project.worktrees(cx).next().unwrap().read(cx).id()
15192 })
15193 })
15194 .unwrap();
15195 let _buffer = project
15196 .update(cx, |project, cx| {
15197 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15198 })
15199 .await
15200 .unwrap();
15201 let editor = workspace
15202 .update(cx, |workspace, window, cx| {
15203 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15204 })
15205 .unwrap()
15206 .await
15207 .unwrap()
15208 .downcast::<Editor>()
15209 .unwrap();
15210 let fake_server = fake_servers.next().await.unwrap();
15211
15212 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15213 let multiline_label_2 = "a\nb\nc\n";
15214 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15215 let multiline_description = "d\ne\nf\n";
15216 let multiline_detail_2 = "g\nh\ni\n";
15217
15218 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15219 move |params, _| async move {
15220 Ok(Some(lsp::CompletionResponse::Array(vec![
15221 lsp::CompletionItem {
15222 label: multiline_label.to_string(),
15223 text_edit: gen_text_edit(¶ms, "new_text_1"),
15224 ..lsp::CompletionItem::default()
15225 },
15226 lsp::CompletionItem {
15227 label: "single line label 1".to_string(),
15228 detail: Some(multiline_detail.to_string()),
15229 text_edit: gen_text_edit(¶ms, "new_text_2"),
15230 ..lsp::CompletionItem::default()
15231 },
15232 lsp::CompletionItem {
15233 label: "single line label 2".to_string(),
15234 label_details: Some(lsp::CompletionItemLabelDetails {
15235 description: Some(multiline_description.to_string()),
15236 detail: None,
15237 }),
15238 text_edit: gen_text_edit(¶ms, "new_text_2"),
15239 ..lsp::CompletionItem::default()
15240 },
15241 lsp::CompletionItem {
15242 label: multiline_label_2.to_string(),
15243 detail: Some(multiline_detail_2.to_string()),
15244 text_edit: gen_text_edit(¶ms, "new_text_3"),
15245 ..lsp::CompletionItem::default()
15246 },
15247 lsp::CompletionItem {
15248 label: "Label with many spaces and \t but without newlines".to_string(),
15249 detail: Some(
15250 "Details with many spaces and \t but without newlines".to_string(),
15251 ),
15252 text_edit: gen_text_edit(¶ms, "new_text_4"),
15253 ..lsp::CompletionItem::default()
15254 },
15255 ])))
15256 },
15257 );
15258
15259 editor.update_in(cx, |editor, window, cx| {
15260 cx.focus_self(window);
15261 editor.move_to_end(&MoveToEnd, window, cx);
15262 editor.handle_input(".", window, cx);
15263 });
15264 cx.run_until_parked();
15265 completion_handle.next().await.unwrap();
15266
15267 editor.update(cx, |editor, _| {
15268 assert!(editor.context_menu_visible());
15269 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15270 {
15271 let completion_labels = menu
15272 .completions
15273 .borrow()
15274 .iter()
15275 .map(|c| c.label.text.clone())
15276 .collect::<Vec<_>>();
15277 assert_eq!(
15278 completion_labels,
15279 &[
15280 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15281 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15282 "single line label 2 d e f ",
15283 "a b c g h i ",
15284 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15285 ],
15286 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15287 );
15288
15289 for completion in menu
15290 .completions
15291 .borrow()
15292 .iter() {
15293 assert_eq!(
15294 completion.label.filter_range,
15295 0..completion.label.text.len(),
15296 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15297 );
15298 }
15299 } else {
15300 panic!("expected completion menu to be open");
15301 }
15302 });
15303}
15304
15305#[gpui::test]
15306async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15307 init_test(cx, |_| {});
15308 let mut cx = EditorLspTestContext::new_rust(
15309 lsp::ServerCapabilities {
15310 completion_provider: Some(lsp::CompletionOptions {
15311 trigger_characters: Some(vec![".".to_string()]),
15312 ..Default::default()
15313 }),
15314 ..Default::default()
15315 },
15316 cx,
15317 )
15318 .await;
15319 cx.lsp
15320 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15321 Ok(Some(lsp::CompletionResponse::Array(vec![
15322 lsp::CompletionItem {
15323 label: "first".into(),
15324 ..Default::default()
15325 },
15326 lsp::CompletionItem {
15327 label: "last".into(),
15328 ..Default::default()
15329 },
15330 ])))
15331 });
15332 cx.set_state("variableˇ");
15333 cx.simulate_keystroke(".");
15334 cx.executor().run_until_parked();
15335
15336 cx.update_editor(|editor, _, _| {
15337 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15338 {
15339 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15340 } else {
15341 panic!("expected completion menu to be open");
15342 }
15343 });
15344
15345 cx.update_editor(|editor, window, cx| {
15346 editor.move_page_down(&MovePageDown::default(), window, cx);
15347 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15348 {
15349 assert!(
15350 menu.selected_item == 1,
15351 "expected PageDown to select the last item from the context menu"
15352 );
15353 } else {
15354 panic!("expected completion menu to stay open after PageDown");
15355 }
15356 });
15357
15358 cx.update_editor(|editor, window, cx| {
15359 editor.move_page_up(&MovePageUp::default(), window, cx);
15360 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15361 {
15362 assert!(
15363 menu.selected_item == 0,
15364 "expected PageUp to select the first item from the context menu"
15365 );
15366 } else {
15367 panic!("expected completion menu to stay open after PageUp");
15368 }
15369 });
15370}
15371
15372#[gpui::test]
15373async fn test_as_is_completions(cx: &mut TestAppContext) {
15374 init_test(cx, |_| {});
15375 let mut cx = EditorLspTestContext::new_rust(
15376 lsp::ServerCapabilities {
15377 completion_provider: Some(lsp::CompletionOptions {
15378 ..Default::default()
15379 }),
15380 ..Default::default()
15381 },
15382 cx,
15383 )
15384 .await;
15385 cx.lsp
15386 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15387 Ok(Some(lsp::CompletionResponse::Array(vec![
15388 lsp::CompletionItem {
15389 label: "unsafe".into(),
15390 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15391 range: lsp::Range {
15392 start: lsp::Position {
15393 line: 1,
15394 character: 2,
15395 },
15396 end: lsp::Position {
15397 line: 1,
15398 character: 3,
15399 },
15400 },
15401 new_text: "unsafe".to_string(),
15402 })),
15403 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15404 ..Default::default()
15405 },
15406 ])))
15407 });
15408 cx.set_state("fn a() {}\n nˇ");
15409 cx.executor().run_until_parked();
15410 cx.update_editor(|editor, window, cx| {
15411 editor.show_completions(
15412 &ShowCompletions {
15413 trigger: Some("\n".into()),
15414 },
15415 window,
15416 cx,
15417 );
15418 });
15419 cx.executor().run_until_parked();
15420
15421 cx.update_editor(|editor, window, cx| {
15422 editor.confirm_completion(&Default::default(), window, cx)
15423 });
15424 cx.executor().run_until_parked();
15425 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15426}
15427
15428#[gpui::test]
15429async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15430 init_test(cx, |_| {});
15431 let language =
15432 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15433 let mut cx = EditorLspTestContext::new(
15434 language,
15435 lsp::ServerCapabilities {
15436 completion_provider: Some(lsp::CompletionOptions {
15437 ..lsp::CompletionOptions::default()
15438 }),
15439 ..lsp::ServerCapabilities::default()
15440 },
15441 cx,
15442 )
15443 .await;
15444
15445 cx.set_state(
15446 "#ifndef BAR_H
15447#define BAR_H
15448
15449#include <stdbool.h>
15450
15451int fn_branch(bool do_branch1, bool do_branch2);
15452
15453#endif // BAR_H
15454ˇ",
15455 );
15456 cx.executor().run_until_parked();
15457 cx.update_editor(|editor, window, cx| {
15458 editor.handle_input("#", window, cx);
15459 });
15460 cx.executor().run_until_parked();
15461 cx.update_editor(|editor, window, cx| {
15462 editor.handle_input("i", window, cx);
15463 });
15464 cx.executor().run_until_parked();
15465 cx.update_editor(|editor, window, cx| {
15466 editor.handle_input("n", window, cx);
15467 });
15468 cx.executor().run_until_parked();
15469 cx.assert_editor_state(
15470 "#ifndef BAR_H
15471#define BAR_H
15472
15473#include <stdbool.h>
15474
15475int fn_branch(bool do_branch1, bool do_branch2);
15476
15477#endif // BAR_H
15478#inˇ",
15479 );
15480
15481 cx.lsp
15482 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15483 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15484 is_incomplete: false,
15485 item_defaults: None,
15486 items: vec![lsp::CompletionItem {
15487 kind: Some(lsp::CompletionItemKind::SNIPPET),
15488 label_details: Some(lsp::CompletionItemLabelDetails {
15489 detail: Some("header".to_string()),
15490 description: None,
15491 }),
15492 label: " include".to_string(),
15493 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15494 range: lsp::Range {
15495 start: lsp::Position {
15496 line: 8,
15497 character: 1,
15498 },
15499 end: lsp::Position {
15500 line: 8,
15501 character: 1,
15502 },
15503 },
15504 new_text: "include \"$0\"".to_string(),
15505 })),
15506 sort_text: Some("40b67681include".to_string()),
15507 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15508 filter_text: Some("include".to_string()),
15509 insert_text: Some("include \"$0\"".to_string()),
15510 ..lsp::CompletionItem::default()
15511 }],
15512 })))
15513 });
15514 cx.update_editor(|editor, window, cx| {
15515 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15516 });
15517 cx.executor().run_until_parked();
15518 cx.update_editor(|editor, window, cx| {
15519 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15520 });
15521 cx.executor().run_until_parked();
15522 cx.assert_editor_state(
15523 "#ifndef BAR_H
15524#define BAR_H
15525
15526#include <stdbool.h>
15527
15528int fn_branch(bool do_branch1, bool do_branch2);
15529
15530#endif // BAR_H
15531#include \"ˇ\"",
15532 );
15533
15534 cx.lsp
15535 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15536 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15537 is_incomplete: true,
15538 item_defaults: None,
15539 items: vec![lsp::CompletionItem {
15540 kind: Some(lsp::CompletionItemKind::FILE),
15541 label: "AGL/".to_string(),
15542 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15543 range: lsp::Range {
15544 start: lsp::Position {
15545 line: 8,
15546 character: 10,
15547 },
15548 end: lsp::Position {
15549 line: 8,
15550 character: 11,
15551 },
15552 },
15553 new_text: "AGL/".to_string(),
15554 })),
15555 sort_text: Some("40b67681AGL/".to_string()),
15556 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15557 filter_text: Some("AGL/".to_string()),
15558 insert_text: Some("AGL/".to_string()),
15559 ..lsp::CompletionItem::default()
15560 }],
15561 })))
15562 });
15563 cx.update_editor(|editor, window, cx| {
15564 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15565 });
15566 cx.executor().run_until_parked();
15567 cx.update_editor(|editor, window, cx| {
15568 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15569 });
15570 cx.executor().run_until_parked();
15571 cx.assert_editor_state(
15572 r##"#ifndef BAR_H
15573#define BAR_H
15574
15575#include <stdbool.h>
15576
15577int fn_branch(bool do_branch1, bool do_branch2);
15578
15579#endif // BAR_H
15580#include "AGL/ˇ"##,
15581 );
15582
15583 cx.update_editor(|editor, window, cx| {
15584 editor.handle_input("\"", window, cx);
15585 });
15586 cx.executor().run_until_parked();
15587 cx.assert_editor_state(
15588 r##"#ifndef BAR_H
15589#define BAR_H
15590
15591#include <stdbool.h>
15592
15593int fn_branch(bool do_branch1, bool do_branch2);
15594
15595#endif // BAR_H
15596#include "AGL/"ˇ"##,
15597 );
15598}
15599
15600#[gpui::test]
15601async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15602 init_test(cx, |_| {});
15603
15604 let mut cx = EditorLspTestContext::new_rust(
15605 lsp::ServerCapabilities {
15606 completion_provider: Some(lsp::CompletionOptions {
15607 trigger_characters: Some(vec![".".to_string()]),
15608 resolve_provider: Some(true),
15609 ..Default::default()
15610 }),
15611 ..Default::default()
15612 },
15613 cx,
15614 )
15615 .await;
15616
15617 cx.set_state("fn main() { let a = 2ˇ; }");
15618 cx.simulate_keystroke(".");
15619 let completion_item = lsp::CompletionItem {
15620 label: "Some".into(),
15621 kind: Some(lsp::CompletionItemKind::SNIPPET),
15622 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15623 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15624 kind: lsp::MarkupKind::Markdown,
15625 value: "```rust\nSome(2)\n```".to_string(),
15626 })),
15627 deprecated: Some(false),
15628 sort_text: Some("Some".to_string()),
15629 filter_text: Some("Some".to_string()),
15630 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15631 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15632 range: lsp::Range {
15633 start: lsp::Position {
15634 line: 0,
15635 character: 22,
15636 },
15637 end: lsp::Position {
15638 line: 0,
15639 character: 22,
15640 },
15641 },
15642 new_text: "Some(2)".to_string(),
15643 })),
15644 additional_text_edits: Some(vec![lsp::TextEdit {
15645 range: lsp::Range {
15646 start: lsp::Position {
15647 line: 0,
15648 character: 20,
15649 },
15650 end: lsp::Position {
15651 line: 0,
15652 character: 22,
15653 },
15654 },
15655 new_text: "".to_string(),
15656 }]),
15657 ..Default::default()
15658 };
15659
15660 let closure_completion_item = completion_item.clone();
15661 let counter = Arc::new(AtomicUsize::new(0));
15662 let counter_clone = counter.clone();
15663 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15664 let task_completion_item = closure_completion_item.clone();
15665 counter_clone.fetch_add(1, atomic::Ordering::Release);
15666 async move {
15667 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15668 is_incomplete: true,
15669 item_defaults: None,
15670 items: vec![task_completion_item],
15671 })))
15672 }
15673 });
15674
15675 cx.condition(|editor, _| editor.context_menu_visible())
15676 .await;
15677 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15678 assert!(request.next().await.is_some());
15679 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15680
15681 cx.simulate_keystrokes("S o m");
15682 cx.condition(|editor, _| editor.context_menu_visible())
15683 .await;
15684 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15685 assert!(request.next().await.is_some());
15686 assert!(request.next().await.is_some());
15687 assert!(request.next().await.is_some());
15688 request.close();
15689 assert!(request.next().await.is_none());
15690 assert_eq!(
15691 counter.load(atomic::Ordering::Acquire),
15692 4,
15693 "With the completions menu open, only one LSP request should happen per input"
15694 );
15695}
15696
15697#[gpui::test]
15698async fn test_toggle_comment(cx: &mut TestAppContext) {
15699 init_test(cx, |_| {});
15700 let mut cx = EditorTestContext::new(cx).await;
15701 let language = Arc::new(Language::new(
15702 LanguageConfig {
15703 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15704 ..Default::default()
15705 },
15706 Some(tree_sitter_rust::LANGUAGE.into()),
15707 ));
15708 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15709
15710 // If multiple selections intersect a line, the line is only toggled once.
15711 cx.set_state(indoc! {"
15712 fn a() {
15713 «//b();
15714 ˇ»// «c();
15715 //ˇ» d();
15716 }
15717 "});
15718
15719 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15720
15721 cx.assert_editor_state(indoc! {"
15722 fn a() {
15723 «b();
15724 c();
15725 ˇ» d();
15726 }
15727 "});
15728
15729 // The comment prefix is inserted at the same column for every line in a
15730 // selection.
15731 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15732
15733 cx.assert_editor_state(indoc! {"
15734 fn a() {
15735 // «b();
15736 // c();
15737 ˇ»// d();
15738 }
15739 "});
15740
15741 // If a selection ends at the beginning of a line, that line is not toggled.
15742 cx.set_selections_state(indoc! {"
15743 fn a() {
15744 // b();
15745 «// c();
15746 ˇ» // d();
15747 }
15748 "});
15749
15750 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15751
15752 cx.assert_editor_state(indoc! {"
15753 fn a() {
15754 // b();
15755 «c();
15756 ˇ» // d();
15757 }
15758 "});
15759
15760 // If a selection span a single line and is empty, the line is toggled.
15761 cx.set_state(indoc! {"
15762 fn a() {
15763 a();
15764 b();
15765 ˇ
15766 }
15767 "});
15768
15769 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15770
15771 cx.assert_editor_state(indoc! {"
15772 fn a() {
15773 a();
15774 b();
15775 //•ˇ
15776 }
15777 "});
15778
15779 // If a selection span multiple lines, empty lines are not toggled.
15780 cx.set_state(indoc! {"
15781 fn a() {
15782 «a();
15783
15784 c();ˇ»
15785 }
15786 "});
15787
15788 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15789
15790 cx.assert_editor_state(indoc! {"
15791 fn a() {
15792 // «a();
15793
15794 // c();ˇ»
15795 }
15796 "});
15797
15798 // If a selection includes multiple comment prefixes, all lines are uncommented.
15799 cx.set_state(indoc! {"
15800 fn a() {
15801 «// a();
15802 /// b();
15803 //! c();ˇ»
15804 }
15805 "});
15806
15807 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15808
15809 cx.assert_editor_state(indoc! {"
15810 fn a() {
15811 «a();
15812 b();
15813 c();ˇ»
15814 }
15815 "});
15816}
15817
15818#[gpui::test]
15819async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15820 init_test(cx, |_| {});
15821 let mut cx = EditorTestContext::new(cx).await;
15822 let language = Arc::new(Language::new(
15823 LanguageConfig {
15824 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15825 ..Default::default()
15826 },
15827 Some(tree_sitter_rust::LANGUAGE.into()),
15828 ));
15829 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15830
15831 let toggle_comments = &ToggleComments {
15832 advance_downwards: false,
15833 ignore_indent: true,
15834 };
15835
15836 // If multiple selections intersect a line, the line is only toggled once.
15837 cx.set_state(indoc! {"
15838 fn a() {
15839 // «b();
15840 // c();
15841 // ˇ» d();
15842 }
15843 "});
15844
15845 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15846
15847 cx.assert_editor_state(indoc! {"
15848 fn a() {
15849 «b();
15850 c();
15851 ˇ» d();
15852 }
15853 "});
15854
15855 // The comment prefix is inserted at the beginning of each line
15856 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15857
15858 cx.assert_editor_state(indoc! {"
15859 fn a() {
15860 // «b();
15861 // c();
15862 // ˇ» d();
15863 }
15864 "});
15865
15866 // If a selection ends at the beginning of a line, that line is not toggled.
15867 cx.set_selections_state(indoc! {"
15868 fn a() {
15869 // b();
15870 // «c();
15871 ˇ»// d();
15872 }
15873 "});
15874
15875 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15876
15877 cx.assert_editor_state(indoc! {"
15878 fn a() {
15879 // b();
15880 «c();
15881 ˇ»// d();
15882 }
15883 "});
15884
15885 // If a selection span a single line and is empty, the line is toggled.
15886 cx.set_state(indoc! {"
15887 fn a() {
15888 a();
15889 b();
15890 ˇ
15891 }
15892 "});
15893
15894 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15895
15896 cx.assert_editor_state(indoc! {"
15897 fn a() {
15898 a();
15899 b();
15900 //ˇ
15901 }
15902 "});
15903
15904 // If a selection span multiple lines, empty lines are not toggled.
15905 cx.set_state(indoc! {"
15906 fn a() {
15907 «a();
15908
15909 c();ˇ»
15910 }
15911 "});
15912
15913 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15914
15915 cx.assert_editor_state(indoc! {"
15916 fn a() {
15917 // «a();
15918
15919 // c();ˇ»
15920 }
15921 "});
15922
15923 // If a selection includes multiple comment prefixes, all lines are uncommented.
15924 cx.set_state(indoc! {"
15925 fn a() {
15926 // «a();
15927 /// b();
15928 //! c();ˇ»
15929 }
15930 "});
15931
15932 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15933
15934 cx.assert_editor_state(indoc! {"
15935 fn a() {
15936 «a();
15937 b();
15938 c();ˇ»
15939 }
15940 "});
15941}
15942
15943#[gpui::test]
15944async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15945 init_test(cx, |_| {});
15946
15947 let language = Arc::new(Language::new(
15948 LanguageConfig {
15949 line_comments: vec!["// ".into()],
15950 ..Default::default()
15951 },
15952 Some(tree_sitter_rust::LANGUAGE.into()),
15953 ));
15954
15955 let mut cx = EditorTestContext::new(cx).await;
15956
15957 cx.language_registry().add(language.clone());
15958 cx.update_buffer(|buffer, cx| {
15959 buffer.set_language(Some(language), cx);
15960 });
15961
15962 let toggle_comments = &ToggleComments {
15963 advance_downwards: true,
15964 ignore_indent: false,
15965 };
15966
15967 // Single cursor on one line -> advance
15968 // Cursor moves horizontally 3 characters as well on non-blank line
15969 cx.set_state(indoc!(
15970 "fn a() {
15971 ˇdog();
15972 cat();
15973 }"
15974 ));
15975 cx.update_editor(|editor, window, cx| {
15976 editor.toggle_comments(toggle_comments, window, cx);
15977 });
15978 cx.assert_editor_state(indoc!(
15979 "fn a() {
15980 // dog();
15981 catˇ();
15982 }"
15983 ));
15984
15985 // Single selection on one line -> don't advance
15986 cx.set_state(indoc!(
15987 "fn a() {
15988 «dog()ˇ»;
15989 cat();
15990 }"
15991 ));
15992 cx.update_editor(|editor, window, cx| {
15993 editor.toggle_comments(toggle_comments, window, cx);
15994 });
15995 cx.assert_editor_state(indoc!(
15996 "fn a() {
15997 // «dog()ˇ»;
15998 cat();
15999 }"
16000 ));
16001
16002 // Multiple cursors on one line -> advance
16003 cx.set_state(indoc!(
16004 "fn a() {
16005 ˇdˇog();
16006 cat();
16007 }"
16008 ));
16009 cx.update_editor(|editor, window, cx| {
16010 editor.toggle_comments(toggle_comments, window, cx);
16011 });
16012 cx.assert_editor_state(indoc!(
16013 "fn a() {
16014 // dog();
16015 catˇ(ˇ);
16016 }"
16017 ));
16018
16019 // Multiple cursors on one line, with selection -> don't advance
16020 cx.set_state(indoc!(
16021 "fn a() {
16022 ˇdˇog«()ˇ»;
16023 cat();
16024 }"
16025 ));
16026 cx.update_editor(|editor, window, cx| {
16027 editor.toggle_comments(toggle_comments, window, cx);
16028 });
16029 cx.assert_editor_state(indoc!(
16030 "fn a() {
16031 // ˇdˇog«()ˇ»;
16032 cat();
16033 }"
16034 ));
16035
16036 // Single cursor on one line -> advance
16037 // Cursor moves to column 0 on blank line
16038 cx.set_state(indoc!(
16039 "fn a() {
16040 ˇdog();
16041
16042 cat();
16043 }"
16044 ));
16045 cx.update_editor(|editor, window, cx| {
16046 editor.toggle_comments(toggle_comments, window, cx);
16047 });
16048 cx.assert_editor_state(indoc!(
16049 "fn a() {
16050 // dog();
16051 ˇ
16052 cat();
16053 }"
16054 ));
16055
16056 // Single cursor on one line -> advance
16057 // Cursor starts and ends at column 0
16058 cx.set_state(indoc!(
16059 "fn a() {
16060 ˇ dog();
16061 cat();
16062 }"
16063 ));
16064 cx.update_editor(|editor, window, cx| {
16065 editor.toggle_comments(toggle_comments, window, cx);
16066 });
16067 cx.assert_editor_state(indoc!(
16068 "fn a() {
16069 // dog();
16070 ˇ cat();
16071 }"
16072 ));
16073}
16074
16075#[gpui::test]
16076async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16077 init_test(cx, |_| {});
16078
16079 let mut cx = EditorTestContext::new(cx).await;
16080
16081 let html_language = Arc::new(
16082 Language::new(
16083 LanguageConfig {
16084 name: "HTML".into(),
16085 block_comment: Some(BlockCommentConfig {
16086 start: "<!-- ".into(),
16087 prefix: "".into(),
16088 end: " -->".into(),
16089 tab_size: 0,
16090 }),
16091 ..Default::default()
16092 },
16093 Some(tree_sitter_html::LANGUAGE.into()),
16094 )
16095 .with_injection_query(
16096 r#"
16097 (script_element
16098 (raw_text) @injection.content
16099 (#set! injection.language "javascript"))
16100 "#,
16101 )
16102 .unwrap(),
16103 );
16104
16105 let javascript_language = Arc::new(Language::new(
16106 LanguageConfig {
16107 name: "JavaScript".into(),
16108 line_comments: vec!["// ".into()],
16109 ..Default::default()
16110 },
16111 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16112 ));
16113
16114 cx.language_registry().add(html_language.clone());
16115 cx.language_registry().add(javascript_language);
16116 cx.update_buffer(|buffer, cx| {
16117 buffer.set_language(Some(html_language), cx);
16118 });
16119
16120 // Toggle comments for empty selections
16121 cx.set_state(
16122 &r#"
16123 <p>A</p>ˇ
16124 <p>B</p>ˇ
16125 <p>C</p>ˇ
16126 "#
16127 .unindent(),
16128 );
16129 cx.update_editor(|editor, window, cx| {
16130 editor.toggle_comments(&ToggleComments::default(), window, cx)
16131 });
16132 cx.assert_editor_state(
16133 &r#"
16134 <!-- <p>A</p>ˇ -->
16135 <!-- <p>B</p>ˇ -->
16136 <!-- <p>C</p>ˇ -->
16137 "#
16138 .unindent(),
16139 );
16140 cx.update_editor(|editor, window, cx| {
16141 editor.toggle_comments(&ToggleComments::default(), window, cx)
16142 });
16143 cx.assert_editor_state(
16144 &r#"
16145 <p>A</p>ˇ
16146 <p>B</p>ˇ
16147 <p>C</p>ˇ
16148 "#
16149 .unindent(),
16150 );
16151
16152 // Toggle comments for mixture of empty and non-empty selections, where
16153 // multiple selections occupy a given line.
16154 cx.set_state(
16155 &r#"
16156 <p>A«</p>
16157 <p>ˇ»B</p>ˇ
16158 <p>C«</p>
16159 <p>ˇ»D</p>ˇ
16160 "#
16161 .unindent(),
16162 );
16163
16164 cx.update_editor(|editor, window, cx| {
16165 editor.toggle_comments(&ToggleComments::default(), window, cx)
16166 });
16167 cx.assert_editor_state(
16168 &r#"
16169 <!-- <p>A«</p>
16170 <p>ˇ»B</p>ˇ -->
16171 <!-- <p>C«</p>
16172 <p>ˇ»D</p>ˇ -->
16173 "#
16174 .unindent(),
16175 );
16176 cx.update_editor(|editor, window, cx| {
16177 editor.toggle_comments(&ToggleComments::default(), window, cx)
16178 });
16179 cx.assert_editor_state(
16180 &r#"
16181 <p>A«</p>
16182 <p>ˇ»B</p>ˇ
16183 <p>C«</p>
16184 <p>ˇ»D</p>ˇ
16185 "#
16186 .unindent(),
16187 );
16188
16189 // Toggle comments when different languages are active for different
16190 // selections.
16191 cx.set_state(
16192 &r#"
16193 ˇ<script>
16194 ˇvar x = new Y();
16195 ˇ</script>
16196 "#
16197 .unindent(),
16198 );
16199 cx.executor().run_until_parked();
16200 cx.update_editor(|editor, window, cx| {
16201 editor.toggle_comments(&ToggleComments::default(), window, cx)
16202 });
16203 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16204 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16205 cx.assert_editor_state(
16206 &r#"
16207 <!-- ˇ<script> -->
16208 // ˇvar x = new Y();
16209 <!-- ˇ</script> -->
16210 "#
16211 .unindent(),
16212 );
16213}
16214
16215#[gpui::test]
16216fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16217 init_test(cx, |_| {});
16218
16219 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16220 let multibuffer = cx.new(|cx| {
16221 let mut multibuffer = MultiBuffer::new(ReadWrite);
16222 multibuffer.push_excerpts(
16223 buffer.clone(),
16224 [
16225 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16226 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16227 ],
16228 cx,
16229 );
16230 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16231 multibuffer
16232 });
16233
16234 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16235 editor.update_in(cx, |editor, window, cx| {
16236 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16237 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16238 s.select_ranges([
16239 Point::new(0, 0)..Point::new(0, 0),
16240 Point::new(1, 0)..Point::new(1, 0),
16241 ])
16242 });
16243
16244 editor.handle_input("X", window, cx);
16245 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16246 assert_eq!(
16247 editor.selections.ranges(&editor.display_snapshot(cx)),
16248 [
16249 Point::new(0, 1)..Point::new(0, 1),
16250 Point::new(1, 1)..Point::new(1, 1),
16251 ]
16252 );
16253
16254 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16256 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16257 });
16258 editor.backspace(&Default::default(), window, cx);
16259 assert_eq!(editor.text(cx), "Xa\nbbb");
16260 assert_eq!(
16261 editor.selections.ranges(&editor.display_snapshot(cx)),
16262 [Point::new(1, 0)..Point::new(1, 0)]
16263 );
16264
16265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16266 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16267 });
16268 editor.backspace(&Default::default(), window, cx);
16269 assert_eq!(editor.text(cx), "X\nbb");
16270 assert_eq!(
16271 editor.selections.ranges(&editor.display_snapshot(cx)),
16272 [Point::new(0, 1)..Point::new(0, 1)]
16273 );
16274 });
16275}
16276
16277#[gpui::test]
16278fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16279 init_test(cx, |_| {});
16280
16281 let markers = vec![('[', ']').into(), ('(', ')').into()];
16282 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16283 indoc! {"
16284 [aaaa
16285 (bbbb]
16286 cccc)",
16287 },
16288 markers.clone(),
16289 );
16290 let excerpt_ranges = markers.into_iter().map(|marker| {
16291 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16292 ExcerptRange::new(context)
16293 });
16294 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16295 let multibuffer = cx.new(|cx| {
16296 let mut multibuffer = MultiBuffer::new(ReadWrite);
16297 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16298 multibuffer
16299 });
16300
16301 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16302 editor.update_in(cx, |editor, window, cx| {
16303 let (expected_text, selection_ranges) = marked_text_ranges(
16304 indoc! {"
16305 aaaa
16306 bˇbbb
16307 bˇbbˇb
16308 cccc"
16309 },
16310 true,
16311 );
16312 assert_eq!(editor.text(cx), expected_text);
16313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16314 s.select_ranges(selection_ranges)
16315 });
16316
16317 editor.handle_input("X", window, cx);
16318
16319 let (expected_text, expected_selections) = marked_text_ranges(
16320 indoc! {"
16321 aaaa
16322 bXˇbbXb
16323 bXˇbbXˇb
16324 cccc"
16325 },
16326 false,
16327 );
16328 assert_eq!(editor.text(cx), expected_text);
16329 assert_eq!(
16330 editor.selections.ranges(&editor.display_snapshot(cx)),
16331 expected_selections
16332 );
16333
16334 editor.newline(&Newline, window, cx);
16335 let (expected_text, expected_selections) = marked_text_ranges(
16336 indoc! {"
16337 aaaa
16338 bX
16339 ˇbbX
16340 b
16341 bX
16342 ˇbbX
16343 ˇb
16344 cccc"
16345 },
16346 false,
16347 );
16348 assert_eq!(editor.text(cx), expected_text);
16349 assert_eq!(
16350 editor.selections.ranges(&editor.display_snapshot(cx)),
16351 expected_selections
16352 );
16353 });
16354}
16355
16356#[gpui::test]
16357fn test_refresh_selections(cx: &mut TestAppContext) {
16358 init_test(cx, |_| {});
16359
16360 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16361 let mut excerpt1_id = None;
16362 let multibuffer = cx.new(|cx| {
16363 let mut multibuffer = MultiBuffer::new(ReadWrite);
16364 excerpt1_id = multibuffer
16365 .push_excerpts(
16366 buffer.clone(),
16367 [
16368 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16369 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16370 ],
16371 cx,
16372 )
16373 .into_iter()
16374 .next();
16375 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16376 multibuffer
16377 });
16378
16379 let editor = cx.add_window(|window, cx| {
16380 let mut editor = build_editor(multibuffer.clone(), window, cx);
16381 let snapshot = editor.snapshot(window, cx);
16382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16383 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16384 });
16385 editor.begin_selection(
16386 Point::new(2, 1).to_display_point(&snapshot),
16387 true,
16388 1,
16389 window,
16390 cx,
16391 );
16392 assert_eq!(
16393 editor.selections.ranges(&editor.display_snapshot(cx)),
16394 [
16395 Point::new(1, 3)..Point::new(1, 3),
16396 Point::new(2, 1)..Point::new(2, 1),
16397 ]
16398 );
16399 editor
16400 });
16401
16402 // Refreshing selections is a no-op when excerpts haven't changed.
16403 _ = editor.update(cx, |editor, window, cx| {
16404 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16405 assert_eq!(
16406 editor.selections.ranges(&editor.display_snapshot(cx)),
16407 [
16408 Point::new(1, 3)..Point::new(1, 3),
16409 Point::new(2, 1)..Point::new(2, 1),
16410 ]
16411 );
16412 });
16413
16414 multibuffer.update(cx, |multibuffer, cx| {
16415 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16416 });
16417 _ = editor.update(cx, |editor, window, cx| {
16418 // Removing an excerpt causes the first selection to become degenerate.
16419 assert_eq!(
16420 editor.selections.ranges(&editor.display_snapshot(cx)),
16421 [
16422 Point::new(0, 0)..Point::new(0, 0),
16423 Point::new(0, 1)..Point::new(0, 1)
16424 ]
16425 );
16426
16427 // Refreshing selections will relocate the first selection to the original buffer
16428 // location.
16429 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16430 assert_eq!(
16431 editor.selections.ranges(&editor.display_snapshot(cx)),
16432 [
16433 Point::new(0, 1)..Point::new(0, 1),
16434 Point::new(0, 3)..Point::new(0, 3)
16435 ]
16436 );
16437 assert!(editor.selections.pending_anchor().is_some());
16438 });
16439}
16440
16441#[gpui::test]
16442fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16443 init_test(cx, |_| {});
16444
16445 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16446 let mut excerpt1_id = None;
16447 let multibuffer = cx.new(|cx| {
16448 let mut multibuffer = MultiBuffer::new(ReadWrite);
16449 excerpt1_id = multibuffer
16450 .push_excerpts(
16451 buffer.clone(),
16452 [
16453 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16454 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16455 ],
16456 cx,
16457 )
16458 .into_iter()
16459 .next();
16460 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16461 multibuffer
16462 });
16463
16464 let editor = cx.add_window(|window, cx| {
16465 let mut editor = build_editor(multibuffer.clone(), window, cx);
16466 let snapshot = editor.snapshot(window, cx);
16467 editor.begin_selection(
16468 Point::new(1, 3).to_display_point(&snapshot),
16469 false,
16470 1,
16471 window,
16472 cx,
16473 );
16474 assert_eq!(
16475 editor.selections.ranges(&editor.display_snapshot(cx)),
16476 [Point::new(1, 3)..Point::new(1, 3)]
16477 );
16478 editor
16479 });
16480
16481 multibuffer.update(cx, |multibuffer, cx| {
16482 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16483 });
16484 _ = editor.update(cx, |editor, window, cx| {
16485 assert_eq!(
16486 editor.selections.ranges(&editor.display_snapshot(cx)),
16487 [Point::new(0, 0)..Point::new(0, 0)]
16488 );
16489
16490 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16492 assert_eq!(
16493 editor.selections.ranges(&editor.display_snapshot(cx)),
16494 [Point::new(0, 3)..Point::new(0, 3)]
16495 );
16496 assert!(editor.selections.pending_anchor().is_some());
16497 });
16498}
16499
16500#[gpui::test]
16501async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16502 init_test(cx, |_| {});
16503
16504 let language = Arc::new(
16505 Language::new(
16506 LanguageConfig {
16507 brackets: BracketPairConfig {
16508 pairs: vec![
16509 BracketPair {
16510 start: "{".to_string(),
16511 end: "}".to_string(),
16512 close: true,
16513 surround: true,
16514 newline: true,
16515 },
16516 BracketPair {
16517 start: "/* ".to_string(),
16518 end: " */".to_string(),
16519 close: true,
16520 surround: true,
16521 newline: true,
16522 },
16523 ],
16524 ..Default::default()
16525 },
16526 ..Default::default()
16527 },
16528 Some(tree_sitter_rust::LANGUAGE.into()),
16529 )
16530 .with_indents_query("")
16531 .unwrap(),
16532 );
16533
16534 let text = concat!(
16535 "{ }\n", //
16536 " x\n", //
16537 " /* */\n", //
16538 "x\n", //
16539 "{{} }\n", //
16540 );
16541
16542 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16543 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16544 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16545 editor
16546 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16547 .await;
16548
16549 editor.update_in(cx, |editor, window, cx| {
16550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16551 s.select_display_ranges([
16552 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16553 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16554 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16555 ])
16556 });
16557 editor.newline(&Newline, window, cx);
16558
16559 assert_eq!(
16560 editor.buffer().read(cx).read(cx).text(),
16561 concat!(
16562 "{ \n", // Suppress rustfmt
16563 "\n", //
16564 "}\n", //
16565 " x\n", //
16566 " /* \n", //
16567 " \n", //
16568 " */\n", //
16569 "x\n", //
16570 "{{} \n", //
16571 "}\n", //
16572 )
16573 );
16574 });
16575}
16576
16577#[gpui::test]
16578fn test_highlighted_ranges(cx: &mut TestAppContext) {
16579 init_test(cx, |_| {});
16580
16581 let editor = cx.add_window(|window, cx| {
16582 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16583 build_editor(buffer, window, cx)
16584 });
16585
16586 _ = editor.update(cx, |editor, window, cx| {
16587 struct Type1;
16588 struct Type2;
16589
16590 let buffer = editor.buffer.read(cx).snapshot(cx);
16591
16592 let anchor_range =
16593 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16594
16595 editor.highlight_background::<Type1>(
16596 &[
16597 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16598 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16599 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16600 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16601 ],
16602 |_| Hsla::red(),
16603 cx,
16604 );
16605 editor.highlight_background::<Type2>(
16606 &[
16607 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16608 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16609 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16610 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16611 ],
16612 |_| Hsla::green(),
16613 cx,
16614 );
16615
16616 let snapshot = editor.snapshot(window, cx);
16617 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16618 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16619 &snapshot,
16620 cx.theme(),
16621 );
16622 assert_eq!(
16623 highlighted_ranges,
16624 &[
16625 (
16626 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16627 Hsla::green(),
16628 ),
16629 (
16630 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16631 Hsla::red(),
16632 ),
16633 (
16634 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16635 Hsla::green(),
16636 ),
16637 (
16638 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16639 Hsla::red(),
16640 ),
16641 ]
16642 );
16643 assert_eq!(
16644 editor.sorted_background_highlights_in_range(
16645 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16646 &snapshot,
16647 cx.theme(),
16648 ),
16649 &[(
16650 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16651 Hsla::red(),
16652 )]
16653 );
16654 });
16655}
16656
16657#[gpui::test]
16658async fn test_following(cx: &mut TestAppContext) {
16659 init_test(cx, |_| {});
16660
16661 let fs = FakeFs::new(cx.executor());
16662 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16663
16664 let buffer = project.update(cx, |project, cx| {
16665 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16666 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16667 });
16668 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16669 let follower = cx.update(|cx| {
16670 cx.open_window(
16671 WindowOptions {
16672 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16673 gpui::Point::new(px(0.), px(0.)),
16674 gpui::Point::new(px(10.), px(80.)),
16675 ))),
16676 ..Default::default()
16677 },
16678 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16679 )
16680 .unwrap()
16681 });
16682
16683 let is_still_following = Rc::new(RefCell::new(true));
16684 let follower_edit_event_count = Rc::new(RefCell::new(0));
16685 let pending_update = Rc::new(RefCell::new(None));
16686 let leader_entity = leader.root(cx).unwrap();
16687 let follower_entity = follower.root(cx).unwrap();
16688 _ = follower.update(cx, {
16689 let update = pending_update.clone();
16690 let is_still_following = is_still_following.clone();
16691 let follower_edit_event_count = follower_edit_event_count.clone();
16692 |_, window, cx| {
16693 cx.subscribe_in(
16694 &leader_entity,
16695 window,
16696 move |_, leader, event, window, cx| {
16697 leader.read(cx).add_event_to_update_proto(
16698 event,
16699 &mut update.borrow_mut(),
16700 window,
16701 cx,
16702 );
16703 },
16704 )
16705 .detach();
16706
16707 cx.subscribe_in(
16708 &follower_entity,
16709 window,
16710 move |_, _, event: &EditorEvent, _window, _cx| {
16711 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16712 *is_still_following.borrow_mut() = false;
16713 }
16714
16715 if let EditorEvent::BufferEdited = event {
16716 *follower_edit_event_count.borrow_mut() += 1;
16717 }
16718 },
16719 )
16720 .detach();
16721 }
16722 });
16723
16724 // Update the selections only
16725 _ = leader.update(cx, |leader, window, cx| {
16726 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16727 s.select_ranges([1..1])
16728 });
16729 });
16730 follower
16731 .update(cx, |follower, window, cx| {
16732 follower.apply_update_proto(
16733 &project,
16734 pending_update.borrow_mut().take().unwrap(),
16735 window,
16736 cx,
16737 )
16738 })
16739 .unwrap()
16740 .await
16741 .unwrap();
16742 _ = follower.update(cx, |follower, _, cx| {
16743 assert_eq!(
16744 follower.selections.ranges(&follower.display_snapshot(cx)),
16745 vec![1..1]
16746 );
16747 });
16748 assert!(*is_still_following.borrow());
16749 assert_eq!(*follower_edit_event_count.borrow(), 0);
16750
16751 // Update the scroll position only
16752 _ = leader.update(cx, |leader, window, cx| {
16753 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16754 });
16755 follower
16756 .update(cx, |follower, window, cx| {
16757 follower.apply_update_proto(
16758 &project,
16759 pending_update.borrow_mut().take().unwrap(),
16760 window,
16761 cx,
16762 )
16763 })
16764 .unwrap()
16765 .await
16766 .unwrap();
16767 assert_eq!(
16768 follower
16769 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16770 .unwrap(),
16771 gpui::Point::new(1.5, 3.5)
16772 );
16773 assert!(*is_still_following.borrow());
16774 assert_eq!(*follower_edit_event_count.borrow(), 0);
16775
16776 // Update the selections and scroll position. The follower's scroll position is updated
16777 // via autoscroll, not via the leader's exact scroll position.
16778 _ = leader.update(cx, |leader, window, cx| {
16779 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16780 s.select_ranges([0..0])
16781 });
16782 leader.request_autoscroll(Autoscroll::newest(), cx);
16783 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16784 });
16785 follower
16786 .update(cx, |follower, window, cx| {
16787 follower.apply_update_proto(
16788 &project,
16789 pending_update.borrow_mut().take().unwrap(),
16790 window,
16791 cx,
16792 )
16793 })
16794 .unwrap()
16795 .await
16796 .unwrap();
16797 _ = follower.update(cx, |follower, _, cx| {
16798 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16799 assert_eq!(
16800 follower.selections.ranges(&follower.display_snapshot(cx)),
16801 vec![0..0]
16802 );
16803 });
16804 assert!(*is_still_following.borrow());
16805
16806 // Creating a pending selection that precedes another selection
16807 _ = leader.update(cx, |leader, window, cx| {
16808 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16809 s.select_ranges([1..1])
16810 });
16811 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16812 });
16813 follower
16814 .update(cx, |follower, window, cx| {
16815 follower.apply_update_proto(
16816 &project,
16817 pending_update.borrow_mut().take().unwrap(),
16818 window,
16819 cx,
16820 )
16821 })
16822 .unwrap()
16823 .await
16824 .unwrap();
16825 _ = follower.update(cx, |follower, _, cx| {
16826 assert_eq!(
16827 follower.selections.ranges(&follower.display_snapshot(cx)),
16828 vec![0..0, 1..1]
16829 );
16830 });
16831 assert!(*is_still_following.borrow());
16832
16833 // Extend the pending selection so that it surrounds another selection
16834 _ = leader.update(cx, |leader, window, cx| {
16835 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16836 });
16837 follower
16838 .update(cx, |follower, window, cx| {
16839 follower.apply_update_proto(
16840 &project,
16841 pending_update.borrow_mut().take().unwrap(),
16842 window,
16843 cx,
16844 )
16845 })
16846 .unwrap()
16847 .await
16848 .unwrap();
16849 _ = follower.update(cx, |follower, _, cx| {
16850 assert_eq!(
16851 follower.selections.ranges(&follower.display_snapshot(cx)),
16852 vec![0..2]
16853 );
16854 });
16855
16856 // Scrolling locally breaks the follow
16857 _ = follower.update(cx, |follower, window, cx| {
16858 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16859 follower.set_scroll_anchor(
16860 ScrollAnchor {
16861 anchor: top_anchor,
16862 offset: gpui::Point::new(0.0, 0.5),
16863 },
16864 window,
16865 cx,
16866 );
16867 });
16868 assert!(!(*is_still_following.borrow()));
16869}
16870
16871#[gpui::test]
16872async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16873 init_test(cx, |_| {});
16874
16875 let fs = FakeFs::new(cx.executor());
16876 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16877 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16878 let pane = workspace
16879 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16880 .unwrap();
16881
16882 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16883
16884 let leader = pane.update_in(cx, |_, window, cx| {
16885 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16886 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16887 });
16888
16889 // Start following the editor when it has no excerpts.
16890 let mut state_message =
16891 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16892 let workspace_entity = workspace.root(cx).unwrap();
16893 let follower_1 = cx
16894 .update_window(*workspace.deref(), |_, window, cx| {
16895 Editor::from_state_proto(
16896 workspace_entity,
16897 ViewId {
16898 creator: CollaboratorId::PeerId(PeerId::default()),
16899 id: 0,
16900 },
16901 &mut state_message,
16902 window,
16903 cx,
16904 )
16905 })
16906 .unwrap()
16907 .unwrap()
16908 .await
16909 .unwrap();
16910
16911 let update_message = Rc::new(RefCell::new(None));
16912 follower_1.update_in(cx, {
16913 let update = update_message.clone();
16914 |_, window, cx| {
16915 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16916 leader.read(cx).add_event_to_update_proto(
16917 event,
16918 &mut update.borrow_mut(),
16919 window,
16920 cx,
16921 );
16922 })
16923 .detach();
16924 }
16925 });
16926
16927 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16928 (
16929 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16930 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16931 )
16932 });
16933
16934 // Insert some excerpts.
16935 leader.update(cx, |leader, cx| {
16936 leader.buffer.update(cx, |multibuffer, cx| {
16937 multibuffer.set_excerpts_for_path(
16938 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16939 buffer_1.clone(),
16940 vec![
16941 Point::row_range(0..3),
16942 Point::row_range(1..6),
16943 Point::row_range(12..15),
16944 ],
16945 0,
16946 cx,
16947 );
16948 multibuffer.set_excerpts_for_path(
16949 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16950 buffer_2.clone(),
16951 vec![Point::row_range(0..6), Point::row_range(8..12)],
16952 0,
16953 cx,
16954 );
16955 });
16956 });
16957
16958 // Apply the update of adding the excerpts.
16959 follower_1
16960 .update_in(cx, |follower, window, cx| {
16961 follower.apply_update_proto(
16962 &project,
16963 update_message.borrow().clone().unwrap(),
16964 window,
16965 cx,
16966 )
16967 })
16968 .await
16969 .unwrap();
16970 assert_eq!(
16971 follower_1.update(cx, |editor, cx| editor.text(cx)),
16972 leader.update(cx, |editor, cx| editor.text(cx))
16973 );
16974 update_message.borrow_mut().take();
16975
16976 // Start following separately after it already has excerpts.
16977 let mut state_message =
16978 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16979 let workspace_entity = workspace.root(cx).unwrap();
16980 let follower_2 = cx
16981 .update_window(*workspace.deref(), |_, window, cx| {
16982 Editor::from_state_proto(
16983 workspace_entity,
16984 ViewId {
16985 creator: CollaboratorId::PeerId(PeerId::default()),
16986 id: 0,
16987 },
16988 &mut state_message,
16989 window,
16990 cx,
16991 )
16992 })
16993 .unwrap()
16994 .unwrap()
16995 .await
16996 .unwrap();
16997 assert_eq!(
16998 follower_2.update(cx, |editor, cx| editor.text(cx)),
16999 leader.update(cx, |editor, cx| editor.text(cx))
17000 );
17001
17002 // Remove some excerpts.
17003 leader.update(cx, |leader, cx| {
17004 leader.buffer.update(cx, |multibuffer, cx| {
17005 let excerpt_ids = multibuffer.excerpt_ids();
17006 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17007 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17008 });
17009 });
17010
17011 // Apply the update of removing the excerpts.
17012 follower_1
17013 .update_in(cx, |follower, window, cx| {
17014 follower.apply_update_proto(
17015 &project,
17016 update_message.borrow().clone().unwrap(),
17017 window,
17018 cx,
17019 )
17020 })
17021 .await
17022 .unwrap();
17023 follower_2
17024 .update_in(cx, |follower, window, cx| {
17025 follower.apply_update_proto(
17026 &project,
17027 update_message.borrow().clone().unwrap(),
17028 window,
17029 cx,
17030 )
17031 })
17032 .await
17033 .unwrap();
17034 update_message.borrow_mut().take();
17035 assert_eq!(
17036 follower_1.update(cx, |editor, cx| editor.text(cx)),
17037 leader.update(cx, |editor, cx| editor.text(cx))
17038 );
17039}
17040
17041#[gpui::test]
17042async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17043 init_test(cx, |_| {});
17044
17045 let mut cx = EditorTestContext::new(cx).await;
17046 let lsp_store =
17047 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17048
17049 cx.set_state(indoc! {"
17050 ˇfn func(abc def: i32) -> u32 {
17051 }
17052 "});
17053
17054 cx.update(|_, cx| {
17055 lsp_store.update(cx, |lsp_store, cx| {
17056 lsp_store
17057 .update_diagnostics(
17058 LanguageServerId(0),
17059 lsp::PublishDiagnosticsParams {
17060 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17061 version: None,
17062 diagnostics: vec![
17063 lsp::Diagnostic {
17064 range: lsp::Range::new(
17065 lsp::Position::new(0, 11),
17066 lsp::Position::new(0, 12),
17067 ),
17068 severity: Some(lsp::DiagnosticSeverity::ERROR),
17069 ..Default::default()
17070 },
17071 lsp::Diagnostic {
17072 range: lsp::Range::new(
17073 lsp::Position::new(0, 12),
17074 lsp::Position::new(0, 15),
17075 ),
17076 severity: Some(lsp::DiagnosticSeverity::ERROR),
17077 ..Default::default()
17078 },
17079 lsp::Diagnostic {
17080 range: lsp::Range::new(
17081 lsp::Position::new(0, 25),
17082 lsp::Position::new(0, 28),
17083 ),
17084 severity: Some(lsp::DiagnosticSeverity::ERROR),
17085 ..Default::default()
17086 },
17087 ],
17088 },
17089 None,
17090 DiagnosticSourceKind::Pushed,
17091 &[],
17092 cx,
17093 )
17094 .unwrap()
17095 });
17096 });
17097
17098 executor.run_until_parked();
17099
17100 cx.update_editor(|editor, window, cx| {
17101 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17102 });
17103
17104 cx.assert_editor_state(indoc! {"
17105 fn func(abc def: i32) -> ˇu32 {
17106 }
17107 "});
17108
17109 cx.update_editor(|editor, window, cx| {
17110 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17111 });
17112
17113 cx.assert_editor_state(indoc! {"
17114 fn func(abc ˇdef: i32) -> u32 {
17115 }
17116 "});
17117
17118 cx.update_editor(|editor, window, cx| {
17119 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17120 });
17121
17122 cx.assert_editor_state(indoc! {"
17123 fn func(abcˇ def: i32) -> u32 {
17124 }
17125 "});
17126
17127 cx.update_editor(|editor, window, cx| {
17128 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17129 });
17130
17131 cx.assert_editor_state(indoc! {"
17132 fn func(abc def: i32) -> ˇu32 {
17133 }
17134 "});
17135}
17136
17137#[gpui::test]
17138async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17139 init_test(cx, |_| {});
17140
17141 let mut cx = EditorTestContext::new(cx).await;
17142
17143 let diff_base = r#"
17144 use some::mod;
17145
17146 const A: u32 = 42;
17147
17148 fn main() {
17149 println!("hello");
17150
17151 println!("world");
17152 }
17153 "#
17154 .unindent();
17155
17156 // Edits are modified, removed, modified, added
17157 cx.set_state(
17158 &r#"
17159 use some::modified;
17160
17161 ˇ
17162 fn main() {
17163 println!("hello there");
17164
17165 println!("around the");
17166 println!("world");
17167 }
17168 "#
17169 .unindent(),
17170 );
17171
17172 cx.set_head_text(&diff_base);
17173 executor.run_until_parked();
17174
17175 cx.update_editor(|editor, window, cx| {
17176 //Wrap around the bottom of the buffer
17177 for _ in 0..3 {
17178 editor.go_to_next_hunk(&GoToHunk, window, cx);
17179 }
17180 });
17181
17182 cx.assert_editor_state(
17183 &r#"
17184 ˇuse some::modified;
17185
17186
17187 fn main() {
17188 println!("hello there");
17189
17190 println!("around the");
17191 println!("world");
17192 }
17193 "#
17194 .unindent(),
17195 );
17196
17197 cx.update_editor(|editor, window, cx| {
17198 //Wrap around the top of the buffer
17199 for _ in 0..2 {
17200 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17201 }
17202 });
17203
17204 cx.assert_editor_state(
17205 &r#"
17206 use some::modified;
17207
17208
17209 fn main() {
17210 ˇ println!("hello there");
17211
17212 println!("around the");
17213 println!("world");
17214 }
17215 "#
17216 .unindent(),
17217 );
17218
17219 cx.update_editor(|editor, window, cx| {
17220 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17221 });
17222
17223 cx.assert_editor_state(
17224 &r#"
17225 use some::modified;
17226
17227 ˇ
17228 fn main() {
17229 println!("hello there");
17230
17231 println!("around the");
17232 println!("world");
17233 }
17234 "#
17235 .unindent(),
17236 );
17237
17238 cx.update_editor(|editor, window, cx| {
17239 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17240 });
17241
17242 cx.assert_editor_state(
17243 &r#"
17244 ˇuse some::modified;
17245
17246
17247 fn main() {
17248 println!("hello there");
17249
17250 println!("around the");
17251 println!("world");
17252 }
17253 "#
17254 .unindent(),
17255 );
17256
17257 cx.update_editor(|editor, window, cx| {
17258 for _ in 0..2 {
17259 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17260 }
17261 });
17262
17263 cx.assert_editor_state(
17264 &r#"
17265 use some::modified;
17266
17267
17268 fn main() {
17269 ˇ println!("hello there");
17270
17271 println!("around the");
17272 println!("world");
17273 }
17274 "#
17275 .unindent(),
17276 );
17277
17278 cx.update_editor(|editor, window, cx| {
17279 editor.fold(&Fold, window, cx);
17280 });
17281
17282 cx.update_editor(|editor, window, cx| {
17283 editor.go_to_next_hunk(&GoToHunk, window, cx);
17284 });
17285
17286 cx.assert_editor_state(
17287 &r#"
17288 ˇuse some::modified;
17289
17290
17291 fn main() {
17292 println!("hello there");
17293
17294 println!("around the");
17295 println!("world");
17296 }
17297 "#
17298 .unindent(),
17299 );
17300}
17301
17302#[test]
17303fn test_split_words() {
17304 fn split(text: &str) -> Vec<&str> {
17305 split_words(text).collect()
17306 }
17307
17308 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17309 assert_eq!(split("hello_world"), &["hello_", "world"]);
17310 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17311 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17312 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17313 assert_eq!(split("helloworld"), &["helloworld"]);
17314
17315 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17316}
17317
17318#[gpui::test]
17319async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17320 init_test(cx, |_| {});
17321
17322 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17323 let mut assert = |before, after| {
17324 let _state_context = cx.set_state(before);
17325 cx.run_until_parked();
17326 cx.update_editor(|editor, window, cx| {
17327 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17328 });
17329 cx.run_until_parked();
17330 cx.assert_editor_state(after);
17331 };
17332
17333 // Outside bracket jumps to outside of matching bracket
17334 assert("console.logˇ(var);", "console.log(var)ˇ;");
17335 assert("console.log(var)ˇ;", "console.logˇ(var);");
17336
17337 // Inside bracket jumps to inside of matching bracket
17338 assert("console.log(ˇvar);", "console.log(varˇ);");
17339 assert("console.log(varˇ);", "console.log(ˇvar);");
17340
17341 // When outside a bracket and inside, favor jumping to the inside bracket
17342 assert(
17343 "console.log('foo', [1, 2, 3]ˇ);",
17344 "console.log(ˇ'foo', [1, 2, 3]);",
17345 );
17346 assert(
17347 "console.log(ˇ'foo', [1, 2, 3]);",
17348 "console.log('foo', [1, 2, 3]ˇ);",
17349 );
17350
17351 // Bias forward if two options are equally likely
17352 assert(
17353 "let result = curried_fun()ˇ();",
17354 "let result = curried_fun()()ˇ;",
17355 );
17356
17357 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17358 assert(
17359 indoc! {"
17360 function test() {
17361 console.log('test')ˇ
17362 }"},
17363 indoc! {"
17364 function test() {
17365 console.logˇ('test')
17366 }"},
17367 );
17368}
17369
17370#[gpui::test]
17371async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17372 init_test(cx, |_| {});
17373
17374 let fs = FakeFs::new(cx.executor());
17375 fs.insert_tree(
17376 path!("/a"),
17377 json!({
17378 "main.rs": "fn main() { let a = 5; }",
17379 "other.rs": "// Test file",
17380 }),
17381 )
17382 .await;
17383 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17384
17385 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17386 language_registry.add(Arc::new(Language::new(
17387 LanguageConfig {
17388 name: "Rust".into(),
17389 matcher: LanguageMatcher {
17390 path_suffixes: vec!["rs".to_string()],
17391 ..Default::default()
17392 },
17393 brackets: BracketPairConfig {
17394 pairs: vec![BracketPair {
17395 start: "{".to_string(),
17396 end: "}".to_string(),
17397 close: true,
17398 surround: true,
17399 newline: true,
17400 }],
17401 disabled_scopes_by_bracket_ix: Vec::new(),
17402 },
17403 ..Default::default()
17404 },
17405 Some(tree_sitter_rust::LANGUAGE.into()),
17406 )));
17407 let mut fake_servers = language_registry.register_fake_lsp(
17408 "Rust",
17409 FakeLspAdapter {
17410 capabilities: lsp::ServerCapabilities {
17411 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17412 first_trigger_character: "{".to_string(),
17413 more_trigger_character: None,
17414 }),
17415 ..Default::default()
17416 },
17417 ..Default::default()
17418 },
17419 );
17420
17421 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17422
17423 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17424
17425 let worktree_id = workspace
17426 .update(cx, |workspace, _, cx| {
17427 workspace.project().update(cx, |project, cx| {
17428 project.worktrees(cx).next().unwrap().read(cx).id()
17429 })
17430 })
17431 .unwrap();
17432
17433 let buffer = project
17434 .update(cx, |project, cx| {
17435 project.open_local_buffer(path!("/a/main.rs"), cx)
17436 })
17437 .await
17438 .unwrap();
17439 let editor_handle = workspace
17440 .update(cx, |workspace, window, cx| {
17441 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17442 })
17443 .unwrap()
17444 .await
17445 .unwrap()
17446 .downcast::<Editor>()
17447 .unwrap();
17448
17449 cx.executor().start_waiting();
17450 let fake_server = fake_servers.next().await.unwrap();
17451
17452 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17453 |params, _| async move {
17454 assert_eq!(
17455 params.text_document_position.text_document.uri,
17456 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17457 );
17458 assert_eq!(
17459 params.text_document_position.position,
17460 lsp::Position::new(0, 21),
17461 );
17462
17463 Ok(Some(vec![lsp::TextEdit {
17464 new_text: "]".to_string(),
17465 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17466 }]))
17467 },
17468 );
17469
17470 editor_handle.update_in(cx, |editor, window, cx| {
17471 window.focus(&editor.focus_handle(cx));
17472 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17473 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17474 });
17475 editor.handle_input("{", window, cx);
17476 });
17477
17478 cx.executor().run_until_parked();
17479
17480 buffer.update(cx, |buffer, _| {
17481 assert_eq!(
17482 buffer.text(),
17483 "fn main() { let a = {5}; }",
17484 "No extra braces from on type formatting should appear in the buffer"
17485 )
17486 });
17487}
17488
17489#[gpui::test(iterations = 20, seeds(31))]
17490async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17491 init_test(cx, |_| {});
17492
17493 let mut cx = EditorLspTestContext::new_rust(
17494 lsp::ServerCapabilities {
17495 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17496 first_trigger_character: ".".to_string(),
17497 more_trigger_character: None,
17498 }),
17499 ..Default::default()
17500 },
17501 cx,
17502 )
17503 .await;
17504
17505 cx.update_buffer(|buffer, _| {
17506 // This causes autoindent to be async.
17507 buffer.set_sync_parse_timeout(Duration::ZERO)
17508 });
17509
17510 cx.set_state("fn c() {\n d()ˇ\n}\n");
17511 cx.simulate_keystroke("\n");
17512 cx.run_until_parked();
17513
17514 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17515 let mut request =
17516 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17517 let buffer_cloned = buffer_cloned.clone();
17518 async move {
17519 buffer_cloned.update(&mut cx, |buffer, _| {
17520 assert_eq!(
17521 buffer.text(),
17522 "fn c() {\n d()\n .\n}\n",
17523 "OnTypeFormatting should triggered after autoindent applied"
17524 )
17525 })?;
17526
17527 Ok(Some(vec![]))
17528 }
17529 });
17530
17531 cx.simulate_keystroke(".");
17532 cx.run_until_parked();
17533
17534 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17535 assert!(request.next().await.is_some());
17536 request.close();
17537 assert!(request.next().await.is_none());
17538}
17539
17540#[gpui::test]
17541async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17542 init_test(cx, |_| {});
17543
17544 let fs = FakeFs::new(cx.executor());
17545 fs.insert_tree(
17546 path!("/a"),
17547 json!({
17548 "main.rs": "fn main() { let a = 5; }",
17549 "other.rs": "// Test file",
17550 }),
17551 )
17552 .await;
17553
17554 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17555
17556 let server_restarts = Arc::new(AtomicUsize::new(0));
17557 let closure_restarts = Arc::clone(&server_restarts);
17558 let language_server_name = "test language server";
17559 let language_name: LanguageName = "Rust".into();
17560
17561 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17562 language_registry.add(Arc::new(Language::new(
17563 LanguageConfig {
17564 name: language_name.clone(),
17565 matcher: LanguageMatcher {
17566 path_suffixes: vec!["rs".to_string()],
17567 ..Default::default()
17568 },
17569 ..Default::default()
17570 },
17571 Some(tree_sitter_rust::LANGUAGE.into()),
17572 )));
17573 let mut fake_servers = language_registry.register_fake_lsp(
17574 "Rust",
17575 FakeLspAdapter {
17576 name: language_server_name,
17577 initialization_options: Some(json!({
17578 "testOptionValue": true
17579 })),
17580 initializer: Some(Box::new(move |fake_server| {
17581 let task_restarts = Arc::clone(&closure_restarts);
17582 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17583 task_restarts.fetch_add(1, atomic::Ordering::Release);
17584 futures::future::ready(Ok(()))
17585 });
17586 })),
17587 ..Default::default()
17588 },
17589 );
17590
17591 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17592 let _buffer = project
17593 .update(cx, |project, cx| {
17594 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17595 })
17596 .await
17597 .unwrap();
17598 let _fake_server = fake_servers.next().await.unwrap();
17599 update_test_language_settings(cx, |language_settings| {
17600 language_settings.languages.0.insert(
17601 language_name.clone().0,
17602 LanguageSettingsContent {
17603 tab_size: NonZeroU32::new(8),
17604 ..Default::default()
17605 },
17606 );
17607 });
17608 cx.executor().run_until_parked();
17609 assert_eq!(
17610 server_restarts.load(atomic::Ordering::Acquire),
17611 0,
17612 "Should not restart LSP server on an unrelated change"
17613 );
17614
17615 update_test_project_settings(cx, |project_settings| {
17616 project_settings.lsp.insert(
17617 "Some other server name".into(),
17618 LspSettings {
17619 binary: None,
17620 settings: None,
17621 initialization_options: Some(json!({
17622 "some other init value": false
17623 })),
17624 enable_lsp_tasks: false,
17625 fetch: None,
17626 },
17627 );
17628 });
17629 cx.executor().run_until_parked();
17630 assert_eq!(
17631 server_restarts.load(atomic::Ordering::Acquire),
17632 0,
17633 "Should not restart LSP server on an unrelated LSP settings change"
17634 );
17635
17636 update_test_project_settings(cx, |project_settings| {
17637 project_settings.lsp.insert(
17638 language_server_name.into(),
17639 LspSettings {
17640 binary: None,
17641 settings: None,
17642 initialization_options: Some(json!({
17643 "anotherInitValue": false
17644 })),
17645 enable_lsp_tasks: false,
17646 fetch: None,
17647 },
17648 );
17649 });
17650 cx.executor().run_until_parked();
17651 assert_eq!(
17652 server_restarts.load(atomic::Ordering::Acquire),
17653 1,
17654 "Should restart LSP server on a related LSP settings change"
17655 );
17656
17657 update_test_project_settings(cx, |project_settings| {
17658 project_settings.lsp.insert(
17659 language_server_name.into(),
17660 LspSettings {
17661 binary: None,
17662 settings: None,
17663 initialization_options: Some(json!({
17664 "anotherInitValue": false
17665 })),
17666 enable_lsp_tasks: false,
17667 fetch: None,
17668 },
17669 );
17670 });
17671 cx.executor().run_until_parked();
17672 assert_eq!(
17673 server_restarts.load(atomic::Ordering::Acquire),
17674 1,
17675 "Should not restart LSP server on a related LSP settings change that is the same"
17676 );
17677
17678 update_test_project_settings(cx, |project_settings| {
17679 project_settings.lsp.insert(
17680 language_server_name.into(),
17681 LspSettings {
17682 binary: None,
17683 settings: None,
17684 initialization_options: None,
17685 enable_lsp_tasks: false,
17686 fetch: None,
17687 },
17688 );
17689 });
17690 cx.executor().run_until_parked();
17691 assert_eq!(
17692 server_restarts.load(atomic::Ordering::Acquire),
17693 2,
17694 "Should restart LSP server on another related LSP settings change"
17695 );
17696}
17697
17698#[gpui::test]
17699async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17700 init_test(cx, |_| {});
17701
17702 let mut cx = EditorLspTestContext::new_rust(
17703 lsp::ServerCapabilities {
17704 completion_provider: Some(lsp::CompletionOptions {
17705 trigger_characters: Some(vec![".".to_string()]),
17706 resolve_provider: Some(true),
17707 ..Default::default()
17708 }),
17709 ..Default::default()
17710 },
17711 cx,
17712 )
17713 .await;
17714
17715 cx.set_state("fn main() { let a = 2ˇ; }");
17716 cx.simulate_keystroke(".");
17717 let completion_item = lsp::CompletionItem {
17718 label: "some".into(),
17719 kind: Some(lsp::CompletionItemKind::SNIPPET),
17720 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17721 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17722 kind: lsp::MarkupKind::Markdown,
17723 value: "```rust\nSome(2)\n```".to_string(),
17724 })),
17725 deprecated: Some(false),
17726 sort_text: Some("fffffff2".to_string()),
17727 filter_text: Some("some".to_string()),
17728 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17729 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17730 range: lsp::Range {
17731 start: lsp::Position {
17732 line: 0,
17733 character: 22,
17734 },
17735 end: lsp::Position {
17736 line: 0,
17737 character: 22,
17738 },
17739 },
17740 new_text: "Some(2)".to_string(),
17741 })),
17742 additional_text_edits: Some(vec![lsp::TextEdit {
17743 range: lsp::Range {
17744 start: lsp::Position {
17745 line: 0,
17746 character: 20,
17747 },
17748 end: lsp::Position {
17749 line: 0,
17750 character: 22,
17751 },
17752 },
17753 new_text: "".to_string(),
17754 }]),
17755 ..Default::default()
17756 };
17757
17758 let closure_completion_item = completion_item.clone();
17759 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17760 let task_completion_item = closure_completion_item.clone();
17761 async move {
17762 Ok(Some(lsp::CompletionResponse::Array(vec![
17763 task_completion_item,
17764 ])))
17765 }
17766 });
17767
17768 request.next().await;
17769
17770 cx.condition(|editor, _| editor.context_menu_visible())
17771 .await;
17772 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17773 editor
17774 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17775 .unwrap()
17776 });
17777 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17778
17779 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17780 let task_completion_item = completion_item.clone();
17781 async move { Ok(task_completion_item) }
17782 })
17783 .next()
17784 .await
17785 .unwrap();
17786 apply_additional_edits.await.unwrap();
17787 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17788}
17789
17790#[gpui::test]
17791async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17792 init_test(cx, |_| {});
17793
17794 let mut cx = EditorLspTestContext::new_rust(
17795 lsp::ServerCapabilities {
17796 completion_provider: Some(lsp::CompletionOptions {
17797 trigger_characters: Some(vec![".".to_string()]),
17798 resolve_provider: Some(true),
17799 ..Default::default()
17800 }),
17801 ..Default::default()
17802 },
17803 cx,
17804 )
17805 .await;
17806
17807 cx.set_state("fn main() { let a = 2ˇ; }");
17808 cx.simulate_keystroke(".");
17809
17810 let item1 = lsp::CompletionItem {
17811 label: "method id()".to_string(),
17812 filter_text: Some("id".to_string()),
17813 detail: None,
17814 documentation: None,
17815 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17816 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17817 new_text: ".id".to_string(),
17818 })),
17819 ..lsp::CompletionItem::default()
17820 };
17821
17822 let item2 = lsp::CompletionItem {
17823 label: "other".to_string(),
17824 filter_text: Some("other".to_string()),
17825 detail: None,
17826 documentation: None,
17827 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17828 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17829 new_text: ".other".to_string(),
17830 })),
17831 ..lsp::CompletionItem::default()
17832 };
17833
17834 let item1 = item1.clone();
17835 cx.set_request_handler::<lsp::request::Completion, _, _>({
17836 let item1 = item1.clone();
17837 move |_, _, _| {
17838 let item1 = item1.clone();
17839 let item2 = item2.clone();
17840 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17841 }
17842 })
17843 .next()
17844 .await;
17845
17846 cx.condition(|editor, _| editor.context_menu_visible())
17847 .await;
17848 cx.update_editor(|editor, _, _| {
17849 let context_menu = editor.context_menu.borrow_mut();
17850 let context_menu = context_menu
17851 .as_ref()
17852 .expect("Should have the context menu deployed");
17853 match context_menu {
17854 CodeContextMenu::Completions(completions_menu) => {
17855 let completions = completions_menu.completions.borrow_mut();
17856 assert_eq!(
17857 completions
17858 .iter()
17859 .map(|completion| &completion.label.text)
17860 .collect::<Vec<_>>(),
17861 vec!["method id()", "other"]
17862 )
17863 }
17864 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17865 }
17866 });
17867
17868 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17869 let item1 = item1.clone();
17870 move |_, item_to_resolve, _| {
17871 let item1 = item1.clone();
17872 async move {
17873 if item1 == item_to_resolve {
17874 Ok(lsp::CompletionItem {
17875 label: "method id()".to_string(),
17876 filter_text: Some("id".to_string()),
17877 detail: Some("Now resolved!".to_string()),
17878 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17879 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17880 range: lsp::Range::new(
17881 lsp::Position::new(0, 22),
17882 lsp::Position::new(0, 22),
17883 ),
17884 new_text: ".id".to_string(),
17885 })),
17886 ..lsp::CompletionItem::default()
17887 })
17888 } else {
17889 Ok(item_to_resolve)
17890 }
17891 }
17892 }
17893 })
17894 .next()
17895 .await
17896 .unwrap();
17897 cx.run_until_parked();
17898
17899 cx.update_editor(|editor, window, cx| {
17900 editor.context_menu_next(&Default::default(), window, cx);
17901 });
17902
17903 cx.update_editor(|editor, _, _| {
17904 let context_menu = editor.context_menu.borrow_mut();
17905 let context_menu = context_menu
17906 .as_ref()
17907 .expect("Should have the context menu deployed");
17908 match context_menu {
17909 CodeContextMenu::Completions(completions_menu) => {
17910 let completions = completions_menu.completions.borrow_mut();
17911 assert_eq!(
17912 completions
17913 .iter()
17914 .map(|completion| &completion.label.text)
17915 .collect::<Vec<_>>(),
17916 vec!["method id() Now resolved!", "other"],
17917 "Should update first completion label, but not second as the filter text did not match."
17918 );
17919 }
17920 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17921 }
17922 });
17923}
17924
17925#[gpui::test]
17926async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17927 init_test(cx, |_| {});
17928 let mut cx = EditorLspTestContext::new_rust(
17929 lsp::ServerCapabilities {
17930 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17931 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17932 completion_provider: Some(lsp::CompletionOptions {
17933 resolve_provider: Some(true),
17934 ..Default::default()
17935 }),
17936 ..Default::default()
17937 },
17938 cx,
17939 )
17940 .await;
17941 cx.set_state(indoc! {"
17942 struct TestStruct {
17943 field: i32
17944 }
17945
17946 fn mainˇ() {
17947 let unused_var = 42;
17948 let test_struct = TestStruct { field: 42 };
17949 }
17950 "});
17951 let symbol_range = cx.lsp_range(indoc! {"
17952 struct TestStruct {
17953 field: i32
17954 }
17955
17956 «fn main»() {
17957 let unused_var = 42;
17958 let test_struct = TestStruct { field: 42 };
17959 }
17960 "});
17961 let mut hover_requests =
17962 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17963 Ok(Some(lsp::Hover {
17964 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17965 kind: lsp::MarkupKind::Markdown,
17966 value: "Function documentation".to_string(),
17967 }),
17968 range: Some(symbol_range),
17969 }))
17970 });
17971
17972 // Case 1: Test that code action menu hide hover popover
17973 cx.dispatch_action(Hover);
17974 hover_requests.next().await;
17975 cx.condition(|editor, _| editor.hover_state.visible()).await;
17976 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17977 move |_, _, _| async move {
17978 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17979 lsp::CodeAction {
17980 title: "Remove unused variable".to_string(),
17981 kind: Some(CodeActionKind::QUICKFIX),
17982 edit: Some(lsp::WorkspaceEdit {
17983 changes: Some(
17984 [(
17985 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17986 vec![lsp::TextEdit {
17987 range: lsp::Range::new(
17988 lsp::Position::new(5, 4),
17989 lsp::Position::new(5, 27),
17990 ),
17991 new_text: "".to_string(),
17992 }],
17993 )]
17994 .into_iter()
17995 .collect(),
17996 ),
17997 ..Default::default()
17998 }),
17999 ..Default::default()
18000 },
18001 )]))
18002 },
18003 );
18004 cx.update_editor(|editor, window, cx| {
18005 editor.toggle_code_actions(
18006 &ToggleCodeActions {
18007 deployed_from: None,
18008 quick_launch: false,
18009 },
18010 window,
18011 cx,
18012 );
18013 });
18014 code_action_requests.next().await;
18015 cx.run_until_parked();
18016 cx.condition(|editor, _| editor.context_menu_visible())
18017 .await;
18018 cx.update_editor(|editor, _, _| {
18019 assert!(
18020 !editor.hover_state.visible(),
18021 "Hover popover should be hidden when code action menu is shown"
18022 );
18023 // Hide code actions
18024 editor.context_menu.take();
18025 });
18026
18027 // Case 2: Test that code completions hide hover popover
18028 cx.dispatch_action(Hover);
18029 hover_requests.next().await;
18030 cx.condition(|editor, _| editor.hover_state.visible()).await;
18031 let counter = Arc::new(AtomicUsize::new(0));
18032 let mut completion_requests =
18033 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18034 let counter = counter.clone();
18035 async move {
18036 counter.fetch_add(1, atomic::Ordering::Release);
18037 Ok(Some(lsp::CompletionResponse::Array(vec![
18038 lsp::CompletionItem {
18039 label: "main".into(),
18040 kind: Some(lsp::CompletionItemKind::FUNCTION),
18041 detail: Some("() -> ()".to_string()),
18042 ..Default::default()
18043 },
18044 lsp::CompletionItem {
18045 label: "TestStruct".into(),
18046 kind: Some(lsp::CompletionItemKind::STRUCT),
18047 detail: Some("struct TestStruct".to_string()),
18048 ..Default::default()
18049 },
18050 ])))
18051 }
18052 });
18053 cx.update_editor(|editor, window, cx| {
18054 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
18055 });
18056 completion_requests.next().await;
18057 cx.condition(|editor, _| editor.context_menu_visible())
18058 .await;
18059 cx.update_editor(|editor, _, _| {
18060 assert!(
18061 !editor.hover_state.visible(),
18062 "Hover popover should be hidden when completion menu is shown"
18063 );
18064 });
18065}
18066
18067#[gpui::test]
18068async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18069 init_test(cx, |_| {});
18070
18071 let mut cx = EditorLspTestContext::new_rust(
18072 lsp::ServerCapabilities {
18073 completion_provider: Some(lsp::CompletionOptions {
18074 trigger_characters: Some(vec![".".to_string()]),
18075 resolve_provider: Some(true),
18076 ..Default::default()
18077 }),
18078 ..Default::default()
18079 },
18080 cx,
18081 )
18082 .await;
18083
18084 cx.set_state("fn main() { let a = 2ˇ; }");
18085 cx.simulate_keystroke(".");
18086
18087 let unresolved_item_1 = lsp::CompletionItem {
18088 label: "id".to_string(),
18089 filter_text: Some("id".to_string()),
18090 detail: None,
18091 documentation: None,
18092 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18093 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18094 new_text: ".id".to_string(),
18095 })),
18096 ..lsp::CompletionItem::default()
18097 };
18098 let resolved_item_1 = lsp::CompletionItem {
18099 additional_text_edits: Some(vec![lsp::TextEdit {
18100 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18101 new_text: "!!".to_string(),
18102 }]),
18103 ..unresolved_item_1.clone()
18104 };
18105 let unresolved_item_2 = lsp::CompletionItem {
18106 label: "other".to_string(),
18107 filter_text: Some("other".to_string()),
18108 detail: None,
18109 documentation: None,
18110 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18111 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18112 new_text: ".other".to_string(),
18113 })),
18114 ..lsp::CompletionItem::default()
18115 };
18116 let resolved_item_2 = lsp::CompletionItem {
18117 additional_text_edits: Some(vec![lsp::TextEdit {
18118 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18119 new_text: "??".to_string(),
18120 }]),
18121 ..unresolved_item_2.clone()
18122 };
18123
18124 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18125 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18126 cx.lsp
18127 .server
18128 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18129 let unresolved_item_1 = unresolved_item_1.clone();
18130 let resolved_item_1 = resolved_item_1.clone();
18131 let unresolved_item_2 = unresolved_item_2.clone();
18132 let resolved_item_2 = resolved_item_2.clone();
18133 let resolve_requests_1 = resolve_requests_1.clone();
18134 let resolve_requests_2 = resolve_requests_2.clone();
18135 move |unresolved_request, _| {
18136 let unresolved_item_1 = unresolved_item_1.clone();
18137 let resolved_item_1 = resolved_item_1.clone();
18138 let unresolved_item_2 = unresolved_item_2.clone();
18139 let resolved_item_2 = resolved_item_2.clone();
18140 let resolve_requests_1 = resolve_requests_1.clone();
18141 let resolve_requests_2 = resolve_requests_2.clone();
18142 async move {
18143 if unresolved_request == unresolved_item_1 {
18144 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18145 Ok(resolved_item_1.clone())
18146 } else if unresolved_request == unresolved_item_2 {
18147 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18148 Ok(resolved_item_2.clone())
18149 } else {
18150 panic!("Unexpected completion item {unresolved_request:?}")
18151 }
18152 }
18153 }
18154 })
18155 .detach();
18156
18157 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18158 let unresolved_item_1 = unresolved_item_1.clone();
18159 let unresolved_item_2 = unresolved_item_2.clone();
18160 async move {
18161 Ok(Some(lsp::CompletionResponse::Array(vec![
18162 unresolved_item_1,
18163 unresolved_item_2,
18164 ])))
18165 }
18166 })
18167 .next()
18168 .await;
18169
18170 cx.condition(|editor, _| editor.context_menu_visible())
18171 .await;
18172 cx.update_editor(|editor, _, _| {
18173 let context_menu = editor.context_menu.borrow_mut();
18174 let context_menu = context_menu
18175 .as_ref()
18176 .expect("Should have the context menu deployed");
18177 match context_menu {
18178 CodeContextMenu::Completions(completions_menu) => {
18179 let completions = completions_menu.completions.borrow_mut();
18180 assert_eq!(
18181 completions
18182 .iter()
18183 .map(|completion| &completion.label.text)
18184 .collect::<Vec<_>>(),
18185 vec!["id", "other"]
18186 )
18187 }
18188 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18189 }
18190 });
18191 cx.run_until_parked();
18192
18193 cx.update_editor(|editor, window, cx| {
18194 editor.context_menu_next(&ContextMenuNext, window, cx);
18195 });
18196 cx.run_until_parked();
18197 cx.update_editor(|editor, window, cx| {
18198 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18199 });
18200 cx.run_until_parked();
18201 cx.update_editor(|editor, window, cx| {
18202 editor.context_menu_next(&ContextMenuNext, window, cx);
18203 });
18204 cx.run_until_parked();
18205 cx.update_editor(|editor, window, cx| {
18206 editor
18207 .compose_completion(&ComposeCompletion::default(), window, cx)
18208 .expect("No task returned")
18209 })
18210 .await
18211 .expect("Completion failed");
18212 cx.run_until_parked();
18213
18214 cx.update_editor(|editor, _, cx| {
18215 assert_eq!(
18216 resolve_requests_1.load(atomic::Ordering::Acquire),
18217 1,
18218 "Should always resolve once despite multiple selections"
18219 );
18220 assert_eq!(
18221 resolve_requests_2.load(atomic::Ordering::Acquire),
18222 1,
18223 "Should always resolve once after multiple selections and applying the completion"
18224 );
18225 assert_eq!(
18226 editor.text(cx),
18227 "fn main() { let a = ??.other; }",
18228 "Should use resolved data when applying the completion"
18229 );
18230 });
18231}
18232
18233#[gpui::test]
18234async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18235 init_test(cx, |_| {});
18236
18237 let item_0 = lsp::CompletionItem {
18238 label: "abs".into(),
18239 insert_text: Some("abs".into()),
18240 data: Some(json!({ "very": "special"})),
18241 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18242 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18243 lsp::InsertReplaceEdit {
18244 new_text: "abs".to_string(),
18245 insert: lsp::Range::default(),
18246 replace: lsp::Range::default(),
18247 },
18248 )),
18249 ..lsp::CompletionItem::default()
18250 };
18251 let items = iter::once(item_0.clone())
18252 .chain((11..51).map(|i| lsp::CompletionItem {
18253 label: format!("item_{}", i),
18254 insert_text: Some(format!("item_{}", i)),
18255 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18256 ..lsp::CompletionItem::default()
18257 }))
18258 .collect::<Vec<_>>();
18259
18260 let default_commit_characters = vec!["?".to_string()];
18261 let default_data = json!({ "default": "data"});
18262 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18263 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18264 let default_edit_range = lsp::Range {
18265 start: lsp::Position {
18266 line: 0,
18267 character: 5,
18268 },
18269 end: lsp::Position {
18270 line: 0,
18271 character: 5,
18272 },
18273 };
18274
18275 let mut cx = EditorLspTestContext::new_rust(
18276 lsp::ServerCapabilities {
18277 completion_provider: Some(lsp::CompletionOptions {
18278 trigger_characters: Some(vec![".".to_string()]),
18279 resolve_provider: Some(true),
18280 ..Default::default()
18281 }),
18282 ..Default::default()
18283 },
18284 cx,
18285 )
18286 .await;
18287
18288 cx.set_state("fn main() { let a = 2ˇ; }");
18289 cx.simulate_keystroke(".");
18290
18291 let completion_data = default_data.clone();
18292 let completion_characters = default_commit_characters.clone();
18293 let completion_items = items.clone();
18294 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18295 let default_data = completion_data.clone();
18296 let default_commit_characters = completion_characters.clone();
18297 let items = completion_items.clone();
18298 async move {
18299 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18300 items,
18301 item_defaults: Some(lsp::CompletionListItemDefaults {
18302 data: Some(default_data.clone()),
18303 commit_characters: Some(default_commit_characters.clone()),
18304 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18305 default_edit_range,
18306 )),
18307 insert_text_format: Some(default_insert_text_format),
18308 insert_text_mode: Some(default_insert_text_mode),
18309 }),
18310 ..lsp::CompletionList::default()
18311 })))
18312 }
18313 })
18314 .next()
18315 .await;
18316
18317 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18318 cx.lsp
18319 .server
18320 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18321 let closure_resolved_items = resolved_items.clone();
18322 move |item_to_resolve, _| {
18323 let closure_resolved_items = closure_resolved_items.clone();
18324 async move {
18325 closure_resolved_items.lock().push(item_to_resolve.clone());
18326 Ok(item_to_resolve)
18327 }
18328 }
18329 })
18330 .detach();
18331
18332 cx.condition(|editor, _| editor.context_menu_visible())
18333 .await;
18334 cx.run_until_parked();
18335 cx.update_editor(|editor, _, _| {
18336 let menu = editor.context_menu.borrow_mut();
18337 match menu.as_ref().expect("should have the completions menu") {
18338 CodeContextMenu::Completions(completions_menu) => {
18339 assert_eq!(
18340 completions_menu
18341 .entries
18342 .borrow()
18343 .iter()
18344 .map(|mat| mat.string.clone())
18345 .collect::<Vec<String>>(),
18346 items
18347 .iter()
18348 .map(|completion| completion.label.clone())
18349 .collect::<Vec<String>>()
18350 );
18351 }
18352 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18353 }
18354 });
18355 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18356 // with 4 from the end.
18357 assert_eq!(
18358 *resolved_items.lock(),
18359 [&items[0..16], &items[items.len() - 4..items.len()]]
18360 .concat()
18361 .iter()
18362 .cloned()
18363 .map(|mut item| {
18364 if item.data.is_none() {
18365 item.data = Some(default_data.clone());
18366 }
18367 item
18368 })
18369 .collect::<Vec<lsp::CompletionItem>>(),
18370 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18371 );
18372 resolved_items.lock().clear();
18373
18374 cx.update_editor(|editor, window, cx| {
18375 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18376 });
18377 cx.run_until_parked();
18378 // Completions that have already been resolved are skipped.
18379 assert_eq!(
18380 *resolved_items.lock(),
18381 items[items.len() - 17..items.len() - 4]
18382 .iter()
18383 .cloned()
18384 .map(|mut item| {
18385 if item.data.is_none() {
18386 item.data = Some(default_data.clone());
18387 }
18388 item
18389 })
18390 .collect::<Vec<lsp::CompletionItem>>()
18391 );
18392 resolved_items.lock().clear();
18393}
18394
18395#[gpui::test]
18396async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18397 init_test(cx, |_| {});
18398
18399 let mut cx = EditorLspTestContext::new(
18400 Language::new(
18401 LanguageConfig {
18402 matcher: LanguageMatcher {
18403 path_suffixes: vec!["jsx".into()],
18404 ..Default::default()
18405 },
18406 overrides: [(
18407 "element".into(),
18408 LanguageConfigOverride {
18409 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18410 ..Default::default()
18411 },
18412 )]
18413 .into_iter()
18414 .collect(),
18415 ..Default::default()
18416 },
18417 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18418 )
18419 .with_override_query("(jsx_self_closing_element) @element")
18420 .unwrap(),
18421 lsp::ServerCapabilities {
18422 completion_provider: Some(lsp::CompletionOptions {
18423 trigger_characters: Some(vec![":".to_string()]),
18424 ..Default::default()
18425 }),
18426 ..Default::default()
18427 },
18428 cx,
18429 )
18430 .await;
18431
18432 cx.lsp
18433 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18434 Ok(Some(lsp::CompletionResponse::Array(vec![
18435 lsp::CompletionItem {
18436 label: "bg-blue".into(),
18437 ..Default::default()
18438 },
18439 lsp::CompletionItem {
18440 label: "bg-red".into(),
18441 ..Default::default()
18442 },
18443 lsp::CompletionItem {
18444 label: "bg-yellow".into(),
18445 ..Default::default()
18446 },
18447 ])))
18448 });
18449
18450 cx.set_state(r#"<p class="bgˇ" />"#);
18451
18452 // Trigger completion when typing a dash, because the dash is an extra
18453 // word character in the 'element' scope, which contains the cursor.
18454 cx.simulate_keystroke("-");
18455 cx.executor().run_until_parked();
18456 cx.update_editor(|editor, _, _| {
18457 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18458 {
18459 assert_eq!(
18460 completion_menu_entries(menu),
18461 &["bg-blue", "bg-red", "bg-yellow"]
18462 );
18463 } else {
18464 panic!("expected completion menu to be open");
18465 }
18466 });
18467
18468 cx.simulate_keystroke("l");
18469 cx.executor().run_until_parked();
18470 cx.update_editor(|editor, _, _| {
18471 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18472 {
18473 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18474 } else {
18475 panic!("expected completion menu to be open");
18476 }
18477 });
18478
18479 // When filtering completions, consider the character after the '-' to
18480 // be the start of a subword.
18481 cx.set_state(r#"<p class="yelˇ" />"#);
18482 cx.simulate_keystroke("l");
18483 cx.executor().run_until_parked();
18484 cx.update_editor(|editor, _, _| {
18485 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18486 {
18487 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18488 } else {
18489 panic!("expected completion menu to be open");
18490 }
18491 });
18492}
18493
18494fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18495 let entries = menu.entries.borrow();
18496 entries.iter().map(|mat| mat.string.clone()).collect()
18497}
18498
18499#[gpui::test]
18500async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18501 init_test(cx, |settings| {
18502 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18503 });
18504
18505 let fs = FakeFs::new(cx.executor());
18506 fs.insert_file(path!("/file.ts"), Default::default()).await;
18507
18508 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18509 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18510
18511 language_registry.add(Arc::new(Language::new(
18512 LanguageConfig {
18513 name: "TypeScript".into(),
18514 matcher: LanguageMatcher {
18515 path_suffixes: vec!["ts".to_string()],
18516 ..Default::default()
18517 },
18518 ..Default::default()
18519 },
18520 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18521 )));
18522 update_test_language_settings(cx, |settings| {
18523 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18524 });
18525
18526 let test_plugin = "test_plugin";
18527 let _ = language_registry.register_fake_lsp(
18528 "TypeScript",
18529 FakeLspAdapter {
18530 prettier_plugins: vec![test_plugin],
18531 ..Default::default()
18532 },
18533 );
18534
18535 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18536 let buffer = project
18537 .update(cx, |project, cx| {
18538 project.open_local_buffer(path!("/file.ts"), cx)
18539 })
18540 .await
18541 .unwrap();
18542
18543 let buffer_text = "one\ntwo\nthree\n";
18544 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18545 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18546 editor.update_in(cx, |editor, window, cx| {
18547 editor.set_text(buffer_text, window, cx)
18548 });
18549
18550 editor
18551 .update_in(cx, |editor, window, cx| {
18552 editor.perform_format(
18553 project.clone(),
18554 FormatTrigger::Manual,
18555 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18556 window,
18557 cx,
18558 )
18559 })
18560 .unwrap()
18561 .await;
18562 assert_eq!(
18563 editor.update(cx, |editor, cx| editor.text(cx)),
18564 buffer_text.to_string() + prettier_format_suffix,
18565 "Test prettier formatting was not applied to the original buffer text",
18566 );
18567
18568 update_test_language_settings(cx, |settings| {
18569 settings.defaults.formatter = Some(FormatterList::default())
18570 });
18571 let format = editor.update_in(cx, |editor, window, cx| {
18572 editor.perform_format(
18573 project.clone(),
18574 FormatTrigger::Manual,
18575 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18576 window,
18577 cx,
18578 )
18579 });
18580 format.await.unwrap();
18581 assert_eq!(
18582 editor.update(cx, |editor, cx| editor.text(cx)),
18583 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18584 "Autoformatting (via test prettier) was not applied to the original buffer text",
18585 );
18586}
18587
18588#[gpui::test]
18589async fn test_addition_reverts(cx: &mut TestAppContext) {
18590 init_test(cx, |_| {});
18591 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18592 let base_text = indoc! {r#"
18593 struct Row;
18594 struct Row1;
18595 struct Row2;
18596
18597 struct Row4;
18598 struct Row5;
18599 struct Row6;
18600
18601 struct Row8;
18602 struct Row9;
18603 struct Row10;"#};
18604
18605 // When addition hunks are not adjacent to carets, no hunk revert is performed
18606 assert_hunk_revert(
18607 indoc! {r#"struct Row;
18608 struct Row1;
18609 struct Row1.1;
18610 struct Row1.2;
18611 struct Row2;ˇ
18612
18613 struct Row4;
18614 struct Row5;
18615 struct Row6;
18616
18617 struct Row8;
18618 ˇstruct Row9;
18619 struct Row9.1;
18620 struct Row9.2;
18621 struct Row9.3;
18622 struct Row10;"#},
18623 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18624 indoc! {r#"struct Row;
18625 struct Row1;
18626 struct Row1.1;
18627 struct Row1.2;
18628 struct Row2;ˇ
18629
18630 struct Row4;
18631 struct Row5;
18632 struct Row6;
18633
18634 struct Row8;
18635 ˇstruct Row9;
18636 struct Row9.1;
18637 struct Row9.2;
18638 struct Row9.3;
18639 struct Row10;"#},
18640 base_text,
18641 &mut cx,
18642 );
18643 // Same for selections
18644 assert_hunk_revert(
18645 indoc! {r#"struct Row;
18646 struct Row1;
18647 struct Row2;
18648 struct Row2.1;
18649 struct Row2.2;
18650 «ˇ
18651 struct Row4;
18652 struct» Row5;
18653 «struct Row6;
18654 ˇ»
18655 struct Row9.1;
18656 struct Row9.2;
18657 struct Row9.3;
18658 struct Row8;
18659 struct Row9;
18660 struct Row10;"#},
18661 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18662 indoc! {r#"struct Row;
18663 struct Row1;
18664 struct Row2;
18665 struct Row2.1;
18666 struct Row2.2;
18667 «ˇ
18668 struct Row4;
18669 struct» Row5;
18670 «struct Row6;
18671 ˇ»
18672 struct Row9.1;
18673 struct Row9.2;
18674 struct Row9.3;
18675 struct Row8;
18676 struct Row9;
18677 struct Row10;"#},
18678 base_text,
18679 &mut cx,
18680 );
18681
18682 // When carets and selections intersect the addition hunks, those are reverted.
18683 // Adjacent carets got merged.
18684 assert_hunk_revert(
18685 indoc! {r#"struct Row;
18686 ˇ// something on the top
18687 struct Row1;
18688 struct Row2;
18689 struct Roˇw3.1;
18690 struct Row2.2;
18691 struct Row2.3;ˇ
18692
18693 struct Row4;
18694 struct ˇRow5.1;
18695 struct Row5.2;
18696 struct «Rowˇ»5.3;
18697 struct Row5;
18698 struct Row6;
18699 ˇ
18700 struct Row9.1;
18701 struct «Rowˇ»9.2;
18702 struct «ˇRow»9.3;
18703 struct Row8;
18704 struct Row9;
18705 «ˇ// something on bottom»
18706 struct Row10;"#},
18707 vec![
18708 DiffHunkStatusKind::Added,
18709 DiffHunkStatusKind::Added,
18710 DiffHunkStatusKind::Added,
18711 DiffHunkStatusKind::Added,
18712 DiffHunkStatusKind::Added,
18713 ],
18714 indoc! {r#"struct Row;
18715 ˇstruct Row1;
18716 struct Row2;
18717 ˇ
18718 struct Row4;
18719 ˇstruct Row5;
18720 struct Row6;
18721 ˇ
18722 ˇstruct Row8;
18723 struct Row9;
18724 ˇstruct Row10;"#},
18725 base_text,
18726 &mut cx,
18727 );
18728}
18729
18730#[gpui::test]
18731async fn test_modification_reverts(cx: &mut TestAppContext) {
18732 init_test(cx, |_| {});
18733 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18734 let base_text = indoc! {r#"
18735 struct Row;
18736 struct Row1;
18737 struct Row2;
18738
18739 struct Row4;
18740 struct Row5;
18741 struct Row6;
18742
18743 struct Row8;
18744 struct Row9;
18745 struct Row10;"#};
18746
18747 // Modification hunks behave the same as the addition ones.
18748 assert_hunk_revert(
18749 indoc! {r#"struct Row;
18750 struct Row1;
18751 struct Row33;
18752 ˇ
18753 struct Row4;
18754 struct Row5;
18755 struct Row6;
18756 ˇ
18757 struct Row99;
18758 struct Row9;
18759 struct Row10;"#},
18760 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18761 indoc! {r#"struct Row;
18762 struct Row1;
18763 struct Row33;
18764 ˇ
18765 struct Row4;
18766 struct Row5;
18767 struct Row6;
18768 ˇ
18769 struct Row99;
18770 struct Row9;
18771 struct Row10;"#},
18772 base_text,
18773 &mut cx,
18774 );
18775 assert_hunk_revert(
18776 indoc! {r#"struct Row;
18777 struct Row1;
18778 struct Row33;
18779 «ˇ
18780 struct Row4;
18781 struct» Row5;
18782 «struct Row6;
18783 ˇ»
18784 struct Row99;
18785 struct Row9;
18786 struct Row10;"#},
18787 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18788 indoc! {r#"struct Row;
18789 struct Row1;
18790 struct Row33;
18791 «ˇ
18792 struct Row4;
18793 struct» Row5;
18794 «struct Row6;
18795 ˇ»
18796 struct Row99;
18797 struct Row9;
18798 struct Row10;"#},
18799 base_text,
18800 &mut cx,
18801 );
18802
18803 assert_hunk_revert(
18804 indoc! {r#"ˇstruct Row1.1;
18805 struct Row1;
18806 «ˇstr»uct Row22;
18807
18808 struct ˇRow44;
18809 struct Row5;
18810 struct «Rˇ»ow66;ˇ
18811
18812 «struˇ»ct Row88;
18813 struct Row9;
18814 struct Row1011;ˇ"#},
18815 vec![
18816 DiffHunkStatusKind::Modified,
18817 DiffHunkStatusKind::Modified,
18818 DiffHunkStatusKind::Modified,
18819 DiffHunkStatusKind::Modified,
18820 DiffHunkStatusKind::Modified,
18821 DiffHunkStatusKind::Modified,
18822 ],
18823 indoc! {r#"struct Row;
18824 ˇstruct Row1;
18825 struct Row2;
18826 ˇ
18827 struct Row4;
18828 ˇstruct Row5;
18829 struct Row6;
18830 ˇ
18831 struct Row8;
18832 ˇstruct Row9;
18833 struct Row10;ˇ"#},
18834 base_text,
18835 &mut cx,
18836 );
18837}
18838
18839#[gpui::test]
18840async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18841 init_test(cx, |_| {});
18842 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18843 let base_text = indoc! {r#"
18844 one
18845
18846 two
18847 three
18848 "#};
18849
18850 cx.set_head_text(base_text);
18851 cx.set_state("\nˇ\n");
18852 cx.executor().run_until_parked();
18853 cx.update_editor(|editor, _window, cx| {
18854 editor.expand_selected_diff_hunks(cx);
18855 });
18856 cx.executor().run_until_parked();
18857 cx.update_editor(|editor, window, cx| {
18858 editor.backspace(&Default::default(), window, cx);
18859 });
18860 cx.run_until_parked();
18861 cx.assert_state_with_diff(
18862 indoc! {r#"
18863
18864 - two
18865 - threeˇ
18866 +
18867 "#}
18868 .to_string(),
18869 );
18870}
18871
18872#[gpui::test]
18873async fn test_deletion_reverts(cx: &mut TestAppContext) {
18874 init_test(cx, |_| {});
18875 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18876 let base_text = indoc! {r#"struct Row;
18877struct Row1;
18878struct Row2;
18879
18880struct Row4;
18881struct Row5;
18882struct Row6;
18883
18884struct Row8;
18885struct Row9;
18886struct Row10;"#};
18887
18888 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18889 assert_hunk_revert(
18890 indoc! {r#"struct Row;
18891 struct Row2;
18892
18893 ˇstruct Row4;
18894 struct Row5;
18895 struct Row6;
18896 ˇ
18897 struct Row8;
18898 struct Row10;"#},
18899 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18900 indoc! {r#"struct Row;
18901 struct Row2;
18902
18903 ˇstruct Row4;
18904 struct Row5;
18905 struct Row6;
18906 ˇ
18907 struct Row8;
18908 struct Row10;"#},
18909 base_text,
18910 &mut cx,
18911 );
18912 assert_hunk_revert(
18913 indoc! {r#"struct Row;
18914 struct Row2;
18915
18916 «ˇstruct Row4;
18917 struct» Row5;
18918 «struct Row6;
18919 ˇ»
18920 struct Row8;
18921 struct Row10;"#},
18922 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18923 indoc! {r#"struct Row;
18924 struct Row2;
18925
18926 «ˇstruct Row4;
18927 struct» Row5;
18928 «struct Row6;
18929 ˇ»
18930 struct Row8;
18931 struct Row10;"#},
18932 base_text,
18933 &mut cx,
18934 );
18935
18936 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18937 assert_hunk_revert(
18938 indoc! {r#"struct Row;
18939 ˇstruct Row2;
18940
18941 struct Row4;
18942 struct Row5;
18943 struct Row6;
18944
18945 struct Row8;ˇ
18946 struct Row10;"#},
18947 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18948 indoc! {r#"struct Row;
18949 struct Row1;
18950 ˇstruct Row2;
18951
18952 struct Row4;
18953 struct Row5;
18954 struct Row6;
18955
18956 struct Row8;ˇ
18957 struct Row9;
18958 struct Row10;"#},
18959 base_text,
18960 &mut cx,
18961 );
18962 assert_hunk_revert(
18963 indoc! {r#"struct Row;
18964 struct Row2«ˇ;
18965 struct Row4;
18966 struct» Row5;
18967 «struct Row6;
18968
18969 struct Row8;ˇ»
18970 struct Row10;"#},
18971 vec![
18972 DiffHunkStatusKind::Deleted,
18973 DiffHunkStatusKind::Deleted,
18974 DiffHunkStatusKind::Deleted,
18975 ],
18976 indoc! {r#"struct Row;
18977 struct Row1;
18978 struct Row2«ˇ;
18979
18980 struct Row4;
18981 struct» Row5;
18982 «struct Row6;
18983
18984 struct Row8;ˇ»
18985 struct Row9;
18986 struct Row10;"#},
18987 base_text,
18988 &mut cx,
18989 );
18990}
18991
18992#[gpui::test]
18993async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18994 init_test(cx, |_| {});
18995
18996 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18997 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18998 let base_text_3 =
18999 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19000
19001 let text_1 = edit_first_char_of_every_line(base_text_1);
19002 let text_2 = edit_first_char_of_every_line(base_text_2);
19003 let text_3 = edit_first_char_of_every_line(base_text_3);
19004
19005 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19006 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19007 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19008
19009 let multibuffer = cx.new(|cx| {
19010 let mut multibuffer = MultiBuffer::new(ReadWrite);
19011 multibuffer.push_excerpts(
19012 buffer_1.clone(),
19013 [
19014 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19015 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19016 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19017 ],
19018 cx,
19019 );
19020 multibuffer.push_excerpts(
19021 buffer_2.clone(),
19022 [
19023 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19024 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19025 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19026 ],
19027 cx,
19028 );
19029 multibuffer.push_excerpts(
19030 buffer_3.clone(),
19031 [
19032 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19033 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19034 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19035 ],
19036 cx,
19037 );
19038 multibuffer
19039 });
19040
19041 let fs = FakeFs::new(cx.executor());
19042 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19043 let (editor, cx) = cx
19044 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19045 editor.update_in(cx, |editor, _window, cx| {
19046 for (buffer, diff_base) in [
19047 (buffer_1.clone(), base_text_1),
19048 (buffer_2.clone(), base_text_2),
19049 (buffer_3.clone(), base_text_3),
19050 ] {
19051 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19052 editor
19053 .buffer
19054 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19055 }
19056 });
19057 cx.executor().run_until_parked();
19058
19059 editor.update_in(cx, |editor, window, cx| {
19060 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}");
19061 editor.select_all(&SelectAll, window, cx);
19062 editor.git_restore(&Default::default(), window, cx);
19063 });
19064 cx.executor().run_until_parked();
19065
19066 // When all ranges are selected, all buffer hunks are reverted.
19067 editor.update(cx, |editor, cx| {
19068 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");
19069 });
19070 buffer_1.update(cx, |buffer, _| {
19071 assert_eq!(buffer.text(), base_text_1);
19072 });
19073 buffer_2.update(cx, |buffer, _| {
19074 assert_eq!(buffer.text(), base_text_2);
19075 });
19076 buffer_3.update(cx, |buffer, _| {
19077 assert_eq!(buffer.text(), base_text_3);
19078 });
19079
19080 editor.update_in(cx, |editor, window, cx| {
19081 editor.undo(&Default::default(), window, cx);
19082 });
19083
19084 editor.update_in(cx, |editor, window, cx| {
19085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19086 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19087 });
19088 editor.git_restore(&Default::default(), window, cx);
19089 });
19090
19091 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19092 // but not affect buffer_2 and its related excerpts.
19093 editor.update(cx, |editor, cx| {
19094 assert_eq!(
19095 editor.text(cx),
19096 "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}"
19097 );
19098 });
19099 buffer_1.update(cx, |buffer, _| {
19100 assert_eq!(buffer.text(), base_text_1);
19101 });
19102 buffer_2.update(cx, |buffer, _| {
19103 assert_eq!(
19104 buffer.text(),
19105 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19106 );
19107 });
19108 buffer_3.update(cx, |buffer, _| {
19109 assert_eq!(
19110 buffer.text(),
19111 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19112 );
19113 });
19114
19115 fn edit_first_char_of_every_line(text: &str) -> String {
19116 text.split('\n')
19117 .map(|line| format!("X{}", &line[1..]))
19118 .collect::<Vec<_>>()
19119 .join("\n")
19120 }
19121}
19122
19123#[gpui::test]
19124async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19125 init_test(cx, |_| {});
19126
19127 let cols = 4;
19128 let rows = 10;
19129 let sample_text_1 = sample_text(rows, cols, 'a');
19130 assert_eq!(
19131 sample_text_1,
19132 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19133 );
19134 let sample_text_2 = sample_text(rows, cols, 'l');
19135 assert_eq!(
19136 sample_text_2,
19137 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19138 );
19139 let sample_text_3 = sample_text(rows, cols, 'v');
19140 assert_eq!(
19141 sample_text_3,
19142 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19143 );
19144
19145 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19146 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19147 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19148
19149 let multi_buffer = cx.new(|cx| {
19150 let mut multibuffer = MultiBuffer::new(ReadWrite);
19151 multibuffer.push_excerpts(
19152 buffer_1.clone(),
19153 [
19154 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19155 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19156 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19157 ],
19158 cx,
19159 );
19160 multibuffer.push_excerpts(
19161 buffer_2.clone(),
19162 [
19163 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19164 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19165 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19166 ],
19167 cx,
19168 );
19169 multibuffer.push_excerpts(
19170 buffer_3.clone(),
19171 [
19172 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19173 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19174 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19175 ],
19176 cx,
19177 );
19178 multibuffer
19179 });
19180
19181 let fs = FakeFs::new(cx.executor());
19182 fs.insert_tree(
19183 "/a",
19184 json!({
19185 "main.rs": sample_text_1,
19186 "other.rs": sample_text_2,
19187 "lib.rs": sample_text_3,
19188 }),
19189 )
19190 .await;
19191 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19192 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19193 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19194 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19195 Editor::new(
19196 EditorMode::full(),
19197 multi_buffer,
19198 Some(project.clone()),
19199 window,
19200 cx,
19201 )
19202 });
19203 let multibuffer_item_id = workspace
19204 .update(cx, |workspace, window, cx| {
19205 assert!(
19206 workspace.active_item(cx).is_none(),
19207 "active item should be None before the first item is added"
19208 );
19209 workspace.add_item_to_active_pane(
19210 Box::new(multi_buffer_editor.clone()),
19211 None,
19212 true,
19213 window,
19214 cx,
19215 );
19216 let active_item = workspace
19217 .active_item(cx)
19218 .expect("should have an active item after adding the multi buffer");
19219 assert_eq!(
19220 active_item.buffer_kind(cx),
19221 ItemBufferKind::Multibuffer,
19222 "A multi buffer was expected to active after adding"
19223 );
19224 active_item.item_id()
19225 })
19226 .unwrap();
19227 cx.executor().run_until_parked();
19228
19229 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19230 editor.change_selections(
19231 SelectionEffects::scroll(Autoscroll::Next),
19232 window,
19233 cx,
19234 |s| s.select_ranges(Some(1..2)),
19235 );
19236 editor.open_excerpts(&OpenExcerpts, window, cx);
19237 });
19238 cx.executor().run_until_parked();
19239 let first_item_id = workspace
19240 .update(cx, |workspace, window, cx| {
19241 let active_item = workspace
19242 .active_item(cx)
19243 .expect("should have an active item after navigating into the 1st buffer");
19244 let first_item_id = active_item.item_id();
19245 assert_ne!(
19246 first_item_id, multibuffer_item_id,
19247 "Should navigate into the 1st buffer and activate it"
19248 );
19249 assert_eq!(
19250 active_item.buffer_kind(cx),
19251 ItemBufferKind::Singleton,
19252 "New active item should be a singleton buffer"
19253 );
19254 assert_eq!(
19255 active_item
19256 .act_as::<Editor>(cx)
19257 .expect("should have navigated into an editor for the 1st buffer")
19258 .read(cx)
19259 .text(cx),
19260 sample_text_1
19261 );
19262
19263 workspace
19264 .go_back(workspace.active_pane().downgrade(), window, cx)
19265 .detach_and_log_err(cx);
19266
19267 first_item_id
19268 })
19269 .unwrap();
19270 cx.executor().run_until_parked();
19271 workspace
19272 .update(cx, |workspace, _, cx| {
19273 let active_item = workspace
19274 .active_item(cx)
19275 .expect("should have an active item after navigating back");
19276 assert_eq!(
19277 active_item.item_id(),
19278 multibuffer_item_id,
19279 "Should navigate back to the multi buffer"
19280 );
19281 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19282 })
19283 .unwrap();
19284
19285 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19286 editor.change_selections(
19287 SelectionEffects::scroll(Autoscroll::Next),
19288 window,
19289 cx,
19290 |s| s.select_ranges(Some(39..40)),
19291 );
19292 editor.open_excerpts(&OpenExcerpts, window, cx);
19293 });
19294 cx.executor().run_until_parked();
19295 let second_item_id = workspace
19296 .update(cx, |workspace, window, cx| {
19297 let active_item = workspace
19298 .active_item(cx)
19299 .expect("should have an active item after navigating into the 2nd buffer");
19300 let second_item_id = active_item.item_id();
19301 assert_ne!(
19302 second_item_id, multibuffer_item_id,
19303 "Should navigate away from the multibuffer"
19304 );
19305 assert_ne!(
19306 second_item_id, first_item_id,
19307 "Should navigate into the 2nd buffer and activate it"
19308 );
19309 assert_eq!(
19310 active_item.buffer_kind(cx),
19311 ItemBufferKind::Singleton,
19312 "New active item should be a singleton buffer"
19313 );
19314 assert_eq!(
19315 active_item
19316 .act_as::<Editor>(cx)
19317 .expect("should have navigated into an editor")
19318 .read(cx)
19319 .text(cx),
19320 sample_text_2
19321 );
19322
19323 workspace
19324 .go_back(workspace.active_pane().downgrade(), window, cx)
19325 .detach_and_log_err(cx);
19326
19327 second_item_id
19328 })
19329 .unwrap();
19330 cx.executor().run_until_parked();
19331 workspace
19332 .update(cx, |workspace, _, cx| {
19333 let active_item = workspace
19334 .active_item(cx)
19335 .expect("should have an active item after navigating back from the 2nd buffer");
19336 assert_eq!(
19337 active_item.item_id(),
19338 multibuffer_item_id,
19339 "Should navigate back from the 2nd buffer to the multi buffer"
19340 );
19341 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19342 })
19343 .unwrap();
19344
19345 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19346 editor.change_selections(
19347 SelectionEffects::scroll(Autoscroll::Next),
19348 window,
19349 cx,
19350 |s| s.select_ranges(Some(70..70)),
19351 );
19352 editor.open_excerpts(&OpenExcerpts, window, cx);
19353 });
19354 cx.executor().run_until_parked();
19355 workspace
19356 .update(cx, |workspace, window, cx| {
19357 let active_item = workspace
19358 .active_item(cx)
19359 .expect("should have an active item after navigating into the 3rd buffer");
19360 let third_item_id = active_item.item_id();
19361 assert_ne!(
19362 third_item_id, multibuffer_item_id,
19363 "Should navigate into the 3rd buffer and activate it"
19364 );
19365 assert_ne!(third_item_id, first_item_id);
19366 assert_ne!(third_item_id, second_item_id);
19367 assert_eq!(
19368 active_item.buffer_kind(cx),
19369 ItemBufferKind::Singleton,
19370 "New active item should be a singleton buffer"
19371 );
19372 assert_eq!(
19373 active_item
19374 .act_as::<Editor>(cx)
19375 .expect("should have navigated into an editor")
19376 .read(cx)
19377 .text(cx),
19378 sample_text_3
19379 );
19380
19381 workspace
19382 .go_back(workspace.active_pane().downgrade(), window, cx)
19383 .detach_and_log_err(cx);
19384 })
19385 .unwrap();
19386 cx.executor().run_until_parked();
19387 workspace
19388 .update(cx, |workspace, _, cx| {
19389 let active_item = workspace
19390 .active_item(cx)
19391 .expect("should have an active item after navigating back from the 3rd buffer");
19392 assert_eq!(
19393 active_item.item_id(),
19394 multibuffer_item_id,
19395 "Should navigate back from the 3rd buffer to the multi buffer"
19396 );
19397 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19398 })
19399 .unwrap();
19400}
19401
19402#[gpui::test]
19403async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19404 init_test(cx, |_| {});
19405
19406 let mut cx = EditorTestContext::new(cx).await;
19407
19408 let diff_base = r#"
19409 use some::mod;
19410
19411 const A: u32 = 42;
19412
19413 fn main() {
19414 println!("hello");
19415
19416 println!("world");
19417 }
19418 "#
19419 .unindent();
19420
19421 cx.set_state(
19422 &r#"
19423 use some::modified;
19424
19425 ˇ
19426 fn main() {
19427 println!("hello there");
19428
19429 println!("around the");
19430 println!("world");
19431 }
19432 "#
19433 .unindent(),
19434 );
19435
19436 cx.set_head_text(&diff_base);
19437 executor.run_until_parked();
19438
19439 cx.update_editor(|editor, window, cx| {
19440 editor.go_to_next_hunk(&GoToHunk, window, cx);
19441 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19442 });
19443 executor.run_until_parked();
19444 cx.assert_state_with_diff(
19445 r#"
19446 use some::modified;
19447
19448
19449 fn main() {
19450 - println!("hello");
19451 + ˇ println!("hello there");
19452
19453 println!("around the");
19454 println!("world");
19455 }
19456 "#
19457 .unindent(),
19458 );
19459
19460 cx.update_editor(|editor, window, cx| {
19461 for _ in 0..2 {
19462 editor.go_to_next_hunk(&GoToHunk, window, cx);
19463 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19464 }
19465 });
19466 executor.run_until_parked();
19467 cx.assert_state_with_diff(
19468 r#"
19469 - use some::mod;
19470 + ˇuse some::modified;
19471
19472
19473 fn main() {
19474 - println!("hello");
19475 + println!("hello there");
19476
19477 + println!("around the");
19478 println!("world");
19479 }
19480 "#
19481 .unindent(),
19482 );
19483
19484 cx.update_editor(|editor, window, cx| {
19485 editor.go_to_next_hunk(&GoToHunk, window, cx);
19486 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19487 });
19488 executor.run_until_parked();
19489 cx.assert_state_with_diff(
19490 r#"
19491 - use some::mod;
19492 + use some::modified;
19493
19494 - const A: u32 = 42;
19495 ˇ
19496 fn main() {
19497 - println!("hello");
19498 + println!("hello there");
19499
19500 + println!("around the");
19501 println!("world");
19502 }
19503 "#
19504 .unindent(),
19505 );
19506
19507 cx.update_editor(|editor, window, cx| {
19508 editor.cancel(&Cancel, window, cx);
19509 });
19510
19511 cx.assert_state_with_diff(
19512 r#"
19513 use some::modified;
19514
19515 ˇ
19516 fn main() {
19517 println!("hello there");
19518
19519 println!("around the");
19520 println!("world");
19521 }
19522 "#
19523 .unindent(),
19524 );
19525}
19526
19527#[gpui::test]
19528async fn test_diff_base_change_with_expanded_diff_hunks(
19529 executor: BackgroundExecutor,
19530 cx: &mut TestAppContext,
19531) {
19532 init_test(cx, |_| {});
19533
19534 let mut cx = EditorTestContext::new(cx).await;
19535
19536 let diff_base = r#"
19537 use some::mod1;
19538 use some::mod2;
19539
19540 const A: u32 = 42;
19541 const B: u32 = 42;
19542 const C: u32 = 42;
19543
19544 fn main() {
19545 println!("hello");
19546
19547 println!("world");
19548 }
19549 "#
19550 .unindent();
19551
19552 cx.set_state(
19553 &r#"
19554 use some::mod2;
19555
19556 const A: u32 = 42;
19557 const C: u32 = 42;
19558
19559 fn main(ˇ) {
19560 //println!("hello");
19561
19562 println!("world");
19563 //
19564 //
19565 }
19566 "#
19567 .unindent(),
19568 );
19569
19570 cx.set_head_text(&diff_base);
19571 executor.run_until_parked();
19572
19573 cx.update_editor(|editor, window, cx| {
19574 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19575 });
19576 executor.run_until_parked();
19577 cx.assert_state_with_diff(
19578 r#"
19579 - use some::mod1;
19580 use some::mod2;
19581
19582 const A: u32 = 42;
19583 - const B: u32 = 42;
19584 const C: u32 = 42;
19585
19586 fn main(ˇ) {
19587 - println!("hello");
19588 + //println!("hello");
19589
19590 println!("world");
19591 + //
19592 + //
19593 }
19594 "#
19595 .unindent(),
19596 );
19597
19598 cx.set_head_text("new diff base!");
19599 executor.run_until_parked();
19600 cx.assert_state_with_diff(
19601 r#"
19602 - new diff base!
19603 + use some::mod2;
19604 +
19605 + const A: u32 = 42;
19606 + const C: u32 = 42;
19607 +
19608 + fn main(ˇ) {
19609 + //println!("hello");
19610 +
19611 + println!("world");
19612 + //
19613 + //
19614 + }
19615 "#
19616 .unindent(),
19617 );
19618}
19619
19620#[gpui::test]
19621async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19622 init_test(cx, |_| {});
19623
19624 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19625 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19626 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19627 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19628 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19629 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19630
19631 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19632 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19633 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19634
19635 let multi_buffer = cx.new(|cx| {
19636 let mut multibuffer = MultiBuffer::new(ReadWrite);
19637 multibuffer.push_excerpts(
19638 buffer_1.clone(),
19639 [
19640 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19641 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19642 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19643 ],
19644 cx,
19645 );
19646 multibuffer.push_excerpts(
19647 buffer_2.clone(),
19648 [
19649 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19650 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19651 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19652 ],
19653 cx,
19654 );
19655 multibuffer.push_excerpts(
19656 buffer_3.clone(),
19657 [
19658 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19659 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19660 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19661 ],
19662 cx,
19663 );
19664 multibuffer
19665 });
19666
19667 let editor =
19668 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19669 editor
19670 .update(cx, |editor, _window, cx| {
19671 for (buffer, diff_base) in [
19672 (buffer_1.clone(), file_1_old),
19673 (buffer_2.clone(), file_2_old),
19674 (buffer_3.clone(), file_3_old),
19675 ] {
19676 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19677 editor
19678 .buffer
19679 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19680 }
19681 })
19682 .unwrap();
19683
19684 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19685 cx.run_until_parked();
19686
19687 cx.assert_editor_state(
19688 &"
19689 ˇaaa
19690 ccc
19691 ddd
19692
19693 ggg
19694 hhh
19695
19696
19697 lll
19698 mmm
19699 NNN
19700
19701 qqq
19702 rrr
19703
19704 uuu
19705 111
19706 222
19707 333
19708
19709 666
19710 777
19711
19712 000
19713 !!!"
19714 .unindent(),
19715 );
19716
19717 cx.update_editor(|editor, window, cx| {
19718 editor.select_all(&SelectAll, window, cx);
19719 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19720 });
19721 cx.executor().run_until_parked();
19722
19723 cx.assert_state_with_diff(
19724 "
19725 «aaa
19726 - bbb
19727 ccc
19728 ddd
19729
19730 ggg
19731 hhh
19732
19733
19734 lll
19735 mmm
19736 - nnn
19737 + NNN
19738
19739 qqq
19740 rrr
19741
19742 uuu
19743 111
19744 222
19745 333
19746
19747 + 666
19748 777
19749
19750 000
19751 !!!ˇ»"
19752 .unindent(),
19753 );
19754}
19755
19756#[gpui::test]
19757async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19758 init_test(cx, |_| {});
19759
19760 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19761 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19762
19763 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19764 let multi_buffer = cx.new(|cx| {
19765 let mut multibuffer = MultiBuffer::new(ReadWrite);
19766 multibuffer.push_excerpts(
19767 buffer.clone(),
19768 [
19769 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19770 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19771 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19772 ],
19773 cx,
19774 );
19775 multibuffer
19776 });
19777
19778 let editor =
19779 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19780 editor
19781 .update(cx, |editor, _window, cx| {
19782 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19783 editor
19784 .buffer
19785 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19786 })
19787 .unwrap();
19788
19789 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19790 cx.run_until_parked();
19791
19792 cx.update_editor(|editor, window, cx| {
19793 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19794 });
19795 cx.executor().run_until_parked();
19796
19797 // When the start of a hunk coincides with the start of its excerpt,
19798 // the hunk is expanded. When the start of a hunk is earlier than
19799 // the start of its excerpt, the hunk is not expanded.
19800 cx.assert_state_with_diff(
19801 "
19802 ˇaaa
19803 - bbb
19804 + BBB
19805
19806 - ddd
19807 - eee
19808 + DDD
19809 + EEE
19810 fff
19811
19812 iii
19813 "
19814 .unindent(),
19815 );
19816}
19817
19818#[gpui::test]
19819async fn test_edits_around_expanded_insertion_hunks(
19820 executor: BackgroundExecutor,
19821 cx: &mut TestAppContext,
19822) {
19823 init_test(cx, |_| {});
19824
19825 let mut cx = EditorTestContext::new(cx).await;
19826
19827 let diff_base = r#"
19828 use some::mod1;
19829 use some::mod2;
19830
19831 const A: u32 = 42;
19832
19833 fn main() {
19834 println!("hello");
19835
19836 println!("world");
19837 }
19838 "#
19839 .unindent();
19840 executor.run_until_parked();
19841 cx.set_state(
19842 &r#"
19843 use some::mod1;
19844 use some::mod2;
19845
19846 const A: u32 = 42;
19847 const B: u32 = 42;
19848 const C: u32 = 42;
19849 ˇ
19850
19851 fn main() {
19852 println!("hello");
19853
19854 println!("world");
19855 }
19856 "#
19857 .unindent(),
19858 );
19859
19860 cx.set_head_text(&diff_base);
19861 executor.run_until_parked();
19862
19863 cx.update_editor(|editor, window, cx| {
19864 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19865 });
19866 executor.run_until_parked();
19867
19868 cx.assert_state_with_diff(
19869 r#"
19870 use some::mod1;
19871 use some::mod2;
19872
19873 const A: u32 = 42;
19874 + const B: u32 = 42;
19875 + const C: u32 = 42;
19876 + ˇ
19877
19878 fn main() {
19879 println!("hello");
19880
19881 println!("world");
19882 }
19883 "#
19884 .unindent(),
19885 );
19886
19887 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19888 executor.run_until_parked();
19889
19890 cx.assert_state_with_diff(
19891 r#"
19892 use some::mod1;
19893 use some::mod2;
19894
19895 const A: u32 = 42;
19896 + const B: u32 = 42;
19897 + const C: u32 = 42;
19898 + const D: u32 = 42;
19899 + ˇ
19900
19901 fn main() {
19902 println!("hello");
19903
19904 println!("world");
19905 }
19906 "#
19907 .unindent(),
19908 );
19909
19910 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19911 executor.run_until_parked();
19912
19913 cx.assert_state_with_diff(
19914 r#"
19915 use some::mod1;
19916 use some::mod2;
19917
19918 const A: u32 = 42;
19919 + const B: u32 = 42;
19920 + const C: u32 = 42;
19921 + const D: u32 = 42;
19922 + const E: u32 = 42;
19923 + ˇ
19924
19925 fn main() {
19926 println!("hello");
19927
19928 println!("world");
19929 }
19930 "#
19931 .unindent(),
19932 );
19933
19934 cx.update_editor(|editor, window, cx| {
19935 editor.delete_line(&DeleteLine, window, cx);
19936 });
19937 executor.run_until_parked();
19938
19939 cx.assert_state_with_diff(
19940 r#"
19941 use some::mod1;
19942 use some::mod2;
19943
19944 const A: u32 = 42;
19945 + const B: u32 = 42;
19946 + const C: u32 = 42;
19947 + const D: u32 = 42;
19948 + const E: u32 = 42;
19949 ˇ
19950 fn main() {
19951 println!("hello");
19952
19953 println!("world");
19954 }
19955 "#
19956 .unindent(),
19957 );
19958
19959 cx.update_editor(|editor, window, cx| {
19960 editor.move_up(&MoveUp, window, cx);
19961 editor.delete_line(&DeleteLine, window, cx);
19962 editor.move_up(&MoveUp, window, cx);
19963 editor.delete_line(&DeleteLine, window, cx);
19964 editor.move_up(&MoveUp, window, cx);
19965 editor.delete_line(&DeleteLine, window, cx);
19966 });
19967 executor.run_until_parked();
19968 cx.assert_state_with_diff(
19969 r#"
19970 use some::mod1;
19971 use some::mod2;
19972
19973 const A: u32 = 42;
19974 + const B: u32 = 42;
19975 ˇ
19976 fn main() {
19977 println!("hello");
19978
19979 println!("world");
19980 }
19981 "#
19982 .unindent(),
19983 );
19984
19985 cx.update_editor(|editor, window, cx| {
19986 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19987 editor.delete_line(&DeleteLine, window, cx);
19988 });
19989 executor.run_until_parked();
19990 cx.assert_state_with_diff(
19991 r#"
19992 ˇ
19993 fn main() {
19994 println!("hello");
19995
19996 println!("world");
19997 }
19998 "#
19999 .unindent(),
20000 );
20001}
20002
20003#[gpui::test]
20004async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20005 init_test(cx, |_| {});
20006
20007 let mut cx = EditorTestContext::new(cx).await;
20008 cx.set_head_text(indoc! { "
20009 one
20010 two
20011 three
20012 four
20013 five
20014 "
20015 });
20016 cx.set_state(indoc! { "
20017 one
20018 ˇthree
20019 five
20020 "});
20021 cx.run_until_parked();
20022 cx.update_editor(|editor, window, cx| {
20023 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20024 });
20025 cx.assert_state_with_diff(
20026 indoc! { "
20027 one
20028 - two
20029 ˇthree
20030 - four
20031 five
20032 "}
20033 .to_string(),
20034 );
20035 cx.update_editor(|editor, window, cx| {
20036 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20037 });
20038
20039 cx.assert_state_with_diff(
20040 indoc! { "
20041 one
20042 ˇthree
20043 five
20044 "}
20045 .to_string(),
20046 );
20047
20048 cx.set_state(indoc! { "
20049 one
20050 ˇTWO
20051 three
20052 four
20053 five
20054 "});
20055 cx.run_until_parked();
20056 cx.update_editor(|editor, window, cx| {
20057 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20058 });
20059
20060 cx.assert_state_with_diff(
20061 indoc! { "
20062 one
20063 - two
20064 + ˇTWO
20065 three
20066 four
20067 five
20068 "}
20069 .to_string(),
20070 );
20071 cx.update_editor(|editor, window, cx| {
20072 editor.move_up(&Default::default(), window, cx);
20073 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20074 });
20075 cx.assert_state_with_diff(
20076 indoc! { "
20077 one
20078 ˇTWO
20079 three
20080 four
20081 five
20082 "}
20083 .to_string(),
20084 );
20085}
20086
20087#[gpui::test]
20088async fn test_edits_around_expanded_deletion_hunks(
20089 executor: BackgroundExecutor,
20090 cx: &mut TestAppContext,
20091) {
20092 init_test(cx, |_| {});
20093
20094 let mut cx = EditorTestContext::new(cx).await;
20095
20096 let diff_base = r#"
20097 use some::mod1;
20098 use some::mod2;
20099
20100 const A: u32 = 42;
20101 const B: u32 = 42;
20102 const C: u32 = 42;
20103
20104
20105 fn main() {
20106 println!("hello");
20107
20108 println!("world");
20109 }
20110 "#
20111 .unindent();
20112 executor.run_until_parked();
20113 cx.set_state(
20114 &r#"
20115 use some::mod1;
20116 use some::mod2;
20117
20118 ˇconst B: u32 = 42;
20119 const C: u32 = 42;
20120
20121
20122 fn main() {
20123 println!("hello");
20124
20125 println!("world");
20126 }
20127 "#
20128 .unindent(),
20129 );
20130
20131 cx.set_head_text(&diff_base);
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 use some::mod1;
20142 use some::mod2;
20143
20144 - const A: u32 = 42;
20145 ˇconst B: u32 = 42;
20146 const C: u32 = 42;
20147
20148
20149 fn main() {
20150 println!("hello");
20151
20152 println!("world");
20153 }
20154 "#
20155 .unindent(),
20156 );
20157
20158 cx.update_editor(|editor, window, cx| {
20159 editor.delete_line(&DeleteLine, window, cx);
20160 });
20161 executor.run_until_parked();
20162 cx.assert_state_with_diff(
20163 r#"
20164 use some::mod1;
20165 use some::mod2;
20166
20167 - const A: u32 = 42;
20168 - const B: u32 = 42;
20169 ˇconst C: u32 = 42;
20170
20171
20172 fn main() {
20173 println!("hello");
20174
20175 println!("world");
20176 }
20177 "#
20178 .unindent(),
20179 );
20180
20181 cx.update_editor(|editor, window, cx| {
20182 editor.delete_line(&DeleteLine, window, cx);
20183 });
20184 executor.run_until_parked();
20185 cx.assert_state_with_diff(
20186 r#"
20187 use some::mod1;
20188 use some::mod2;
20189
20190 - const A: u32 = 42;
20191 - const B: u32 = 42;
20192 - const C: u32 = 42;
20193 ˇ
20194
20195 fn main() {
20196 println!("hello");
20197
20198 println!("world");
20199 }
20200 "#
20201 .unindent(),
20202 );
20203
20204 cx.update_editor(|editor, window, cx| {
20205 editor.handle_input("replacement", window, cx);
20206 });
20207 executor.run_until_parked();
20208 cx.assert_state_with_diff(
20209 r#"
20210 use some::mod1;
20211 use some::mod2;
20212
20213 - const A: u32 = 42;
20214 - const B: u32 = 42;
20215 - const C: u32 = 42;
20216 -
20217 + replacementˇ
20218
20219 fn main() {
20220 println!("hello");
20221
20222 println!("world");
20223 }
20224 "#
20225 .unindent(),
20226 );
20227}
20228
20229#[gpui::test]
20230async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20231 init_test(cx, |_| {});
20232
20233 let mut cx = EditorTestContext::new(cx).await;
20234
20235 let base_text = r#"
20236 one
20237 two
20238 three
20239 four
20240 five
20241 "#
20242 .unindent();
20243 executor.run_until_parked();
20244 cx.set_state(
20245 &r#"
20246 one
20247 two
20248 fˇour
20249 five
20250 "#
20251 .unindent(),
20252 );
20253
20254 cx.set_head_text(&base_text);
20255 executor.run_until_parked();
20256
20257 cx.update_editor(|editor, window, cx| {
20258 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20259 });
20260 executor.run_until_parked();
20261
20262 cx.assert_state_with_diff(
20263 r#"
20264 one
20265 two
20266 - three
20267 fˇour
20268 five
20269 "#
20270 .unindent(),
20271 );
20272
20273 cx.update_editor(|editor, window, cx| {
20274 editor.backspace(&Backspace, window, cx);
20275 editor.backspace(&Backspace, window, cx);
20276 });
20277 executor.run_until_parked();
20278 cx.assert_state_with_diff(
20279 r#"
20280 one
20281 two
20282 - threeˇ
20283 - four
20284 + our
20285 five
20286 "#
20287 .unindent(),
20288 );
20289}
20290
20291#[gpui::test]
20292async fn test_edit_after_expanded_modification_hunk(
20293 executor: BackgroundExecutor,
20294 cx: &mut TestAppContext,
20295) {
20296 init_test(cx, |_| {});
20297
20298 let mut cx = EditorTestContext::new(cx).await;
20299
20300 let diff_base = r#"
20301 use some::mod1;
20302 use some::mod2;
20303
20304 const A: u32 = 42;
20305 const B: u32 = 42;
20306 const C: u32 = 42;
20307 const D: u32 = 42;
20308
20309
20310 fn main() {
20311 println!("hello");
20312
20313 println!("world");
20314 }"#
20315 .unindent();
20316
20317 cx.set_state(
20318 &r#"
20319 use some::mod1;
20320 use some::mod2;
20321
20322 const A: u32 = 42;
20323 const B: u32 = 42;
20324 const C: u32 = 43ˇ
20325 const D: u32 = 42;
20326
20327
20328 fn main() {
20329 println!("hello");
20330
20331 println!("world");
20332 }"#
20333 .unindent(),
20334 );
20335
20336 cx.set_head_text(&diff_base);
20337 executor.run_until_parked();
20338 cx.update_editor(|editor, window, cx| {
20339 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20340 });
20341 executor.run_until_parked();
20342
20343 cx.assert_state_with_diff(
20344 r#"
20345 use some::mod1;
20346 use some::mod2;
20347
20348 const A: u32 = 42;
20349 const B: u32 = 42;
20350 - const C: u32 = 42;
20351 + const C: u32 = 43ˇ
20352 const D: u32 = 42;
20353
20354
20355 fn main() {
20356 println!("hello");
20357
20358 println!("world");
20359 }"#
20360 .unindent(),
20361 );
20362
20363 cx.update_editor(|editor, window, cx| {
20364 editor.handle_input("\nnew_line\n", window, cx);
20365 });
20366 executor.run_until_parked();
20367
20368 cx.assert_state_with_diff(
20369 r#"
20370 use some::mod1;
20371 use some::mod2;
20372
20373 const A: u32 = 42;
20374 const B: u32 = 42;
20375 - const C: u32 = 42;
20376 + const C: u32 = 43
20377 + new_line
20378 + ˇ
20379 const D: u32 = 42;
20380
20381
20382 fn main() {
20383 println!("hello");
20384
20385 println!("world");
20386 }"#
20387 .unindent(),
20388 );
20389}
20390
20391#[gpui::test]
20392async fn test_stage_and_unstage_added_file_hunk(
20393 executor: BackgroundExecutor,
20394 cx: &mut TestAppContext,
20395) {
20396 init_test(cx, |_| {});
20397
20398 let mut cx = EditorTestContext::new(cx).await;
20399 cx.update_editor(|editor, _, cx| {
20400 editor.set_expand_all_diff_hunks(cx);
20401 });
20402
20403 let working_copy = r#"
20404 ˇfn main() {
20405 println!("hello, world!");
20406 }
20407 "#
20408 .unindent();
20409
20410 cx.set_state(&working_copy);
20411 executor.run_until_parked();
20412
20413 cx.assert_state_with_diff(
20414 r#"
20415 + ˇfn main() {
20416 + println!("hello, world!");
20417 + }
20418 "#
20419 .unindent(),
20420 );
20421 cx.assert_index_text(None);
20422
20423 cx.update_editor(|editor, window, cx| {
20424 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20425 });
20426 executor.run_until_parked();
20427 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20428 cx.assert_state_with_diff(
20429 r#"
20430 + ˇfn main() {
20431 + println!("hello, world!");
20432 + }
20433 "#
20434 .unindent(),
20435 );
20436
20437 cx.update_editor(|editor, window, cx| {
20438 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20439 });
20440 executor.run_until_parked();
20441 cx.assert_index_text(None);
20442}
20443
20444async fn setup_indent_guides_editor(
20445 text: &str,
20446 cx: &mut TestAppContext,
20447) -> (BufferId, EditorTestContext) {
20448 init_test(cx, |_| {});
20449
20450 let mut cx = EditorTestContext::new(cx).await;
20451
20452 let buffer_id = cx.update_editor(|editor, window, cx| {
20453 editor.set_text(text, window, cx);
20454 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20455
20456 buffer_ids[0]
20457 });
20458
20459 (buffer_id, cx)
20460}
20461
20462fn assert_indent_guides(
20463 range: Range<u32>,
20464 expected: Vec<IndentGuide>,
20465 active_indices: Option<Vec<usize>>,
20466 cx: &mut EditorTestContext,
20467) {
20468 let indent_guides = cx.update_editor(|editor, window, cx| {
20469 let snapshot = editor.snapshot(window, cx).display_snapshot;
20470 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20471 editor,
20472 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20473 true,
20474 &snapshot,
20475 cx,
20476 );
20477
20478 indent_guides.sort_by(|a, b| {
20479 a.depth.cmp(&b.depth).then(
20480 a.start_row
20481 .cmp(&b.start_row)
20482 .then(a.end_row.cmp(&b.end_row)),
20483 )
20484 });
20485 indent_guides
20486 });
20487
20488 if let Some(expected) = active_indices {
20489 let active_indices = cx.update_editor(|editor, window, cx| {
20490 let snapshot = editor.snapshot(window, cx).display_snapshot;
20491 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20492 });
20493
20494 assert_eq!(
20495 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20496 expected,
20497 "Active indent guide indices do not match"
20498 );
20499 }
20500
20501 assert_eq!(indent_guides, expected, "Indent guides do not match");
20502}
20503
20504fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20505 IndentGuide {
20506 buffer_id,
20507 start_row: MultiBufferRow(start_row),
20508 end_row: MultiBufferRow(end_row),
20509 depth,
20510 tab_size: 4,
20511 settings: IndentGuideSettings {
20512 enabled: true,
20513 line_width: 1,
20514 active_line_width: 1,
20515 coloring: IndentGuideColoring::default(),
20516 background_coloring: IndentGuideBackgroundColoring::default(),
20517 },
20518 }
20519}
20520
20521#[gpui::test]
20522async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20523 let (buffer_id, mut cx) = setup_indent_guides_editor(
20524 &"
20525 fn main() {
20526 let a = 1;
20527 }"
20528 .unindent(),
20529 cx,
20530 )
20531 .await;
20532
20533 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20534}
20535
20536#[gpui::test]
20537async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20538 let (buffer_id, mut cx) = setup_indent_guides_editor(
20539 &"
20540 fn main() {
20541 let a = 1;
20542 let b = 2;
20543 }"
20544 .unindent(),
20545 cx,
20546 )
20547 .await;
20548
20549 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20550}
20551
20552#[gpui::test]
20553async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20554 let (buffer_id, mut cx) = setup_indent_guides_editor(
20555 &"
20556 fn main() {
20557 let a = 1;
20558 if a == 3 {
20559 let b = 2;
20560 } else {
20561 let c = 3;
20562 }
20563 }"
20564 .unindent(),
20565 cx,
20566 )
20567 .await;
20568
20569 assert_indent_guides(
20570 0..8,
20571 vec![
20572 indent_guide(buffer_id, 1, 6, 0),
20573 indent_guide(buffer_id, 3, 3, 1),
20574 indent_guide(buffer_id, 5, 5, 1),
20575 ],
20576 None,
20577 &mut cx,
20578 );
20579}
20580
20581#[gpui::test]
20582async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20583 let (buffer_id, mut cx) = setup_indent_guides_editor(
20584 &"
20585 fn main() {
20586 let a = 1;
20587 let b = 2;
20588 let c = 3;
20589 }"
20590 .unindent(),
20591 cx,
20592 )
20593 .await;
20594
20595 assert_indent_guides(
20596 0..5,
20597 vec![
20598 indent_guide(buffer_id, 1, 3, 0),
20599 indent_guide(buffer_id, 2, 2, 1),
20600 ],
20601 None,
20602 &mut cx,
20603 );
20604}
20605
20606#[gpui::test]
20607async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20608 let (buffer_id, mut cx) = setup_indent_guides_editor(
20609 &"
20610 fn main() {
20611 let a = 1;
20612
20613 let c = 3;
20614 }"
20615 .unindent(),
20616 cx,
20617 )
20618 .await;
20619
20620 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20621}
20622
20623#[gpui::test]
20624async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20625 let (buffer_id, mut cx) = setup_indent_guides_editor(
20626 &"
20627 fn main() {
20628 let a = 1;
20629
20630 let c = 3;
20631
20632 if a == 3 {
20633 let b = 2;
20634 } else {
20635 let c = 3;
20636 }
20637 }"
20638 .unindent(),
20639 cx,
20640 )
20641 .await;
20642
20643 assert_indent_guides(
20644 0..11,
20645 vec![
20646 indent_guide(buffer_id, 1, 9, 0),
20647 indent_guide(buffer_id, 6, 6, 1),
20648 indent_guide(buffer_id, 8, 8, 1),
20649 ],
20650 None,
20651 &mut cx,
20652 );
20653}
20654
20655#[gpui::test]
20656async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20657 let (buffer_id, mut cx) = setup_indent_guides_editor(
20658 &"
20659 fn main() {
20660 let a = 1;
20661
20662 let c = 3;
20663
20664 if a == 3 {
20665 let b = 2;
20666 } else {
20667 let c = 3;
20668 }
20669 }"
20670 .unindent(),
20671 cx,
20672 )
20673 .await;
20674
20675 assert_indent_guides(
20676 1..11,
20677 vec![
20678 indent_guide(buffer_id, 1, 9, 0),
20679 indent_guide(buffer_id, 6, 6, 1),
20680 indent_guide(buffer_id, 8, 8, 1),
20681 ],
20682 None,
20683 &mut cx,
20684 );
20685}
20686
20687#[gpui::test]
20688async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20689 let (buffer_id, mut cx) = setup_indent_guides_editor(
20690 &"
20691 fn main() {
20692 let a = 1;
20693
20694 let c = 3;
20695
20696 if a == 3 {
20697 let b = 2;
20698 } else {
20699 let c = 3;
20700 }
20701 }"
20702 .unindent(),
20703 cx,
20704 )
20705 .await;
20706
20707 assert_indent_guides(
20708 1..10,
20709 vec![
20710 indent_guide(buffer_id, 1, 9, 0),
20711 indent_guide(buffer_id, 6, 6, 1),
20712 indent_guide(buffer_id, 8, 8, 1),
20713 ],
20714 None,
20715 &mut cx,
20716 );
20717}
20718
20719#[gpui::test]
20720async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20721 let (buffer_id, mut cx) = setup_indent_guides_editor(
20722 &"
20723 fn main() {
20724 if a {
20725 b(
20726 c,
20727 d,
20728 )
20729 } else {
20730 e(
20731 f
20732 )
20733 }
20734 }"
20735 .unindent(),
20736 cx,
20737 )
20738 .await;
20739
20740 assert_indent_guides(
20741 0..11,
20742 vec![
20743 indent_guide(buffer_id, 1, 10, 0),
20744 indent_guide(buffer_id, 2, 5, 1),
20745 indent_guide(buffer_id, 7, 9, 1),
20746 indent_guide(buffer_id, 3, 4, 2),
20747 indent_guide(buffer_id, 8, 8, 2),
20748 ],
20749 None,
20750 &mut cx,
20751 );
20752
20753 cx.update_editor(|editor, window, cx| {
20754 editor.fold_at(MultiBufferRow(2), window, cx);
20755 assert_eq!(
20756 editor.display_text(cx),
20757 "
20758 fn main() {
20759 if a {
20760 b(⋯
20761 )
20762 } else {
20763 e(
20764 f
20765 )
20766 }
20767 }"
20768 .unindent()
20769 );
20770 });
20771
20772 assert_indent_guides(
20773 0..11,
20774 vec![
20775 indent_guide(buffer_id, 1, 10, 0),
20776 indent_guide(buffer_id, 2, 5, 1),
20777 indent_guide(buffer_id, 7, 9, 1),
20778 indent_guide(buffer_id, 8, 8, 2),
20779 ],
20780 None,
20781 &mut cx,
20782 );
20783}
20784
20785#[gpui::test]
20786async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20787 let (buffer_id, mut cx) = setup_indent_guides_editor(
20788 &"
20789 block1
20790 block2
20791 block3
20792 block4
20793 block2
20794 block1
20795 block1"
20796 .unindent(),
20797 cx,
20798 )
20799 .await;
20800
20801 assert_indent_guides(
20802 1..10,
20803 vec![
20804 indent_guide(buffer_id, 1, 4, 0),
20805 indent_guide(buffer_id, 2, 3, 1),
20806 indent_guide(buffer_id, 3, 3, 2),
20807 ],
20808 None,
20809 &mut cx,
20810 );
20811}
20812
20813#[gpui::test]
20814async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20815 let (buffer_id, mut cx) = setup_indent_guides_editor(
20816 &"
20817 block1
20818 block2
20819 block3
20820
20821 block1
20822 block1"
20823 .unindent(),
20824 cx,
20825 )
20826 .await;
20827
20828 assert_indent_guides(
20829 0..6,
20830 vec![
20831 indent_guide(buffer_id, 1, 2, 0),
20832 indent_guide(buffer_id, 2, 2, 1),
20833 ],
20834 None,
20835 &mut cx,
20836 );
20837}
20838
20839#[gpui::test]
20840async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20841 let (buffer_id, mut cx) = setup_indent_guides_editor(
20842 &"
20843 function component() {
20844 \treturn (
20845 \t\t\t
20846 \t\t<div>
20847 \t\t\t<abc></abc>
20848 \t\t</div>
20849 \t)
20850 }"
20851 .unindent(),
20852 cx,
20853 )
20854 .await;
20855
20856 assert_indent_guides(
20857 0..8,
20858 vec![
20859 indent_guide(buffer_id, 1, 6, 0),
20860 indent_guide(buffer_id, 2, 5, 1),
20861 indent_guide(buffer_id, 4, 4, 2),
20862 ],
20863 None,
20864 &mut cx,
20865 );
20866}
20867
20868#[gpui::test]
20869async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20870 let (buffer_id, mut cx) = setup_indent_guides_editor(
20871 &"
20872 function component() {
20873 \treturn (
20874 \t
20875 \t\t<div>
20876 \t\t\t<abc></abc>
20877 \t\t</div>
20878 \t)
20879 }"
20880 .unindent(),
20881 cx,
20882 )
20883 .await;
20884
20885 assert_indent_guides(
20886 0..8,
20887 vec![
20888 indent_guide(buffer_id, 1, 6, 0),
20889 indent_guide(buffer_id, 2, 5, 1),
20890 indent_guide(buffer_id, 4, 4, 2),
20891 ],
20892 None,
20893 &mut cx,
20894 );
20895}
20896
20897#[gpui::test]
20898async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20899 let (buffer_id, mut cx) = setup_indent_guides_editor(
20900 &"
20901 block1
20902
20903
20904
20905 block2
20906 "
20907 .unindent(),
20908 cx,
20909 )
20910 .await;
20911
20912 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20913}
20914
20915#[gpui::test]
20916async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20917 let (buffer_id, mut cx) = setup_indent_guides_editor(
20918 &"
20919 def a:
20920 \tb = 3
20921 \tif True:
20922 \t\tc = 4
20923 \t\td = 5
20924 \tprint(b)
20925 "
20926 .unindent(),
20927 cx,
20928 )
20929 .await;
20930
20931 assert_indent_guides(
20932 0..6,
20933 vec![
20934 indent_guide(buffer_id, 1, 5, 0),
20935 indent_guide(buffer_id, 3, 4, 1),
20936 ],
20937 None,
20938 &mut cx,
20939 );
20940}
20941
20942#[gpui::test]
20943async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20944 let (buffer_id, mut cx) = setup_indent_guides_editor(
20945 &"
20946 fn main() {
20947 let a = 1;
20948 }"
20949 .unindent(),
20950 cx,
20951 )
20952 .await;
20953
20954 cx.update_editor(|editor, window, cx| {
20955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20956 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20957 });
20958 });
20959
20960 assert_indent_guides(
20961 0..3,
20962 vec![indent_guide(buffer_id, 1, 1, 0)],
20963 Some(vec![0]),
20964 &mut cx,
20965 );
20966}
20967
20968#[gpui::test]
20969async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20970 let (buffer_id, mut cx) = setup_indent_guides_editor(
20971 &"
20972 fn main() {
20973 if 1 == 2 {
20974 let a = 1;
20975 }
20976 }"
20977 .unindent(),
20978 cx,
20979 )
20980 .await;
20981
20982 cx.update_editor(|editor, window, cx| {
20983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20984 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20985 });
20986 });
20987
20988 assert_indent_guides(
20989 0..4,
20990 vec![
20991 indent_guide(buffer_id, 1, 3, 0),
20992 indent_guide(buffer_id, 2, 2, 1),
20993 ],
20994 Some(vec![1]),
20995 &mut cx,
20996 );
20997
20998 cx.update_editor(|editor, window, cx| {
20999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21000 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21001 });
21002 });
21003
21004 assert_indent_guides(
21005 0..4,
21006 vec![
21007 indent_guide(buffer_id, 1, 3, 0),
21008 indent_guide(buffer_id, 2, 2, 1),
21009 ],
21010 Some(vec![1]),
21011 &mut cx,
21012 );
21013
21014 cx.update_editor(|editor, window, cx| {
21015 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21016 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21017 });
21018 });
21019
21020 assert_indent_guides(
21021 0..4,
21022 vec![
21023 indent_guide(buffer_id, 1, 3, 0),
21024 indent_guide(buffer_id, 2, 2, 1),
21025 ],
21026 Some(vec![0]),
21027 &mut cx,
21028 );
21029}
21030
21031#[gpui::test]
21032async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21033 let (buffer_id, mut cx) = setup_indent_guides_editor(
21034 &"
21035 fn main() {
21036 let a = 1;
21037
21038 let b = 2;
21039 }"
21040 .unindent(),
21041 cx,
21042 )
21043 .await;
21044
21045 cx.update_editor(|editor, window, cx| {
21046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21047 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21048 });
21049 });
21050
21051 assert_indent_guides(
21052 0..5,
21053 vec![indent_guide(buffer_id, 1, 3, 0)],
21054 Some(vec![0]),
21055 &mut cx,
21056 );
21057}
21058
21059#[gpui::test]
21060async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21061 let (buffer_id, mut cx) = setup_indent_guides_editor(
21062 &"
21063 def m:
21064 a = 1
21065 pass"
21066 .unindent(),
21067 cx,
21068 )
21069 .await;
21070
21071 cx.update_editor(|editor, window, cx| {
21072 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21073 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21074 });
21075 });
21076
21077 assert_indent_guides(
21078 0..3,
21079 vec![indent_guide(buffer_id, 1, 2, 0)],
21080 Some(vec![0]),
21081 &mut cx,
21082 );
21083}
21084
21085#[gpui::test]
21086async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21087 init_test(cx, |_| {});
21088 let mut cx = EditorTestContext::new(cx).await;
21089 let text = indoc! {
21090 "
21091 impl A {
21092 fn b() {
21093 0;
21094 3;
21095 5;
21096 6;
21097 7;
21098 }
21099 }
21100 "
21101 };
21102 let base_text = indoc! {
21103 "
21104 impl A {
21105 fn b() {
21106 0;
21107 1;
21108 2;
21109 3;
21110 4;
21111 }
21112 fn c() {
21113 5;
21114 6;
21115 7;
21116 }
21117 }
21118 "
21119 };
21120
21121 cx.update_editor(|editor, window, cx| {
21122 editor.set_text(text, window, cx);
21123
21124 editor.buffer().update(cx, |multibuffer, cx| {
21125 let buffer = multibuffer.as_singleton().unwrap();
21126 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21127
21128 multibuffer.set_all_diff_hunks_expanded(cx);
21129 multibuffer.add_diff(diff, cx);
21130
21131 buffer.read(cx).remote_id()
21132 })
21133 });
21134 cx.run_until_parked();
21135
21136 cx.assert_state_with_diff(
21137 indoc! { "
21138 impl A {
21139 fn b() {
21140 0;
21141 - 1;
21142 - 2;
21143 3;
21144 - 4;
21145 - }
21146 - fn c() {
21147 5;
21148 6;
21149 7;
21150 }
21151 }
21152 ˇ"
21153 }
21154 .to_string(),
21155 );
21156
21157 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21158 editor
21159 .snapshot(window, cx)
21160 .buffer_snapshot()
21161 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21162 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21163 .collect::<Vec<_>>()
21164 });
21165 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21166 assert_eq!(
21167 actual_guides,
21168 vec![
21169 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21170 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21171 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21172 ]
21173 );
21174}
21175
21176#[gpui::test]
21177async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21178 init_test(cx, |_| {});
21179 let mut cx = EditorTestContext::new(cx).await;
21180
21181 let diff_base = r#"
21182 a
21183 b
21184 c
21185 "#
21186 .unindent();
21187
21188 cx.set_state(
21189 &r#"
21190 ˇA
21191 b
21192 C
21193 "#
21194 .unindent(),
21195 );
21196 cx.set_head_text(&diff_base);
21197 cx.update_editor(|editor, window, cx| {
21198 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21199 });
21200 executor.run_until_parked();
21201
21202 let both_hunks_expanded = r#"
21203 - a
21204 + ˇA
21205 b
21206 - c
21207 + C
21208 "#
21209 .unindent();
21210
21211 cx.assert_state_with_diff(both_hunks_expanded.clone());
21212
21213 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21214 let snapshot = editor.snapshot(window, cx);
21215 let hunks = editor
21216 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21217 .collect::<Vec<_>>();
21218 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21219 let buffer_id = hunks[0].buffer_id;
21220 hunks
21221 .into_iter()
21222 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21223 .collect::<Vec<_>>()
21224 });
21225 assert_eq!(hunk_ranges.len(), 2);
21226
21227 cx.update_editor(|editor, _, cx| {
21228 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21229 });
21230 executor.run_until_parked();
21231
21232 let second_hunk_expanded = r#"
21233 ˇA
21234 b
21235 - c
21236 + C
21237 "#
21238 .unindent();
21239
21240 cx.assert_state_with_diff(second_hunk_expanded);
21241
21242 cx.update_editor(|editor, _, cx| {
21243 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21244 });
21245 executor.run_until_parked();
21246
21247 cx.assert_state_with_diff(both_hunks_expanded.clone());
21248
21249 cx.update_editor(|editor, _, cx| {
21250 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21251 });
21252 executor.run_until_parked();
21253
21254 let first_hunk_expanded = r#"
21255 - a
21256 + ˇA
21257 b
21258 C
21259 "#
21260 .unindent();
21261
21262 cx.assert_state_with_diff(first_hunk_expanded);
21263
21264 cx.update_editor(|editor, _, cx| {
21265 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21266 });
21267 executor.run_until_parked();
21268
21269 cx.assert_state_with_diff(both_hunks_expanded);
21270
21271 cx.set_state(
21272 &r#"
21273 ˇA
21274 b
21275 "#
21276 .unindent(),
21277 );
21278 cx.run_until_parked();
21279
21280 // TODO this cursor position seems bad
21281 cx.assert_state_with_diff(
21282 r#"
21283 - ˇa
21284 + A
21285 b
21286 "#
21287 .unindent(),
21288 );
21289
21290 cx.update_editor(|editor, window, cx| {
21291 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21292 });
21293
21294 cx.assert_state_with_diff(
21295 r#"
21296 - ˇa
21297 + A
21298 b
21299 - c
21300 "#
21301 .unindent(),
21302 );
21303
21304 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21305 let snapshot = editor.snapshot(window, cx);
21306 let hunks = editor
21307 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21308 .collect::<Vec<_>>();
21309 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21310 let buffer_id = hunks[0].buffer_id;
21311 hunks
21312 .into_iter()
21313 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21314 .collect::<Vec<_>>()
21315 });
21316 assert_eq!(hunk_ranges.len(), 2);
21317
21318 cx.update_editor(|editor, _, cx| {
21319 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21320 });
21321 executor.run_until_parked();
21322
21323 cx.assert_state_with_diff(
21324 r#"
21325 - ˇa
21326 + A
21327 b
21328 "#
21329 .unindent(),
21330 );
21331}
21332
21333#[gpui::test]
21334async fn test_toggle_deletion_hunk_at_start_of_file(
21335 executor: BackgroundExecutor,
21336 cx: &mut TestAppContext,
21337) {
21338 init_test(cx, |_| {});
21339 let mut cx = EditorTestContext::new(cx).await;
21340
21341 let diff_base = r#"
21342 a
21343 b
21344 c
21345 "#
21346 .unindent();
21347
21348 cx.set_state(
21349 &r#"
21350 ˇb
21351 c
21352 "#
21353 .unindent(),
21354 );
21355 cx.set_head_text(&diff_base);
21356 cx.update_editor(|editor, window, cx| {
21357 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21358 });
21359 executor.run_until_parked();
21360
21361 let hunk_expanded = r#"
21362 - a
21363 ˇb
21364 c
21365 "#
21366 .unindent();
21367
21368 cx.assert_state_with_diff(hunk_expanded.clone());
21369
21370 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21371 let snapshot = editor.snapshot(window, cx);
21372 let hunks = editor
21373 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21374 .collect::<Vec<_>>();
21375 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21376 let buffer_id = hunks[0].buffer_id;
21377 hunks
21378 .into_iter()
21379 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21380 .collect::<Vec<_>>()
21381 });
21382 assert_eq!(hunk_ranges.len(), 1);
21383
21384 cx.update_editor(|editor, _, cx| {
21385 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21386 });
21387 executor.run_until_parked();
21388
21389 let hunk_collapsed = r#"
21390 ˇb
21391 c
21392 "#
21393 .unindent();
21394
21395 cx.assert_state_with_diff(hunk_collapsed);
21396
21397 cx.update_editor(|editor, _, cx| {
21398 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21399 });
21400 executor.run_until_parked();
21401
21402 cx.assert_state_with_diff(hunk_expanded);
21403}
21404
21405#[gpui::test]
21406async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21407 init_test(cx, |_| {});
21408
21409 let fs = FakeFs::new(cx.executor());
21410 fs.insert_tree(
21411 path!("/test"),
21412 json!({
21413 ".git": {},
21414 "file-1": "ONE\n",
21415 "file-2": "TWO\n",
21416 "file-3": "THREE\n",
21417 }),
21418 )
21419 .await;
21420
21421 fs.set_head_for_repo(
21422 path!("/test/.git").as_ref(),
21423 &[
21424 ("file-1", "one\n".into()),
21425 ("file-2", "two\n".into()),
21426 ("file-3", "three\n".into()),
21427 ],
21428 "deadbeef",
21429 );
21430
21431 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21432 let mut buffers = vec![];
21433 for i in 1..=3 {
21434 let buffer = project
21435 .update(cx, |project, cx| {
21436 let path = format!(path!("/test/file-{}"), i);
21437 project.open_local_buffer(path, cx)
21438 })
21439 .await
21440 .unwrap();
21441 buffers.push(buffer);
21442 }
21443
21444 let multibuffer = cx.new(|cx| {
21445 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21446 multibuffer.set_all_diff_hunks_expanded(cx);
21447 for buffer in &buffers {
21448 let snapshot = buffer.read(cx).snapshot();
21449 multibuffer.set_excerpts_for_path(
21450 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21451 buffer.clone(),
21452 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21453 2,
21454 cx,
21455 );
21456 }
21457 multibuffer
21458 });
21459
21460 let editor = cx.add_window(|window, cx| {
21461 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21462 });
21463 cx.run_until_parked();
21464
21465 let snapshot = editor
21466 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21467 .unwrap();
21468 let hunks = snapshot
21469 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21470 .map(|hunk| match hunk {
21471 DisplayDiffHunk::Unfolded {
21472 display_row_range, ..
21473 } => display_row_range,
21474 DisplayDiffHunk::Folded { .. } => unreachable!(),
21475 })
21476 .collect::<Vec<_>>();
21477 assert_eq!(
21478 hunks,
21479 [
21480 DisplayRow(2)..DisplayRow(4),
21481 DisplayRow(7)..DisplayRow(9),
21482 DisplayRow(12)..DisplayRow(14),
21483 ]
21484 );
21485}
21486
21487#[gpui::test]
21488async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21489 init_test(cx, |_| {});
21490
21491 let mut cx = EditorTestContext::new(cx).await;
21492 cx.set_head_text(indoc! { "
21493 one
21494 two
21495 three
21496 four
21497 five
21498 "
21499 });
21500 cx.set_index_text(indoc! { "
21501 one
21502 two
21503 three
21504 four
21505 five
21506 "
21507 });
21508 cx.set_state(indoc! {"
21509 one
21510 TWO
21511 ˇTHREE
21512 FOUR
21513 five
21514 "});
21515 cx.run_until_parked();
21516 cx.update_editor(|editor, window, cx| {
21517 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21518 });
21519 cx.run_until_parked();
21520 cx.assert_index_text(Some(indoc! {"
21521 one
21522 TWO
21523 THREE
21524 FOUR
21525 five
21526 "}));
21527 cx.set_state(indoc! { "
21528 one
21529 TWO
21530 ˇTHREE-HUNDRED
21531 FOUR
21532 five
21533 "});
21534 cx.run_until_parked();
21535 cx.update_editor(|editor, window, cx| {
21536 let snapshot = editor.snapshot(window, cx);
21537 let hunks = editor
21538 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21539 .collect::<Vec<_>>();
21540 assert_eq!(hunks.len(), 1);
21541 assert_eq!(
21542 hunks[0].status(),
21543 DiffHunkStatus {
21544 kind: DiffHunkStatusKind::Modified,
21545 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21546 }
21547 );
21548
21549 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21550 });
21551 cx.run_until_parked();
21552 cx.assert_index_text(Some(indoc! {"
21553 one
21554 TWO
21555 THREE-HUNDRED
21556 FOUR
21557 five
21558 "}));
21559}
21560
21561#[gpui::test]
21562fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21563 init_test(cx, |_| {});
21564
21565 let editor = cx.add_window(|window, cx| {
21566 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21567 build_editor(buffer, window, cx)
21568 });
21569
21570 let render_args = Arc::new(Mutex::new(None));
21571 let snapshot = editor
21572 .update(cx, |editor, window, cx| {
21573 let snapshot = editor.buffer().read(cx).snapshot(cx);
21574 let range =
21575 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21576
21577 struct RenderArgs {
21578 row: MultiBufferRow,
21579 folded: bool,
21580 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21581 }
21582
21583 let crease = Crease::inline(
21584 range,
21585 FoldPlaceholder::test(),
21586 {
21587 let toggle_callback = render_args.clone();
21588 move |row, folded, callback, _window, _cx| {
21589 *toggle_callback.lock() = Some(RenderArgs {
21590 row,
21591 folded,
21592 callback,
21593 });
21594 div()
21595 }
21596 },
21597 |_row, _folded, _window, _cx| div(),
21598 );
21599
21600 editor.insert_creases(Some(crease), cx);
21601 let snapshot = editor.snapshot(window, cx);
21602 let _div =
21603 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21604 snapshot
21605 })
21606 .unwrap();
21607
21608 let render_args = render_args.lock().take().unwrap();
21609 assert_eq!(render_args.row, MultiBufferRow(1));
21610 assert!(!render_args.folded);
21611 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21612
21613 cx.update_window(*editor, |_, window, cx| {
21614 (render_args.callback)(true, window, cx)
21615 })
21616 .unwrap();
21617 let snapshot = editor
21618 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21619 .unwrap();
21620 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21621
21622 cx.update_window(*editor, |_, window, cx| {
21623 (render_args.callback)(false, window, cx)
21624 })
21625 .unwrap();
21626 let snapshot = editor
21627 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21628 .unwrap();
21629 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21630}
21631
21632#[gpui::test]
21633async fn test_input_text(cx: &mut TestAppContext) {
21634 init_test(cx, |_| {});
21635 let mut cx = EditorTestContext::new(cx).await;
21636
21637 cx.set_state(
21638 &r#"ˇone
21639 two
21640
21641 three
21642 fourˇ
21643 five
21644
21645 siˇx"#
21646 .unindent(),
21647 );
21648
21649 cx.dispatch_action(HandleInput(String::new()));
21650 cx.assert_editor_state(
21651 &r#"ˇone
21652 two
21653
21654 three
21655 fourˇ
21656 five
21657
21658 siˇx"#
21659 .unindent(),
21660 );
21661
21662 cx.dispatch_action(HandleInput("AAAA".to_string()));
21663 cx.assert_editor_state(
21664 &r#"AAAAˇone
21665 two
21666
21667 three
21668 fourAAAAˇ
21669 five
21670
21671 siAAAAˇx"#
21672 .unindent(),
21673 );
21674}
21675
21676#[gpui::test]
21677async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21678 init_test(cx, |_| {});
21679
21680 let mut cx = EditorTestContext::new(cx).await;
21681 cx.set_state(
21682 r#"let foo = 1;
21683let foo = 2;
21684let foo = 3;
21685let fooˇ = 4;
21686let foo = 5;
21687let foo = 6;
21688let foo = 7;
21689let foo = 8;
21690let foo = 9;
21691let foo = 10;
21692let foo = 11;
21693let foo = 12;
21694let foo = 13;
21695let foo = 14;
21696let foo = 15;"#,
21697 );
21698
21699 cx.update_editor(|e, window, cx| {
21700 assert_eq!(
21701 e.next_scroll_position,
21702 NextScrollCursorCenterTopBottom::Center,
21703 "Default next scroll direction is center",
21704 );
21705
21706 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21707 assert_eq!(
21708 e.next_scroll_position,
21709 NextScrollCursorCenterTopBottom::Top,
21710 "After center, next scroll direction should be top",
21711 );
21712
21713 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21714 assert_eq!(
21715 e.next_scroll_position,
21716 NextScrollCursorCenterTopBottom::Bottom,
21717 "After top, next scroll direction should be bottom",
21718 );
21719
21720 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21721 assert_eq!(
21722 e.next_scroll_position,
21723 NextScrollCursorCenterTopBottom::Center,
21724 "After bottom, scrolling should start over",
21725 );
21726
21727 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21728 assert_eq!(
21729 e.next_scroll_position,
21730 NextScrollCursorCenterTopBottom::Top,
21731 "Scrolling continues if retriggered fast enough"
21732 );
21733 });
21734
21735 cx.executor()
21736 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21737 cx.executor().run_until_parked();
21738 cx.update_editor(|e, _, _| {
21739 assert_eq!(
21740 e.next_scroll_position,
21741 NextScrollCursorCenterTopBottom::Center,
21742 "If scrolling is not triggered fast enough, it should reset"
21743 );
21744 });
21745}
21746
21747#[gpui::test]
21748async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21749 init_test(cx, |_| {});
21750 let mut cx = EditorLspTestContext::new_rust(
21751 lsp::ServerCapabilities {
21752 definition_provider: Some(lsp::OneOf::Left(true)),
21753 references_provider: Some(lsp::OneOf::Left(true)),
21754 ..lsp::ServerCapabilities::default()
21755 },
21756 cx,
21757 )
21758 .await;
21759
21760 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21761 let go_to_definition = cx
21762 .lsp
21763 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21764 move |params, _| async move {
21765 if empty_go_to_definition {
21766 Ok(None)
21767 } else {
21768 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21769 uri: params.text_document_position_params.text_document.uri,
21770 range: lsp::Range::new(
21771 lsp::Position::new(4, 3),
21772 lsp::Position::new(4, 6),
21773 ),
21774 })))
21775 }
21776 },
21777 );
21778 let references = cx
21779 .lsp
21780 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21781 Ok(Some(vec![lsp::Location {
21782 uri: params.text_document_position.text_document.uri,
21783 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21784 }]))
21785 });
21786 (go_to_definition, references)
21787 };
21788
21789 cx.set_state(
21790 &r#"fn one() {
21791 let mut a = ˇtwo();
21792 }
21793
21794 fn two() {}"#
21795 .unindent(),
21796 );
21797 set_up_lsp_handlers(false, &mut cx);
21798 let navigated = cx
21799 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21800 .await
21801 .expect("Failed to navigate to definition");
21802 assert_eq!(
21803 navigated,
21804 Navigated::Yes,
21805 "Should have navigated to definition from the GetDefinition response"
21806 );
21807 cx.assert_editor_state(
21808 &r#"fn one() {
21809 let mut a = two();
21810 }
21811
21812 fn «twoˇ»() {}"#
21813 .unindent(),
21814 );
21815
21816 let editors = cx.update_workspace(|workspace, _, cx| {
21817 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21818 });
21819 cx.update_editor(|_, _, test_editor_cx| {
21820 assert_eq!(
21821 editors.len(),
21822 1,
21823 "Initially, only one, test, editor should be open in the workspace"
21824 );
21825 assert_eq!(
21826 test_editor_cx.entity(),
21827 editors.last().expect("Asserted len is 1").clone()
21828 );
21829 });
21830
21831 set_up_lsp_handlers(true, &mut cx);
21832 let navigated = cx
21833 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21834 .await
21835 .expect("Failed to navigate to lookup references");
21836 assert_eq!(
21837 navigated,
21838 Navigated::Yes,
21839 "Should have navigated to references as a fallback after empty GoToDefinition response"
21840 );
21841 // We should not change the selections in the existing file,
21842 // if opening another milti buffer with the references
21843 cx.assert_editor_state(
21844 &r#"fn one() {
21845 let mut a = two();
21846 }
21847
21848 fn «twoˇ»() {}"#
21849 .unindent(),
21850 );
21851 let editors = cx.update_workspace(|workspace, _, cx| {
21852 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21853 });
21854 cx.update_editor(|_, _, test_editor_cx| {
21855 assert_eq!(
21856 editors.len(),
21857 2,
21858 "After falling back to references search, we open a new editor with the results"
21859 );
21860 let references_fallback_text = editors
21861 .into_iter()
21862 .find(|new_editor| *new_editor != test_editor_cx.entity())
21863 .expect("Should have one non-test editor now")
21864 .read(test_editor_cx)
21865 .text(test_editor_cx);
21866 assert_eq!(
21867 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21868 "Should use the range from the references response and not the GoToDefinition one"
21869 );
21870 });
21871}
21872
21873#[gpui::test]
21874async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21875 init_test(cx, |_| {});
21876 cx.update(|cx| {
21877 let mut editor_settings = EditorSettings::get_global(cx).clone();
21878 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21879 EditorSettings::override_global(editor_settings, cx);
21880 });
21881 let mut cx = EditorLspTestContext::new_rust(
21882 lsp::ServerCapabilities {
21883 definition_provider: Some(lsp::OneOf::Left(true)),
21884 references_provider: Some(lsp::OneOf::Left(true)),
21885 ..lsp::ServerCapabilities::default()
21886 },
21887 cx,
21888 )
21889 .await;
21890 let original_state = r#"fn one() {
21891 let mut a = ˇtwo();
21892 }
21893
21894 fn two() {}"#
21895 .unindent();
21896 cx.set_state(&original_state);
21897
21898 let mut go_to_definition = cx
21899 .lsp
21900 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21901 move |_, _| async move { Ok(None) },
21902 );
21903 let _references = cx
21904 .lsp
21905 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21906 panic!("Should not call for references with no go to definition fallback")
21907 });
21908
21909 let navigated = cx
21910 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21911 .await
21912 .expect("Failed to navigate to lookup references");
21913 go_to_definition
21914 .next()
21915 .await
21916 .expect("Should have called the go_to_definition handler");
21917
21918 assert_eq!(
21919 navigated,
21920 Navigated::No,
21921 "Should have navigated to references as a fallback after empty GoToDefinition response"
21922 );
21923 cx.assert_editor_state(&original_state);
21924 let editors = cx.update_workspace(|workspace, _, cx| {
21925 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21926 });
21927 cx.update_editor(|_, _, _| {
21928 assert_eq!(
21929 editors.len(),
21930 1,
21931 "After unsuccessful fallback, no other editor should have been opened"
21932 );
21933 });
21934}
21935
21936#[gpui::test]
21937async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21938 init_test(cx, |_| {});
21939 let mut cx = EditorLspTestContext::new_rust(
21940 lsp::ServerCapabilities {
21941 references_provider: Some(lsp::OneOf::Left(true)),
21942 ..lsp::ServerCapabilities::default()
21943 },
21944 cx,
21945 )
21946 .await;
21947
21948 cx.set_state(
21949 &r#"
21950 fn one() {
21951 let mut a = two();
21952 }
21953
21954 fn ˇtwo() {}"#
21955 .unindent(),
21956 );
21957 cx.lsp
21958 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21959 Ok(Some(vec![
21960 lsp::Location {
21961 uri: params.text_document_position.text_document.uri.clone(),
21962 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21963 },
21964 lsp::Location {
21965 uri: params.text_document_position.text_document.uri,
21966 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21967 },
21968 ]))
21969 });
21970 let navigated = cx
21971 .update_editor(|editor, window, cx| {
21972 editor.find_all_references(&FindAllReferences, window, cx)
21973 })
21974 .unwrap()
21975 .await
21976 .expect("Failed to navigate to references");
21977 assert_eq!(
21978 navigated,
21979 Navigated::Yes,
21980 "Should have navigated to references from the FindAllReferences response"
21981 );
21982 cx.assert_editor_state(
21983 &r#"fn one() {
21984 let mut a = two();
21985 }
21986
21987 fn ˇtwo() {}"#
21988 .unindent(),
21989 );
21990
21991 let editors = cx.update_workspace(|workspace, _, cx| {
21992 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21993 });
21994 cx.update_editor(|_, _, _| {
21995 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21996 });
21997
21998 cx.set_state(
21999 &r#"fn one() {
22000 let mut a = ˇtwo();
22001 }
22002
22003 fn two() {}"#
22004 .unindent(),
22005 );
22006 let navigated = cx
22007 .update_editor(|editor, window, cx| {
22008 editor.find_all_references(&FindAllReferences, window, cx)
22009 })
22010 .unwrap()
22011 .await
22012 .expect("Failed to navigate to references");
22013 assert_eq!(
22014 navigated,
22015 Navigated::Yes,
22016 "Should have navigated to references from the FindAllReferences response"
22017 );
22018 cx.assert_editor_state(
22019 &r#"fn one() {
22020 let mut a = ˇtwo();
22021 }
22022
22023 fn two() {}"#
22024 .unindent(),
22025 );
22026 let editors = cx.update_workspace(|workspace, _, cx| {
22027 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22028 });
22029 cx.update_editor(|_, _, _| {
22030 assert_eq!(
22031 editors.len(),
22032 2,
22033 "should have re-used the previous multibuffer"
22034 );
22035 });
22036
22037 cx.set_state(
22038 &r#"fn one() {
22039 let mut a = ˇtwo();
22040 }
22041 fn three() {}
22042 fn two() {}"#
22043 .unindent(),
22044 );
22045 cx.lsp
22046 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22047 Ok(Some(vec![
22048 lsp::Location {
22049 uri: params.text_document_position.text_document.uri.clone(),
22050 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22051 },
22052 lsp::Location {
22053 uri: params.text_document_position.text_document.uri,
22054 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22055 },
22056 ]))
22057 });
22058 let navigated = cx
22059 .update_editor(|editor, window, cx| {
22060 editor.find_all_references(&FindAllReferences, window, cx)
22061 })
22062 .unwrap()
22063 .await
22064 .expect("Failed to navigate to references");
22065 assert_eq!(
22066 navigated,
22067 Navigated::Yes,
22068 "Should have navigated to references from the FindAllReferences response"
22069 );
22070 cx.assert_editor_state(
22071 &r#"fn one() {
22072 let mut a = ˇtwo();
22073 }
22074 fn three() {}
22075 fn two() {}"#
22076 .unindent(),
22077 );
22078 let editors = cx.update_workspace(|workspace, _, cx| {
22079 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22080 });
22081 cx.update_editor(|_, _, _| {
22082 assert_eq!(
22083 editors.len(),
22084 3,
22085 "should have used a new multibuffer as offsets changed"
22086 );
22087 });
22088}
22089#[gpui::test]
22090async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22091 init_test(cx, |_| {});
22092
22093 let language = Arc::new(Language::new(
22094 LanguageConfig::default(),
22095 Some(tree_sitter_rust::LANGUAGE.into()),
22096 ));
22097
22098 let text = r#"
22099 #[cfg(test)]
22100 mod tests() {
22101 #[test]
22102 fn runnable_1() {
22103 let a = 1;
22104 }
22105
22106 #[test]
22107 fn runnable_2() {
22108 let a = 1;
22109 let b = 2;
22110 }
22111 }
22112 "#
22113 .unindent();
22114
22115 let fs = FakeFs::new(cx.executor());
22116 fs.insert_file("/file.rs", Default::default()).await;
22117
22118 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22119 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22120 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22121 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22122 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22123
22124 let editor = cx.new_window_entity(|window, cx| {
22125 Editor::new(
22126 EditorMode::full(),
22127 multi_buffer,
22128 Some(project.clone()),
22129 window,
22130 cx,
22131 )
22132 });
22133
22134 editor.update_in(cx, |editor, window, cx| {
22135 let snapshot = editor.buffer().read(cx).snapshot(cx);
22136 editor.tasks.insert(
22137 (buffer.read(cx).remote_id(), 3),
22138 RunnableTasks {
22139 templates: vec![],
22140 offset: snapshot.anchor_before(43),
22141 column: 0,
22142 extra_variables: HashMap::default(),
22143 context_range: BufferOffset(43)..BufferOffset(85),
22144 },
22145 );
22146 editor.tasks.insert(
22147 (buffer.read(cx).remote_id(), 8),
22148 RunnableTasks {
22149 templates: vec![],
22150 offset: snapshot.anchor_before(86),
22151 column: 0,
22152 extra_variables: HashMap::default(),
22153 context_range: BufferOffset(86)..BufferOffset(191),
22154 },
22155 );
22156
22157 // Test finding task when cursor is inside function body
22158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22159 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22160 });
22161 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22162 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22163
22164 // Test finding task when cursor is on function name
22165 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22166 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22167 });
22168 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22169 assert_eq!(row, 8, "Should find task when cursor is on function name");
22170 });
22171}
22172
22173#[gpui::test]
22174async fn test_folding_buffers(cx: &mut TestAppContext) {
22175 init_test(cx, |_| {});
22176
22177 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22178 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22179 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22180
22181 let fs = FakeFs::new(cx.executor());
22182 fs.insert_tree(
22183 path!("/a"),
22184 json!({
22185 "first.rs": sample_text_1,
22186 "second.rs": sample_text_2,
22187 "third.rs": sample_text_3,
22188 }),
22189 )
22190 .await;
22191 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22192 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22193 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22194 let worktree = project.update(cx, |project, cx| {
22195 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22196 assert_eq!(worktrees.len(), 1);
22197 worktrees.pop().unwrap()
22198 });
22199 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22200
22201 let buffer_1 = project
22202 .update(cx, |project, cx| {
22203 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22204 })
22205 .await
22206 .unwrap();
22207 let buffer_2 = project
22208 .update(cx, |project, cx| {
22209 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22210 })
22211 .await
22212 .unwrap();
22213 let buffer_3 = project
22214 .update(cx, |project, cx| {
22215 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22216 })
22217 .await
22218 .unwrap();
22219
22220 let multi_buffer = cx.new(|cx| {
22221 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22222 multi_buffer.push_excerpts(
22223 buffer_1.clone(),
22224 [
22225 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22226 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22227 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22228 ],
22229 cx,
22230 );
22231 multi_buffer.push_excerpts(
22232 buffer_2.clone(),
22233 [
22234 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22235 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22236 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22237 ],
22238 cx,
22239 );
22240 multi_buffer.push_excerpts(
22241 buffer_3.clone(),
22242 [
22243 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22244 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22245 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22246 ],
22247 cx,
22248 );
22249 multi_buffer
22250 });
22251 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22252 Editor::new(
22253 EditorMode::full(),
22254 multi_buffer.clone(),
22255 Some(project.clone()),
22256 window,
22257 cx,
22258 )
22259 });
22260
22261 assert_eq!(
22262 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22263 "\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",
22264 );
22265
22266 multi_buffer_editor.update(cx, |editor, cx| {
22267 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22268 });
22269 assert_eq!(
22270 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22271 "\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",
22272 "After folding the first buffer, its text should not be displayed"
22273 );
22274
22275 multi_buffer_editor.update(cx, |editor, cx| {
22276 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22277 });
22278 assert_eq!(
22279 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22280 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22281 "After folding the second buffer, its text should not be displayed"
22282 );
22283
22284 multi_buffer_editor.update(cx, |editor, cx| {
22285 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22286 });
22287 assert_eq!(
22288 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22289 "\n\n\n\n\n",
22290 "After folding the third buffer, its text should not be displayed"
22291 );
22292
22293 // Emulate selection inside the fold logic, that should work
22294 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22295 editor
22296 .snapshot(window, cx)
22297 .next_line_boundary(Point::new(0, 4));
22298 });
22299
22300 multi_buffer_editor.update(cx, |editor, cx| {
22301 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22302 });
22303 assert_eq!(
22304 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22305 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22306 "After unfolding the second buffer, its text should be displayed"
22307 );
22308
22309 // Typing inside of buffer 1 causes that buffer to be unfolded.
22310 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22311 assert_eq!(
22312 multi_buffer
22313 .read(cx)
22314 .snapshot(cx)
22315 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22316 .collect::<String>(),
22317 "bbbb"
22318 );
22319 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22320 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22321 });
22322 editor.handle_input("B", window, cx);
22323 });
22324
22325 assert_eq!(
22326 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22327 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22328 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22329 );
22330
22331 multi_buffer_editor.update(cx, |editor, cx| {
22332 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22333 });
22334 assert_eq!(
22335 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22336 "\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",
22337 "After unfolding the all buffers, all original text should be displayed"
22338 );
22339}
22340
22341#[gpui::test]
22342async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22343 init_test(cx, |_| {});
22344
22345 let sample_text_1 = "1111\n2222\n3333".to_string();
22346 let sample_text_2 = "4444\n5555\n6666".to_string();
22347 let sample_text_3 = "7777\n8888\n9999".to_string();
22348
22349 let fs = FakeFs::new(cx.executor());
22350 fs.insert_tree(
22351 path!("/a"),
22352 json!({
22353 "first.rs": sample_text_1,
22354 "second.rs": sample_text_2,
22355 "third.rs": sample_text_3,
22356 }),
22357 )
22358 .await;
22359 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22360 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22361 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22362 let worktree = project.update(cx, |project, cx| {
22363 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22364 assert_eq!(worktrees.len(), 1);
22365 worktrees.pop().unwrap()
22366 });
22367 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22368
22369 let buffer_1 = project
22370 .update(cx, |project, cx| {
22371 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22372 })
22373 .await
22374 .unwrap();
22375 let buffer_2 = project
22376 .update(cx, |project, cx| {
22377 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22378 })
22379 .await
22380 .unwrap();
22381 let buffer_3 = project
22382 .update(cx, |project, cx| {
22383 project.open_buffer((worktree_id, rel_path("third.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(Point::new(0, 0)..Point::new(3, 0))],
22393 cx,
22394 );
22395 multi_buffer.push_excerpts(
22396 buffer_2.clone(),
22397 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22398 cx,
22399 );
22400 multi_buffer.push_excerpts(
22401 buffer_3.clone(),
22402 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22403 cx,
22404 );
22405 multi_buffer
22406 });
22407
22408 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22409 Editor::new(
22410 EditorMode::full(),
22411 multi_buffer,
22412 Some(project.clone()),
22413 window,
22414 cx,
22415 )
22416 });
22417
22418 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22419 assert_eq!(
22420 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22421 full_text,
22422 );
22423
22424 multi_buffer_editor.update(cx, |editor, cx| {
22425 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22426 });
22427 assert_eq!(
22428 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22429 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22430 "After folding the first buffer, its text should not be displayed"
22431 );
22432
22433 multi_buffer_editor.update(cx, |editor, cx| {
22434 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22435 });
22436
22437 assert_eq!(
22438 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22439 "\n\n\n\n\n\n7777\n8888\n9999",
22440 "After folding the second buffer, its text should not be displayed"
22441 );
22442
22443 multi_buffer_editor.update(cx, |editor, cx| {
22444 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22445 });
22446 assert_eq!(
22447 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22448 "\n\n\n\n\n",
22449 "After folding the third buffer, its text should not be displayed"
22450 );
22451
22452 multi_buffer_editor.update(cx, |editor, cx| {
22453 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22454 });
22455 assert_eq!(
22456 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22457 "\n\n\n\n4444\n5555\n6666\n\n",
22458 "After unfolding the second buffer, its text should be displayed"
22459 );
22460
22461 multi_buffer_editor.update(cx, |editor, cx| {
22462 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22463 });
22464 assert_eq!(
22465 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22466 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22467 "After unfolding the first buffer, its text should be displayed"
22468 );
22469
22470 multi_buffer_editor.update(cx, |editor, cx| {
22471 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22472 });
22473 assert_eq!(
22474 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22475 full_text,
22476 "After unfolding all buffers, all original text should be displayed"
22477 );
22478}
22479
22480#[gpui::test]
22481async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22482 init_test(cx, |_| {});
22483
22484 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22485
22486 let fs = FakeFs::new(cx.executor());
22487 fs.insert_tree(
22488 path!("/a"),
22489 json!({
22490 "main.rs": sample_text,
22491 }),
22492 )
22493 .await;
22494 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22495 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22496 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22497 let worktree = project.update(cx, |project, cx| {
22498 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22499 assert_eq!(worktrees.len(), 1);
22500 worktrees.pop().unwrap()
22501 });
22502 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22503
22504 let buffer_1 = project
22505 .update(cx, |project, cx| {
22506 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22507 })
22508 .await
22509 .unwrap();
22510
22511 let multi_buffer = cx.new(|cx| {
22512 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22513 multi_buffer.push_excerpts(
22514 buffer_1.clone(),
22515 [ExcerptRange::new(
22516 Point::new(0, 0)
22517 ..Point::new(
22518 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22519 0,
22520 ),
22521 )],
22522 cx,
22523 );
22524 multi_buffer
22525 });
22526 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22527 Editor::new(
22528 EditorMode::full(),
22529 multi_buffer,
22530 Some(project.clone()),
22531 window,
22532 cx,
22533 )
22534 });
22535
22536 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22537 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22538 enum TestHighlight {}
22539 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22540 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22541 editor.highlight_text::<TestHighlight>(
22542 vec![highlight_range.clone()],
22543 HighlightStyle::color(Hsla::green()),
22544 cx,
22545 );
22546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22547 s.select_ranges(Some(highlight_range))
22548 });
22549 });
22550
22551 let full_text = format!("\n\n{sample_text}");
22552 assert_eq!(
22553 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22554 full_text,
22555 );
22556}
22557
22558#[gpui::test]
22559async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22560 init_test(cx, |_| {});
22561 cx.update(|cx| {
22562 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22563 "keymaps/default-linux.json",
22564 cx,
22565 )
22566 .unwrap();
22567 cx.bind_keys(default_key_bindings);
22568 });
22569
22570 let (editor, cx) = cx.add_window_view(|window, cx| {
22571 let multi_buffer = MultiBuffer::build_multi(
22572 [
22573 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22574 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22575 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22576 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22577 ],
22578 cx,
22579 );
22580 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22581
22582 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22583 // fold all but the second buffer, so that we test navigating between two
22584 // adjacent folded buffers, as well as folded buffers at the start and
22585 // end the multibuffer
22586 editor.fold_buffer(buffer_ids[0], cx);
22587 editor.fold_buffer(buffer_ids[2], cx);
22588 editor.fold_buffer(buffer_ids[3], cx);
22589
22590 editor
22591 });
22592 cx.simulate_resize(size(px(1000.), px(1000.)));
22593
22594 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22595 cx.assert_excerpts_with_selections(indoc! {"
22596 [EXCERPT]
22597 ˇ[FOLDED]
22598 [EXCERPT]
22599 a1
22600 b1
22601 [EXCERPT]
22602 [FOLDED]
22603 [EXCERPT]
22604 [FOLDED]
22605 "
22606 });
22607 cx.simulate_keystroke("down");
22608 cx.assert_excerpts_with_selections(indoc! {"
22609 [EXCERPT]
22610 [FOLDED]
22611 [EXCERPT]
22612 ˇa1
22613 b1
22614 [EXCERPT]
22615 [FOLDED]
22616 [EXCERPT]
22617 [FOLDED]
22618 "
22619 });
22620 cx.simulate_keystroke("down");
22621 cx.assert_excerpts_with_selections(indoc! {"
22622 [EXCERPT]
22623 [FOLDED]
22624 [EXCERPT]
22625 a1
22626 ˇb1
22627 [EXCERPT]
22628 [FOLDED]
22629 [EXCERPT]
22630 [FOLDED]
22631 "
22632 });
22633 cx.simulate_keystroke("down");
22634 cx.assert_excerpts_with_selections(indoc! {"
22635 [EXCERPT]
22636 [FOLDED]
22637 [EXCERPT]
22638 a1
22639 b1
22640 ˇ[EXCERPT]
22641 [FOLDED]
22642 [EXCERPT]
22643 [FOLDED]
22644 "
22645 });
22646 cx.simulate_keystroke("down");
22647 cx.assert_excerpts_with_selections(indoc! {"
22648 [EXCERPT]
22649 [FOLDED]
22650 [EXCERPT]
22651 a1
22652 b1
22653 [EXCERPT]
22654 ˇ[FOLDED]
22655 [EXCERPT]
22656 [FOLDED]
22657 "
22658 });
22659 for _ in 0..5 {
22660 cx.simulate_keystroke("down");
22661 cx.assert_excerpts_with_selections(indoc! {"
22662 [EXCERPT]
22663 [FOLDED]
22664 [EXCERPT]
22665 a1
22666 b1
22667 [EXCERPT]
22668 [FOLDED]
22669 [EXCERPT]
22670 ˇ[FOLDED]
22671 "
22672 });
22673 }
22674
22675 cx.simulate_keystroke("up");
22676 cx.assert_excerpts_with_selections(indoc! {"
22677 [EXCERPT]
22678 [FOLDED]
22679 [EXCERPT]
22680 a1
22681 b1
22682 [EXCERPT]
22683 ˇ[FOLDED]
22684 [EXCERPT]
22685 [FOLDED]
22686 "
22687 });
22688 cx.simulate_keystroke("up");
22689 cx.assert_excerpts_with_selections(indoc! {"
22690 [EXCERPT]
22691 [FOLDED]
22692 [EXCERPT]
22693 a1
22694 b1
22695 ˇ[EXCERPT]
22696 [FOLDED]
22697 [EXCERPT]
22698 [FOLDED]
22699 "
22700 });
22701 cx.simulate_keystroke("up");
22702 cx.assert_excerpts_with_selections(indoc! {"
22703 [EXCERPT]
22704 [FOLDED]
22705 [EXCERPT]
22706 a1
22707 ˇb1
22708 [EXCERPT]
22709 [FOLDED]
22710 [EXCERPT]
22711 [FOLDED]
22712 "
22713 });
22714 cx.simulate_keystroke("up");
22715 cx.assert_excerpts_with_selections(indoc! {"
22716 [EXCERPT]
22717 [FOLDED]
22718 [EXCERPT]
22719 ˇa1
22720 b1
22721 [EXCERPT]
22722 [FOLDED]
22723 [EXCERPT]
22724 [FOLDED]
22725 "
22726 });
22727 for _ in 0..5 {
22728 cx.simulate_keystroke("up");
22729 cx.assert_excerpts_with_selections(indoc! {"
22730 [EXCERPT]
22731 ˇ[FOLDED]
22732 [EXCERPT]
22733 a1
22734 b1
22735 [EXCERPT]
22736 [FOLDED]
22737 [EXCERPT]
22738 [FOLDED]
22739 "
22740 });
22741 }
22742}
22743
22744#[gpui::test]
22745async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22746 init_test(cx, |_| {});
22747
22748 // Simple insertion
22749 assert_highlighted_edits(
22750 "Hello, world!",
22751 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22752 true,
22753 cx,
22754 |highlighted_edits, cx| {
22755 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22756 assert_eq!(highlighted_edits.highlights.len(), 1);
22757 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22758 assert_eq!(
22759 highlighted_edits.highlights[0].1.background_color,
22760 Some(cx.theme().status().created_background)
22761 );
22762 },
22763 )
22764 .await;
22765
22766 // Replacement
22767 assert_highlighted_edits(
22768 "This is a test.",
22769 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22770 false,
22771 cx,
22772 |highlighted_edits, cx| {
22773 assert_eq!(highlighted_edits.text, "That is a test.");
22774 assert_eq!(highlighted_edits.highlights.len(), 1);
22775 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22776 assert_eq!(
22777 highlighted_edits.highlights[0].1.background_color,
22778 Some(cx.theme().status().created_background)
22779 );
22780 },
22781 )
22782 .await;
22783
22784 // Multiple edits
22785 assert_highlighted_edits(
22786 "Hello, world!",
22787 vec![
22788 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22789 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22790 ],
22791 false,
22792 cx,
22793 |highlighted_edits, cx| {
22794 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22795 assert_eq!(highlighted_edits.highlights.len(), 2);
22796 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22797 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22798 assert_eq!(
22799 highlighted_edits.highlights[0].1.background_color,
22800 Some(cx.theme().status().created_background)
22801 );
22802 assert_eq!(
22803 highlighted_edits.highlights[1].1.background_color,
22804 Some(cx.theme().status().created_background)
22805 );
22806 },
22807 )
22808 .await;
22809
22810 // Multiple lines with edits
22811 assert_highlighted_edits(
22812 "First line\nSecond line\nThird line\nFourth line",
22813 vec![
22814 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22815 (
22816 Point::new(2, 0)..Point::new(2, 10),
22817 "New third line".to_string(),
22818 ),
22819 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22820 ],
22821 false,
22822 cx,
22823 |highlighted_edits, cx| {
22824 assert_eq!(
22825 highlighted_edits.text,
22826 "Second modified\nNew third line\nFourth updated line"
22827 );
22828 assert_eq!(highlighted_edits.highlights.len(), 3);
22829 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22830 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22831 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22832 for highlight in &highlighted_edits.highlights {
22833 assert_eq!(
22834 highlight.1.background_color,
22835 Some(cx.theme().status().created_background)
22836 );
22837 }
22838 },
22839 )
22840 .await;
22841}
22842
22843#[gpui::test]
22844async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22845 init_test(cx, |_| {});
22846
22847 // Deletion
22848 assert_highlighted_edits(
22849 "Hello, world!",
22850 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22851 true,
22852 cx,
22853 |highlighted_edits, cx| {
22854 assert_eq!(highlighted_edits.text, "Hello, world!");
22855 assert_eq!(highlighted_edits.highlights.len(), 1);
22856 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22857 assert_eq!(
22858 highlighted_edits.highlights[0].1.background_color,
22859 Some(cx.theme().status().deleted_background)
22860 );
22861 },
22862 )
22863 .await;
22864
22865 // Insertion
22866 assert_highlighted_edits(
22867 "Hello, world!",
22868 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22869 true,
22870 cx,
22871 |highlighted_edits, cx| {
22872 assert_eq!(highlighted_edits.highlights.len(), 1);
22873 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22874 assert_eq!(
22875 highlighted_edits.highlights[0].1.background_color,
22876 Some(cx.theme().status().created_background)
22877 );
22878 },
22879 )
22880 .await;
22881}
22882
22883async fn assert_highlighted_edits(
22884 text: &str,
22885 edits: Vec<(Range<Point>, String)>,
22886 include_deletions: bool,
22887 cx: &mut TestAppContext,
22888 assertion_fn: impl Fn(HighlightedText, &App),
22889) {
22890 let window = cx.add_window(|window, cx| {
22891 let buffer = MultiBuffer::build_simple(text, cx);
22892 Editor::new(EditorMode::full(), buffer, None, window, cx)
22893 });
22894 let cx = &mut VisualTestContext::from_window(*window, cx);
22895
22896 let (buffer, snapshot) = window
22897 .update(cx, |editor, _window, cx| {
22898 (
22899 editor.buffer().clone(),
22900 editor.buffer().read(cx).snapshot(cx),
22901 )
22902 })
22903 .unwrap();
22904
22905 let edits = edits
22906 .into_iter()
22907 .map(|(range, edit)| {
22908 (
22909 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22910 edit,
22911 )
22912 })
22913 .collect::<Vec<_>>();
22914
22915 let text_anchor_edits = edits
22916 .clone()
22917 .into_iter()
22918 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22919 .collect::<Vec<_>>();
22920
22921 let edit_preview = window
22922 .update(cx, |_, _window, cx| {
22923 buffer
22924 .read(cx)
22925 .as_singleton()
22926 .unwrap()
22927 .read(cx)
22928 .preview_edits(text_anchor_edits.into(), cx)
22929 })
22930 .unwrap()
22931 .await;
22932
22933 cx.update(|_window, cx| {
22934 let highlighted_edits = edit_prediction_edit_text(
22935 snapshot.as_singleton().unwrap().2,
22936 &edits,
22937 &edit_preview,
22938 include_deletions,
22939 cx,
22940 );
22941 assertion_fn(highlighted_edits, cx)
22942 });
22943}
22944
22945#[track_caller]
22946fn assert_breakpoint(
22947 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22948 path: &Arc<Path>,
22949 expected: Vec<(u32, Breakpoint)>,
22950) {
22951 if expected.is_empty() {
22952 assert!(!breakpoints.contains_key(path), "{}", path.display());
22953 } else {
22954 let mut breakpoint = breakpoints
22955 .get(path)
22956 .unwrap()
22957 .iter()
22958 .map(|breakpoint| {
22959 (
22960 breakpoint.row,
22961 Breakpoint {
22962 message: breakpoint.message.clone(),
22963 state: breakpoint.state,
22964 condition: breakpoint.condition.clone(),
22965 hit_condition: breakpoint.hit_condition.clone(),
22966 },
22967 )
22968 })
22969 .collect::<Vec<_>>();
22970
22971 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22972
22973 assert_eq!(expected, breakpoint);
22974 }
22975}
22976
22977fn add_log_breakpoint_at_cursor(
22978 editor: &mut Editor,
22979 log_message: &str,
22980 window: &mut Window,
22981 cx: &mut Context<Editor>,
22982) {
22983 let (anchor, bp) = editor
22984 .breakpoints_at_cursors(window, cx)
22985 .first()
22986 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22987 .unwrap_or_else(|| {
22988 let snapshot = editor.snapshot(window, cx);
22989 let cursor_position: Point =
22990 editor.selections.newest(&snapshot.display_snapshot).head();
22991
22992 let breakpoint_position = snapshot
22993 .buffer_snapshot()
22994 .anchor_before(Point::new(cursor_position.row, 0));
22995
22996 (breakpoint_position, Breakpoint::new_log(log_message))
22997 });
22998
22999 editor.edit_breakpoint_at_anchor(
23000 anchor,
23001 bp,
23002 BreakpointEditAction::EditLogMessage(log_message.into()),
23003 cx,
23004 );
23005}
23006
23007#[gpui::test]
23008async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23009 init_test(cx, |_| {});
23010
23011 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23012 let fs = FakeFs::new(cx.executor());
23013 fs.insert_tree(
23014 path!("/a"),
23015 json!({
23016 "main.rs": sample_text,
23017 }),
23018 )
23019 .await;
23020 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23021 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23022 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23023
23024 let fs = FakeFs::new(cx.executor());
23025 fs.insert_tree(
23026 path!("/a"),
23027 json!({
23028 "main.rs": sample_text,
23029 }),
23030 )
23031 .await;
23032 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23033 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23034 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23035 let worktree_id = workspace
23036 .update(cx, |workspace, _window, cx| {
23037 workspace.project().update(cx, |project, cx| {
23038 project.worktrees(cx).next().unwrap().read(cx).id()
23039 })
23040 })
23041 .unwrap();
23042
23043 let buffer = project
23044 .update(cx, |project, cx| {
23045 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23046 })
23047 .await
23048 .unwrap();
23049
23050 let (editor, cx) = cx.add_window_view(|window, cx| {
23051 Editor::new(
23052 EditorMode::full(),
23053 MultiBuffer::build_from_buffer(buffer, cx),
23054 Some(project.clone()),
23055 window,
23056 cx,
23057 )
23058 });
23059
23060 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23061 let abs_path = project.read_with(cx, |project, cx| {
23062 project
23063 .absolute_path(&project_path, cx)
23064 .map(Arc::from)
23065 .unwrap()
23066 });
23067
23068 // assert we can add breakpoint on the first line
23069 editor.update_in(cx, |editor, window, cx| {
23070 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23071 editor.move_to_end(&MoveToEnd, window, cx);
23072 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23073 });
23074
23075 let breakpoints = editor.update(cx, |editor, cx| {
23076 editor
23077 .breakpoint_store()
23078 .as_ref()
23079 .unwrap()
23080 .read(cx)
23081 .all_source_breakpoints(cx)
23082 });
23083
23084 assert_eq!(1, breakpoints.len());
23085 assert_breakpoint(
23086 &breakpoints,
23087 &abs_path,
23088 vec![
23089 (0, Breakpoint::new_standard()),
23090 (3, Breakpoint::new_standard()),
23091 ],
23092 );
23093
23094 editor.update_in(cx, |editor, window, cx| {
23095 editor.move_to_beginning(&MoveToBeginning, window, cx);
23096 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23097 });
23098
23099 let breakpoints = editor.update(cx, |editor, cx| {
23100 editor
23101 .breakpoint_store()
23102 .as_ref()
23103 .unwrap()
23104 .read(cx)
23105 .all_source_breakpoints(cx)
23106 });
23107
23108 assert_eq!(1, breakpoints.len());
23109 assert_breakpoint(
23110 &breakpoints,
23111 &abs_path,
23112 vec![(3, Breakpoint::new_standard())],
23113 );
23114
23115 editor.update_in(cx, |editor, window, cx| {
23116 editor.move_to_end(&MoveToEnd, window, cx);
23117 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23118 });
23119
23120 let breakpoints = editor.update(cx, |editor, cx| {
23121 editor
23122 .breakpoint_store()
23123 .as_ref()
23124 .unwrap()
23125 .read(cx)
23126 .all_source_breakpoints(cx)
23127 });
23128
23129 assert_eq!(0, breakpoints.len());
23130 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23131}
23132
23133#[gpui::test]
23134async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23135 init_test(cx, |_| {});
23136
23137 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23138
23139 let fs = FakeFs::new(cx.executor());
23140 fs.insert_tree(
23141 path!("/a"),
23142 json!({
23143 "main.rs": sample_text,
23144 }),
23145 )
23146 .await;
23147 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23148 let (workspace, cx) =
23149 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23150
23151 let worktree_id = workspace.update(cx, |workspace, cx| {
23152 workspace.project().update(cx, |project, cx| {
23153 project.worktrees(cx).next().unwrap().read(cx).id()
23154 })
23155 });
23156
23157 let buffer = project
23158 .update(cx, |project, cx| {
23159 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23160 })
23161 .await
23162 .unwrap();
23163
23164 let (editor, cx) = cx.add_window_view(|window, cx| {
23165 Editor::new(
23166 EditorMode::full(),
23167 MultiBuffer::build_from_buffer(buffer, cx),
23168 Some(project.clone()),
23169 window,
23170 cx,
23171 )
23172 });
23173
23174 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23175 let abs_path = project.read_with(cx, |project, cx| {
23176 project
23177 .absolute_path(&project_path, cx)
23178 .map(Arc::from)
23179 .unwrap()
23180 });
23181
23182 editor.update_in(cx, |editor, window, cx| {
23183 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23184 });
23185
23186 let breakpoints = editor.update(cx, |editor, cx| {
23187 editor
23188 .breakpoint_store()
23189 .as_ref()
23190 .unwrap()
23191 .read(cx)
23192 .all_source_breakpoints(cx)
23193 });
23194
23195 assert_breakpoint(
23196 &breakpoints,
23197 &abs_path,
23198 vec![(0, Breakpoint::new_log("hello world"))],
23199 );
23200
23201 // Removing a log message from a log breakpoint should remove it
23202 editor.update_in(cx, |editor, window, cx| {
23203 add_log_breakpoint_at_cursor(editor, "", window, cx);
23204 });
23205
23206 let breakpoints = editor.update(cx, |editor, cx| {
23207 editor
23208 .breakpoint_store()
23209 .as_ref()
23210 .unwrap()
23211 .read(cx)
23212 .all_source_breakpoints(cx)
23213 });
23214
23215 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23216
23217 editor.update_in(cx, |editor, window, cx| {
23218 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23219 editor.move_to_end(&MoveToEnd, window, cx);
23220 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23221 // Not adding a log message to a standard breakpoint shouldn't remove it
23222 add_log_breakpoint_at_cursor(editor, "", window, cx);
23223 });
23224
23225 let breakpoints = editor.update(cx, |editor, cx| {
23226 editor
23227 .breakpoint_store()
23228 .as_ref()
23229 .unwrap()
23230 .read(cx)
23231 .all_source_breakpoints(cx)
23232 });
23233
23234 assert_breakpoint(
23235 &breakpoints,
23236 &abs_path,
23237 vec![
23238 (0, Breakpoint::new_standard()),
23239 (3, Breakpoint::new_standard()),
23240 ],
23241 );
23242
23243 editor.update_in(cx, |editor, window, cx| {
23244 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23245 });
23246
23247 let breakpoints = editor.update(cx, |editor, cx| {
23248 editor
23249 .breakpoint_store()
23250 .as_ref()
23251 .unwrap()
23252 .read(cx)
23253 .all_source_breakpoints(cx)
23254 });
23255
23256 assert_breakpoint(
23257 &breakpoints,
23258 &abs_path,
23259 vec![
23260 (0, Breakpoint::new_standard()),
23261 (3, Breakpoint::new_log("hello world")),
23262 ],
23263 );
23264
23265 editor.update_in(cx, |editor, window, cx| {
23266 add_log_breakpoint_at_cursor(editor, "hello Earth!!", 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 assert_breakpoint(
23279 &breakpoints,
23280 &abs_path,
23281 vec![
23282 (0, Breakpoint::new_standard()),
23283 (3, Breakpoint::new_log("hello Earth!!")),
23284 ],
23285 );
23286}
23287
23288/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23289/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23290/// or when breakpoints were placed out of order. This tests for a regression too
23291#[gpui::test]
23292async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23293 init_test(cx, |_| {});
23294
23295 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23296 let fs = FakeFs::new(cx.executor());
23297 fs.insert_tree(
23298 path!("/a"),
23299 json!({
23300 "main.rs": sample_text,
23301 }),
23302 )
23303 .await;
23304 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23305 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23306 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23307
23308 let fs = FakeFs::new(cx.executor());
23309 fs.insert_tree(
23310 path!("/a"),
23311 json!({
23312 "main.rs": sample_text,
23313 }),
23314 )
23315 .await;
23316 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23317 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23318 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23319 let worktree_id = workspace
23320 .update(cx, |workspace, _window, cx| {
23321 workspace.project().update(cx, |project, cx| {
23322 project.worktrees(cx).next().unwrap().read(cx).id()
23323 })
23324 })
23325 .unwrap();
23326
23327 let buffer = project
23328 .update(cx, |project, cx| {
23329 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23330 })
23331 .await
23332 .unwrap();
23333
23334 let (editor, cx) = cx.add_window_view(|window, cx| {
23335 Editor::new(
23336 EditorMode::full(),
23337 MultiBuffer::build_from_buffer(buffer, cx),
23338 Some(project.clone()),
23339 window,
23340 cx,
23341 )
23342 });
23343
23344 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23345 let abs_path = project.read_with(cx, |project, cx| {
23346 project
23347 .absolute_path(&project_path, cx)
23348 .map(Arc::from)
23349 .unwrap()
23350 });
23351
23352 // assert we can add breakpoint on the first line
23353 editor.update_in(cx, |editor, window, cx| {
23354 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23355 editor.move_to_end(&MoveToEnd, window, cx);
23356 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23357 editor.move_up(&MoveUp, window, cx);
23358 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23359 });
23360
23361 let breakpoints = editor.update(cx, |editor, cx| {
23362 editor
23363 .breakpoint_store()
23364 .as_ref()
23365 .unwrap()
23366 .read(cx)
23367 .all_source_breakpoints(cx)
23368 });
23369
23370 assert_eq!(1, breakpoints.len());
23371 assert_breakpoint(
23372 &breakpoints,
23373 &abs_path,
23374 vec![
23375 (0, Breakpoint::new_standard()),
23376 (2, Breakpoint::new_standard()),
23377 (3, Breakpoint::new_standard()),
23378 ],
23379 );
23380
23381 editor.update_in(cx, |editor, window, cx| {
23382 editor.move_to_beginning(&MoveToBeginning, window, cx);
23383 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23384 editor.move_to_end(&MoveToEnd, window, cx);
23385 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23386 // Disabling a breakpoint that doesn't exist should do nothing
23387 editor.move_up(&MoveUp, window, cx);
23388 editor.move_up(&MoveUp, window, cx);
23389 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23390 });
23391
23392 let breakpoints = editor.update(cx, |editor, cx| {
23393 editor
23394 .breakpoint_store()
23395 .as_ref()
23396 .unwrap()
23397 .read(cx)
23398 .all_source_breakpoints(cx)
23399 });
23400
23401 let disable_breakpoint = {
23402 let mut bp = Breakpoint::new_standard();
23403 bp.state = BreakpointState::Disabled;
23404 bp
23405 };
23406
23407 assert_eq!(1, breakpoints.len());
23408 assert_breakpoint(
23409 &breakpoints,
23410 &abs_path,
23411 vec![
23412 (0, disable_breakpoint.clone()),
23413 (2, Breakpoint::new_standard()),
23414 (3, disable_breakpoint.clone()),
23415 ],
23416 );
23417
23418 editor.update_in(cx, |editor, window, cx| {
23419 editor.move_to_beginning(&MoveToBeginning, window, cx);
23420 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23421 editor.move_to_end(&MoveToEnd, window, cx);
23422 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23423 editor.move_up(&MoveUp, window, cx);
23424 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23425 });
23426
23427 let breakpoints = editor.update(cx, |editor, cx| {
23428 editor
23429 .breakpoint_store()
23430 .as_ref()
23431 .unwrap()
23432 .read(cx)
23433 .all_source_breakpoints(cx)
23434 });
23435
23436 assert_eq!(1, breakpoints.len());
23437 assert_breakpoint(
23438 &breakpoints,
23439 &abs_path,
23440 vec![
23441 (0, Breakpoint::new_standard()),
23442 (2, disable_breakpoint),
23443 (3, Breakpoint::new_standard()),
23444 ],
23445 );
23446}
23447
23448#[gpui::test]
23449async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23450 init_test(cx, |_| {});
23451 let capabilities = lsp::ServerCapabilities {
23452 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23453 prepare_provider: Some(true),
23454 work_done_progress_options: Default::default(),
23455 })),
23456 ..Default::default()
23457 };
23458 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23459
23460 cx.set_state(indoc! {"
23461 struct Fˇoo {}
23462 "});
23463
23464 cx.update_editor(|editor, _, cx| {
23465 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23466 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23467 editor.highlight_background::<DocumentHighlightRead>(
23468 &[highlight_range],
23469 |theme| theme.colors().editor_document_highlight_read_background,
23470 cx,
23471 );
23472 });
23473
23474 let mut prepare_rename_handler = cx
23475 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23476 move |_, _, _| async move {
23477 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23478 start: lsp::Position {
23479 line: 0,
23480 character: 7,
23481 },
23482 end: lsp::Position {
23483 line: 0,
23484 character: 10,
23485 },
23486 })))
23487 },
23488 );
23489 let prepare_rename_task = cx
23490 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23491 .expect("Prepare rename was not started");
23492 prepare_rename_handler.next().await.unwrap();
23493 prepare_rename_task.await.expect("Prepare rename failed");
23494
23495 let mut rename_handler =
23496 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23497 let edit = lsp::TextEdit {
23498 range: lsp::Range {
23499 start: lsp::Position {
23500 line: 0,
23501 character: 7,
23502 },
23503 end: lsp::Position {
23504 line: 0,
23505 character: 10,
23506 },
23507 },
23508 new_text: "FooRenamed".to_string(),
23509 };
23510 Ok(Some(lsp::WorkspaceEdit::new(
23511 // Specify the same edit twice
23512 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23513 )))
23514 });
23515 let rename_task = cx
23516 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23517 .expect("Confirm rename was not started");
23518 rename_handler.next().await.unwrap();
23519 rename_task.await.expect("Confirm rename failed");
23520 cx.run_until_parked();
23521
23522 // Despite two edits, only one is actually applied as those are identical
23523 cx.assert_editor_state(indoc! {"
23524 struct FooRenamedˇ {}
23525 "});
23526}
23527
23528#[gpui::test]
23529async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23530 init_test(cx, |_| {});
23531 // These capabilities indicate that the server does not support prepare rename.
23532 let capabilities = lsp::ServerCapabilities {
23533 rename_provider: Some(lsp::OneOf::Left(true)),
23534 ..Default::default()
23535 };
23536 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23537
23538 cx.set_state(indoc! {"
23539 struct Fˇoo {}
23540 "});
23541
23542 cx.update_editor(|editor, _window, cx| {
23543 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23544 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23545 editor.highlight_background::<DocumentHighlightRead>(
23546 &[highlight_range],
23547 |theme| theme.colors().editor_document_highlight_read_background,
23548 cx,
23549 );
23550 });
23551
23552 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23553 .expect("Prepare rename was not started")
23554 .await
23555 .expect("Prepare rename failed");
23556
23557 let mut rename_handler =
23558 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23559 let edit = lsp::TextEdit {
23560 range: lsp::Range {
23561 start: lsp::Position {
23562 line: 0,
23563 character: 7,
23564 },
23565 end: lsp::Position {
23566 line: 0,
23567 character: 10,
23568 },
23569 },
23570 new_text: "FooRenamed".to_string(),
23571 };
23572 Ok(Some(lsp::WorkspaceEdit::new(
23573 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23574 )))
23575 });
23576 let rename_task = cx
23577 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23578 .expect("Confirm rename was not started");
23579 rename_handler.next().await.unwrap();
23580 rename_task.await.expect("Confirm rename failed");
23581 cx.run_until_parked();
23582
23583 // Correct range is renamed, as `surrounding_word` is used to find it.
23584 cx.assert_editor_state(indoc! {"
23585 struct FooRenamedˇ {}
23586 "});
23587}
23588
23589#[gpui::test]
23590async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23591 init_test(cx, |_| {});
23592 let mut cx = EditorTestContext::new(cx).await;
23593
23594 let language = Arc::new(
23595 Language::new(
23596 LanguageConfig::default(),
23597 Some(tree_sitter_html::LANGUAGE.into()),
23598 )
23599 .with_brackets_query(
23600 r#"
23601 ("<" @open "/>" @close)
23602 ("</" @open ">" @close)
23603 ("<" @open ">" @close)
23604 ("\"" @open "\"" @close)
23605 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23606 "#,
23607 )
23608 .unwrap(),
23609 );
23610 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23611
23612 cx.set_state(indoc! {"
23613 <span>ˇ</span>
23614 "});
23615 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23616 cx.assert_editor_state(indoc! {"
23617 <span>
23618 ˇ
23619 </span>
23620 "});
23621
23622 cx.set_state(indoc! {"
23623 <span><span></span>ˇ</span>
23624 "});
23625 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23626 cx.assert_editor_state(indoc! {"
23627 <span><span></span>
23628 ˇ</span>
23629 "});
23630
23631 cx.set_state(indoc! {"
23632 <span>ˇ
23633 </span>
23634 "});
23635 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23636 cx.assert_editor_state(indoc! {"
23637 <span>
23638 ˇ
23639 </span>
23640 "});
23641}
23642
23643#[gpui::test(iterations = 10)]
23644async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23645 init_test(cx, |_| {});
23646
23647 let fs = FakeFs::new(cx.executor());
23648 fs.insert_tree(
23649 path!("/dir"),
23650 json!({
23651 "a.ts": "a",
23652 }),
23653 )
23654 .await;
23655
23656 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23657 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23658 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23659
23660 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23661 language_registry.add(Arc::new(Language::new(
23662 LanguageConfig {
23663 name: "TypeScript".into(),
23664 matcher: LanguageMatcher {
23665 path_suffixes: vec!["ts".to_string()],
23666 ..Default::default()
23667 },
23668 ..Default::default()
23669 },
23670 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23671 )));
23672 let mut fake_language_servers = language_registry.register_fake_lsp(
23673 "TypeScript",
23674 FakeLspAdapter {
23675 capabilities: lsp::ServerCapabilities {
23676 code_lens_provider: Some(lsp::CodeLensOptions {
23677 resolve_provider: Some(true),
23678 }),
23679 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23680 commands: vec!["_the/command".to_string()],
23681 ..lsp::ExecuteCommandOptions::default()
23682 }),
23683 ..lsp::ServerCapabilities::default()
23684 },
23685 ..FakeLspAdapter::default()
23686 },
23687 );
23688
23689 let editor = workspace
23690 .update(cx, |workspace, window, cx| {
23691 workspace.open_abs_path(
23692 PathBuf::from(path!("/dir/a.ts")),
23693 OpenOptions::default(),
23694 window,
23695 cx,
23696 )
23697 })
23698 .unwrap()
23699 .await
23700 .unwrap()
23701 .downcast::<Editor>()
23702 .unwrap();
23703 cx.executor().run_until_parked();
23704
23705 let fake_server = fake_language_servers.next().await.unwrap();
23706
23707 let buffer = editor.update(cx, |editor, cx| {
23708 editor
23709 .buffer()
23710 .read(cx)
23711 .as_singleton()
23712 .expect("have opened a single file by path")
23713 });
23714
23715 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23716 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23717 drop(buffer_snapshot);
23718 let actions = cx
23719 .update_window(*workspace, |_, window, cx| {
23720 project.code_actions(&buffer, anchor..anchor, window, cx)
23721 })
23722 .unwrap();
23723
23724 fake_server
23725 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23726 Ok(Some(vec![
23727 lsp::CodeLens {
23728 range: lsp::Range::default(),
23729 command: Some(lsp::Command {
23730 title: "Code lens command".to_owned(),
23731 command: "_the/command".to_owned(),
23732 arguments: None,
23733 }),
23734 data: None,
23735 },
23736 lsp::CodeLens {
23737 range: lsp::Range::default(),
23738 command: Some(lsp::Command {
23739 title: "Command not in capabilities".to_owned(),
23740 command: "not in capabilities".to_owned(),
23741 arguments: None,
23742 }),
23743 data: None,
23744 },
23745 lsp::CodeLens {
23746 range: lsp::Range {
23747 start: lsp::Position {
23748 line: 1,
23749 character: 1,
23750 },
23751 end: lsp::Position {
23752 line: 1,
23753 character: 1,
23754 },
23755 },
23756 command: Some(lsp::Command {
23757 title: "Command not in range".to_owned(),
23758 command: "_the/command".to_owned(),
23759 arguments: None,
23760 }),
23761 data: None,
23762 },
23763 ]))
23764 })
23765 .next()
23766 .await;
23767
23768 let actions = actions.await.unwrap();
23769 assert_eq!(
23770 actions.len(),
23771 1,
23772 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23773 );
23774 let action = actions[0].clone();
23775 let apply = project.update(cx, |project, cx| {
23776 project.apply_code_action(buffer.clone(), action, true, cx)
23777 });
23778
23779 // Resolving the code action does not populate its edits. In absence of
23780 // edits, we must execute the given command.
23781 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23782 |mut lens, _| async move {
23783 let lens_command = lens.command.as_mut().expect("should have a command");
23784 assert_eq!(lens_command.title, "Code lens command");
23785 lens_command.arguments = Some(vec![json!("the-argument")]);
23786 Ok(lens)
23787 },
23788 );
23789
23790 // While executing the command, the language server sends the editor
23791 // a `workspaceEdit` request.
23792 fake_server
23793 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23794 let fake = fake_server.clone();
23795 move |params, _| {
23796 assert_eq!(params.command, "_the/command");
23797 let fake = fake.clone();
23798 async move {
23799 fake.server
23800 .request::<lsp::request::ApplyWorkspaceEdit>(
23801 lsp::ApplyWorkspaceEditParams {
23802 label: None,
23803 edit: lsp::WorkspaceEdit {
23804 changes: Some(
23805 [(
23806 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23807 vec![lsp::TextEdit {
23808 range: lsp::Range::new(
23809 lsp::Position::new(0, 0),
23810 lsp::Position::new(0, 0),
23811 ),
23812 new_text: "X".into(),
23813 }],
23814 )]
23815 .into_iter()
23816 .collect(),
23817 ),
23818 ..lsp::WorkspaceEdit::default()
23819 },
23820 },
23821 )
23822 .await
23823 .into_response()
23824 .unwrap();
23825 Ok(Some(json!(null)))
23826 }
23827 }
23828 })
23829 .next()
23830 .await;
23831
23832 // Applying the code lens command returns a project transaction containing the edits
23833 // sent by the language server in its `workspaceEdit` request.
23834 let transaction = apply.await.unwrap();
23835 assert!(transaction.0.contains_key(&buffer));
23836 buffer.update(cx, |buffer, cx| {
23837 assert_eq!(buffer.text(), "Xa");
23838 buffer.undo(cx);
23839 assert_eq!(buffer.text(), "a");
23840 });
23841
23842 let actions_after_edits = cx
23843 .update_window(*workspace, |_, window, cx| {
23844 project.code_actions(&buffer, anchor..anchor, window, cx)
23845 })
23846 .unwrap()
23847 .await
23848 .unwrap();
23849 assert_eq!(
23850 actions, actions_after_edits,
23851 "For the same selection, same code lens actions should be returned"
23852 );
23853
23854 let _responses =
23855 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23856 panic!("No more code lens requests are expected");
23857 });
23858 editor.update_in(cx, |editor, window, cx| {
23859 editor.select_all(&SelectAll, window, cx);
23860 });
23861 cx.executor().run_until_parked();
23862 let new_actions = cx
23863 .update_window(*workspace, |_, window, cx| {
23864 project.code_actions(&buffer, anchor..anchor, window, cx)
23865 })
23866 .unwrap()
23867 .await
23868 .unwrap();
23869 assert_eq!(
23870 actions, new_actions,
23871 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23872 );
23873}
23874
23875#[gpui::test]
23876async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23877 init_test(cx, |_| {});
23878
23879 let fs = FakeFs::new(cx.executor());
23880 let main_text = r#"fn main() {
23881println!("1");
23882println!("2");
23883println!("3");
23884println!("4");
23885println!("5");
23886}"#;
23887 let lib_text = "mod foo {}";
23888 fs.insert_tree(
23889 path!("/a"),
23890 json!({
23891 "lib.rs": lib_text,
23892 "main.rs": main_text,
23893 }),
23894 )
23895 .await;
23896
23897 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23898 let (workspace, cx) =
23899 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23900 let worktree_id = workspace.update(cx, |workspace, cx| {
23901 workspace.project().update(cx, |project, cx| {
23902 project.worktrees(cx).next().unwrap().read(cx).id()
23903 })
23904 });
23905
23906 let expected_ranges = vec![
23907 Point::new(0, 0)..Point::new(0, 0),
23908 Point::new(1, 0)..Point::new(1, 1),
23909 Point::new(2, 0)..Point::new(2, 2),
23910 Point::new(3, 0)..Point::new(3, 3),
23911 ];
23912
23913 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23914 let editor_1 = workspace
23915 .update_in(cx, |workspace, window, cx| {
23916 workspace.open_path(
23917 (worktree_id, rel_path("main.rs")),
23918 Some(pane_1.downgrade()),
23919 true,
23920 window,
23921 cx,
23922 )
23923 })
23924 .unwrap()
23925 .await
23926 .downcast::<Editor>()
23927 .unwrap();
23928 pane_1.update(cx, |pane, cx| {
23929 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23930 open_editor.update(cx, |editor, cx| {
23931 assert_eq!(
23932 editor.display_text(cx),
23933 main_text,
23934 "Original main.rs text on initial open",
23935 );
23936 assert_eq!(
23937 editor
23938 .selections
23939 .all::<Point>(&editor.display_snapshot(cx))
23940 .into_iter()
23941 .map(|s| s.range())
23942 .collect::<Vec<_>>(),
23943 vec![Point::zero()..Point::zero()],
23944 "Default selections on initial open",
23945 );
23946 })
23947 });
23948 editor_1.update_in(cx, |editor, window, cx| {
23949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23950 s.select_ranges(expected_ranges.clone());
23951 });
23952 });
23953
23954 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23955 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23956 });
23957 let editor_2 = workspace
23958 .update_in(cx, |workspace, window, cx| {
23959 workspace.open_path(
23960 (worktree_id, rel_path("main.rs")),
23961 Some(pane_2.downgrade()),
23962 true,
23963 window,
23964 cx,
23965 )
23966 })
23967 .unwrap()
23968 .await
23969 .downcast::<Editor>()
23970 .unwrap();
23971 pane_2.update(cx, |pane, cx| {
23972 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23973 open_editor.update(cx, |editor, cx| {
23974 assert_eq!(
23975 editor.display_text(cx),
23976 main_text,
23977 "Original main.rs text on initial open in another panel",
23978 );
23979 assert_eq!(
23980 editor
23981 .selections
23982 .all::<Point>(&editor.display_snapshot(cx))
23983 .into_iter()
23984 .map(|s| s.range())
23985 .collect::<Vec<_>>(),
23986 vec![Point::zero()..Point::zero()],
23987 "Default selections on initial open in another panel",
23988 );
23989 })
23990 });
23991
23992 editor_2.update_in(cx, |editor, window, cx| {
23993 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23994 });
23995
23996 let _other_editor_1 = workspace
23997 .update_in(cx, |workspace, window, cx| {
23998 workspace.open_path(
23999 (worktree_id, rel_path("lib.rs")),
24000 Some(pane_1.downgrade()),
24001 true,
24002 window,
24003 cx,
24004 )
24005 })
24006 .unwrap()
24007 .await
24008 .downcast::<Editor>()
24009 .unwrap();
24010 pane_1
24011 .update_in(cx, |pane, window, cx| {
24012 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24013 })
24014 .await
24015 .unwrap();
24016 drop(editor_1);
24017 pane_1.update(cx, |pane, cx| {
24018 pane.active_item()
24019 .unwrap()
24020 .downcast::<Editor>()
24021 .unwrap()
24022 .update(cx, |editor, cx| {
24023 assert_eq!(
24024 editor.display_text(cx),
24025 lib_text,
24026 "Other file should be open and active",
24027 );
24028 });
24029 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24030 });
24031
24032 let _other_editor_2 = workspace
24033 .update_in(cx, |workspace, window, cx| {
24034 workspace.open_path(
24035 (worktree_id, rel_path("lib.rs")),
24036 Some(pane_2.downgrade()),
24037 true,
24038 window,
24039 cx,
24040 )
24041 })
24042 .unwrap()
24043 .await
24044 .downcast::<Editor>()
24045 .unwrap();
24046 pane_2
24047 .update_in(cx, |pane, window, cx| {
24048 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24049 })
24050 .await
24051 .unwrap();
24052 drop(editor_2);
24053 pane_2.update(cx, |pane, cx| {
24054 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24055 open_editor.update(cx, |editor, cx| {
24056 assert_eq!(
24057 editor.display_text(cx),
24058 lib_text,
24059 "Other file should be open and active in another panel too",
24060 );
24061 });
24062 assert_eq!(
24063 pane.items().count(),
24064 1,
24065 "No other editors should be open in another pane",
24066 );
24067 });
24068
24069 let _editor_1_reopened = workspace
24070 .update_in(cx, |workspace, window, cx| {
24071 workspace.open_path(
24072 (worktree_id, rel_path("main.rs")),
24073 Some(pane_1.downgrade()),
24074 true,
24075 window,
24076 cx,
24077 )
24078 })
24079 .unwrap()
24080 .await
24081 .downcast::<Editor>()
24082 .unwrap();
24083 let _editor_2_reopened = workspace
24084 .update_in(cx, |workspace, window, cx| {
24085 workspace.open_path(
24086 (worktree_id, rel_path("main.rs")),
24087 Some(pane_2.downgrade()),
24088 true,
24089 window,
24090 cx,
24091 )
24092 })
24093 .unwrap()
24094 .await
24095 .downcast::<Editor>()
24096 .unwrap();
24097 pane_1.update(cx, |pane, cx| {
24098 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24099 open_editor.update(cx, |editor, cx| {
24100 assert_eq!(
24101 editor.display_text(cx),
24102 main_text,
24103 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24104 );
24105 assert_eq!(
24106 editor
24107 .selections
24108 .all::<Point>(&editor.display_snapshot(cx))
24109 .into_iter()
24110 .map(|s| s.range())
24111 .collect::<Vec<_>>(),
24112 expected_ranges,
24113 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24114 );
24115 })
24116 });
24117 pane_2.update(cx, |pane, cx| {
24118 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24119 open_editor.update(cx, |editor, cx| {
24120 assert_eq!(
24121 editor.display_text(cx),
24122 r#"fn main() {
24123⋯rintln!("1");
24124⋯intln!("2");
24125⋯ntln!("3");
24126println!("4");
24127println!("5");
24128}"#,
24129 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24130 );
24131 assert_eq!(
24132 editor
24133 .selections
24134 .all::<Point>(&editor.display_snapshot(cx))
24135 .into_iter()
24136 .map(|s| s.range())
24137 .collect::<Vec<_>>(),
24138 vec![Point::zero()..Point::zero()],
24139 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24140 );
24141 })
24142 });
24143}
24144
24145#[gpui::test]
24146async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24147 init_test(cx, |_| {});
24148
24149 let fs = FakeFs::new(cx.executor());
24150 let main_text = r#"fn main() {
24151println!("1");
24152println!("2");
24153println!("3");
24154println!("4");
24155println!("5");
24156}"#;
24157 let lib_text = "mod foo {}";
24158 fs.insert_tree(
24159 path!("/a"),
24160 json!({
24161 "lib.rs": lib_text,
24162 "main.rs": main_text,
24163 }),
24164 )
24165 .await;
24166
24167 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24168 let (workspace, cx) =
24169 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24170 let worktree_id = workspace.update(cx, |workspace, cx| {
24171 workspace.project().update(cx, |project, cx| {
24172 project.worktrees(cx).next().unwrap().read(cx).id()
24173 })
24174 });
24175
24176 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24177 let editor = workspace
24178 .update_in(cx, |workspace, window, cx| {
24179 workspace.open_path(
24180 (worktree_id, rel_path("main.rs")),
24181 Some(pane.downgrade()),
24182 true,
24183 window,
24184 cx,
24185 )
24186 })
24187 .unwrap()
24188 .await
24189 .downcast::<Editor>()
24190 .unwrap();
24191 pane.update(cx, |pane, cx| {
24192 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24193 open_editor.update(cx, |editor, cx| {
24194 assert_eq!(
24195 editor.display_text(cx),
24196 main_text,
24197 "Original main.rs text on initial open",
24198 );
24199 })
24200 });
24201 editor.update_in(cx, |editor, window, cx| {
24202 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24203 });
24204
24205 cx.update_global(|store: &mut SettingsStore, cx| {
24206 store.update_user_settings(cx, |s| {
24207 s.workspace.restore_on_file_reopen = Some(false);
24208 });
24209 });
24210 editor.update_in(cx, |editor, window, cx| {
24211 editor.fold_ranges(
24212 vec![
24213 Point::new(1, 0)..Point::new(1, 1),
24214 Point::new(2, 0)..Point::new(2, 2),
24215 Point::new(3, 0)..Point::new(3, 3),
24216 ],
24217 false,
24218 window,
24219 cx,
24220 );
24221 });
24222 pane.update_in(cx, |pane, window, cx| {
24223 pane.close_all_items(&CloseAllItems::default(), window, cx)
24224 })
24225 .await
24226 .unwrap();
24227 pane.update(cx, |pane, _| {
24228 assert!(pane.active_item().is_none());
24229 });
24230 cx.update_global(|store: &mut SettingsStore, cx| {
24231 store.update_user_settings(cx, |s| {
24232 s.workspace.restore_on_file_reopen = Some(true);
24233 });
24234 });
24235
24236 let _editor_reopened = workspace
24237 .update_in(cx, |workspace, window, cx| {
24238 workspace.open_path(
24239 (worktree_id, rel_path("main.rs")),
24240 Some(pane.downgrade()),
24241 true,
24242 window,
24243 cx,
24244 )
24245 })
24246 .unwrap()
24247 .await
24248 .downcast::<Editor>()
24249 .unwrap();
24250 pane.update(cx, |pane, cx| {
24251 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24252 open_editor.update(cx, |editor, cx| {
24253 assert_eq!(
24254 editor.display_text(cx),
24255 main_text,
24256 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24257 );
24258 })
24259 });
24260}
24261
24262#[gpui::test]
24263async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24264 struct EmptyModalView {
24265 focus_handle: gpui::FocusHandle,
24266 }
24267 impl EventEmitter<DismissEvent> for EmptyModalView {}
24268 impl Render for EmptyModalView {
24269 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24270 div()
24271 }
24272 }
24273 impl Focusable for EmptyModalView {
24274 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24275 self.focus_handle.clone()
24276 }
24277 }
24278 impl workspace::ModalView for EmptyModalView {}
24279 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24280 EmptyModalView {
24281 focus_handle: cx.focus_handle(),
24282 }
24283 }
24284
24285 init_test(cx, |_| {});
24286
24287 let fs = FakeFs::new(cx.executor());
24288 let project = Project::test(fs, [], cx).await;
24289 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24290 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24291 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24292 let editor = cx.new_window_entity(|window, cx| {
24293 Editor::new(
24294 EditorMode::full(),
24295 buffer,
24296 Some(project.clone()),
24297 window,
24298 cx,
24299 )
24300 });
24301 workspace
24302 .update(cx, |workspace, window, cx| {
24303 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24304 })
24305 .unwrap();
24306 editor.update_in(cx, |editor, window, cx| {
24307 editor.open_context_menu(&OpenContextMenu, window, cx);
24308 assert!(editor.mouse_context_menu.is_some());
24309 });
24310 workspace
24311 .update(cx, |workspace, window, cx| {
24312 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24313 })
24314 .unwrap();
24315 cx.read(|cx| {
24316 assert!(editor.read(cx).mouse_context_menu.is_none());
24317 });
24318}
24319
24320fn set_linked_edit_ranges(
24321 opening: (Point, Point),
24322 closing: (Point, Point),
24323 editor: &mut Editor,
24324 cx: &mut Context<Editor>,
24325) {
24326 let Some((buffer, _)) = editor
24327 .buffer
24328 .read(cx)
24329 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24330 else {
24331 panic!("Failed to get buffer for selection position");
24332 };
24333 let buffer = buffer.read(cx);
24334 let buffer_id = buffer.remote_id();
24335 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24336 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24337 let mut linked_ranges = HashMap::default();
24338 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24339 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24340}
24341
24342#[gpui::test]
24343async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24344 init_test(cx, |_| {});
24345
24346 let fs = FakeFs::new(cx.executor());
24347 fs.insert_file(path!("/file.html"), Default::default())
24348 .await;
24349
24350 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24351
24352 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24353 let html_language = Arc::new(Language::new(
24354 LanguageConfig {
24355 name: "HTML".into(),
24356 matcher: LanguageMatcher {
24357 path_suffixes: vec!["html".to_string()],
24358 ..LanguageMatcher::default()
24359 },
24360 brackets: BracketPairConfig {
24361 pairs: vec![BracketPair {
24362 start: "<".into(),
24363 end: ">".into(),
24364 close: true,
24365 ..Default::default()
24366 }],
24367 ..Default::default()
24368 },
24369 ..Default::default()
24370 },
24371 Some(tree_sitter_html::LANGUAGE.into()),
24372 ));
24373 language_registry.add(html_language);
24374 let mut fake_servers = language_registry.register_fake_lsp(
24375 "HTML",
24376 FakeLspAdapter {
24377 capabilities: lsp::ServerCapabilities {
24378 completion_provider: Some(lsp::CompletionOptions {
24379 resolve_provider: Some(true),
24380 ..Default::default()
24381 }),
24382 ..Default::default()
24383 },
24384 ..Default::default()
24385 },
24386 );
24387
24388 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24389 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24390
24391 let worktree_id = workspace
24392 .update(cx, |workspace, _window, cx| {
24393 workspace.project().update(cx, |project, cx| {
24394 project.worktrees(cx).next().unwrap().read(cx).id()
24395 })
24396 })
24397 .unwrap();
24398 project
24399 .update(cx, |project, cx| {
24400 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24401 })
24402 .await
24403 .unwrap();
24404 let editor = workspace
24405 .update(cx, |workspace, window, cx| {
24406 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24407 })
24408 .unwrap()
24409 .await
24410 .unwrap()
24411 .downcast::<Editor>()
24412 .unwrap();
24413
24414 let fake_server = fake_servers.next().await.unwrap();
24415 editor.update_in(cx, |editor, window, cx| {
24416 editor.set_text("<ad></ad>", window, cx);
24417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24418 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24419 });
24420 set_linked_edit_ranges(
24421 (Point::new(0, 1), Point::new(0, 3)),
24422 (Point::new(0, 6), Point::new(0, 8)),
24423 editor,
24424 cx,
24425 );
24426 });
24427 let mut completion_handle =
24428 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24429 Ok(Some(lsp::CompletionResponse::Array(vec![
24430 lsp::CompletionItem {
24431 label: "head".to_string(),
24432 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24433 lsp::InsertReplaceEdit {
24434 new_text: "head".to_string(),
24435 insert: lsp::Range::new(
24436 lsp::Position::new(0, 1),
24437 lsp::Position::new(0, 3),
24438 ),
24439 replace: lsp::Range::new(
24440 lsp::Position::new(0, 1),
24441 lsp::Position::new(0, 3),
24442 ),
24443 },
24444 )),
24445 ..Default::default()
24446 },
24447 ])))
24448 });
24449 editor.update_in(cx, |editor, window, cx| {
24450 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24451 });
24452 cx.run_until_parked();
24453 completion_handle.next().await.unwrap();
24454 editor.update(cx, |editor, _| {
24455 assert!(
24456 editor.context_menu_visible(),
24457 "Completion menu should be visible"
24458 );
24459 });
24460 editor.update_in(cx, |editor, window, cx| {
24461 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24462 });
24463 cx.executor().run_until_parked();
24464 editor.update(cx, |editor, cx| {
24465 assert_eq!(editor.text(cx), "<head></head>");
24466 });
24467}
24468
24469#[gpui::test]
24470async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24471 init_test(cx, |_| {});
24472
24473 let mut cx = EditorTestContext::new(cx).await;
24474 let language = Arc::new(Language::new(
24475 LanguageConfig {
24476 name: "TSX".into(),
24477 matcher: LanguageMatcher {
24478 path_suffixes: vec!["tsx".to_string()],
24479 ..LanguageMatcher::default()
24480 },
24481 brackets: BracketPairConfig {
24482 pairs: vec![BracketPair {
24483 start: "<".into(),
24484 end: ">".into(),
24485 close: true,
24486 ..Default::default()
24487 }],
24488 ..Default::default()
24489 },
24490 linked_edit_characters: HashSet::from_iter(['.']),
24491 ..Default::default()
24492 },
24493 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24494 ));
24495 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24496
24497 // Test typing > does not extend linked pair
24498 cx.set_state("<divˇ<div></div>");
24499 cx.update_editor(|editor, _, cx| {
24500 set_linked_edit_ranges(
24501 (Point::new(0, 1), Point::new(0, 4)),
24502 (Point::new(0, 11), Point::new(0, 14)),
24503 editor,
24504 cx,
24505 );
24506 });
24507 cx.update_editor(|editor, window, cx| {
24508 editor.handle_input(">", window, cx);
24509 });
24510 cx.assert_editor_state("<div>ˇ<div></div>");
24511
24512 // Test typing . do extend linked pair
24513 cx.set_state("<Animatedˇ></Animated>");
24514 cx.update_editor(|editor, _, cx| {
24515 set_linked_edit_ranges(
24516 (Point::new(0, 1), Point::new(0, 9)),
24517 (Point::new(0, 12), Point::new(0, 20)),
24518 editor,
24519 cx,
24520 );
24521 });
24522 cx.update_editor(|editor, window, cx| {
24523 editor.handle_input(".", window, cx);
24524 });
24525 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24526 cx.update_editor(|editor, _, cx| {
24527 set_linked_edit_ranges(
24528 (Point::new(0, 1), Point::new(0, 10)),
24529 (Point::new(0, 13), Point::new(0, 21)),
24530 editor,
24531 cx,
24532 );
24533 });
24534 cx.update_editor(|editor, window, cx| {
24535 editor.handle_input("V", window, cx);
24536 });
24537 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24538}
24539
24540#[gpui::test]
24541async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24542 init_test(cx, |_| {});
24543
24544 let fs = FakeFs::new(cx.executor());
24545 fs.insert_tree(
24546 path!("/root"),
24547 json!({
24548 "a": {
24549 "main.rs": "fn main() {}",
24550 },
24551 "foo": {
24552 "bar": {
24553 "external_file.rs": "pub mod external {}",
24554 }
24555 }
24556 }),
24557 )
24558 .await;
24559
24560 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24561 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24562 language_registry.add(rust_lang());
24563 let _fake_servers = language_registry.register_fake_lsp(
24564 "Rust",
24565 FakeLspAdapter {
24566 ..FakeLspAdapter::default()
24567 },
24568 );
24569 let (workspace, cx) =
24570 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24571 let worktree_id = workspace.update(cx, |workspace, cx| {
24572 workspace.project().update(cx, |project, cx| {
24573 project.worktrees(cx).next().unwrap().read(cx).id()
24574 })
24575 });
24576
24577 let assert_language_servers_count =
24578 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24579 project.update(cx, |project, cx| {
24580 let current = project
24581 .lsp_store()
24582 .read(cx)
24583 .as_local()
24584 .unwrap()
24585 .language_servers
24586 .len();
24587 assert_eq!(expected, current, "{context}");
24588 });
24589 };
24590
24591 assert_language_servers_count(
24592 0,
24593 "No servers should be running before any file is open",
24594 cx,
24595 );
24596 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24597 let main_editor = workspace
24598 .update_in(cx, |workspace, window, cx| {
24599 workspace.open_path(
24600 (worktree_id, rel_path("main.rs")),
24601 Some(pane.downgrade()),
24602 true,
24603 window,
24604 cx,
24605 )
24606 })
24607 .unwrap()
24608 .await
24609 .downcast::<Editor>()
24610 .unwrap();
24611 pane.update(cx, |pane, cx| {
24612 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24613 open_editor.update(cx, |editor, cx| {
24614 assert_eq!(
24615 editor.display_text(cx),
24616 "fn main() {}",
24617 "Original main.rs text on initial open",
24618 );
24619 });
24620 assert_eq!(open_editor, main_editor);
24621 });
24622 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24623
24624 let external_editor = workspace
24625 .update_in(cx, |workspace, window, cx| {
24626 workspace.open_abs_path(
24627 PathBuf::from("/root/foo/bar/external_file.rs"),
24628 OpenOptions::default(),
24629 window,
24630 cx,
24631 )
24632 })
24633 .await
24634 .expect("opening external file")
24635 .downcast::<Editor>()
24636 .expect("downcasted external file's open element to editor");
24637 pane.update(cx, |pane, cx| {
24638 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24639 open_editor.update(cx, |editor, cx| {
24640 assert_eq!(
24641 editor.display_text(cx),
24642 "pub mod external {}",
24643 "External file is open now",
24644 );
24645 });
24646 assert_eq!(open_editor, external_editor);
24647 });
24648 assert_language_servers_count(
24649 1,
24650 "Second, external, *.rs file should join the existing server",
24651 cx,
24652 );
24653
24654 pane.update_in(cx, |pane, window, cx| {
24655 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24656 })
24657 .await
24658 .unwrap();
24659 pane.update_in(cx, |pane, window, cx| {
24660 pane.navigate_backward(&Default::default(), window, cx);
24661 });
24662 cx.run_until_parked();
24663 pane.update(cx, |pane, cx| {
24664 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24665 open_editor.update(cx, |editor, cx| {
24666 assert_eq!(
24667 editor.display_text(cx),
24668 "pub mod external {}",
24669 "External file is open now",
24670 );
24671 });
24672 });
24673 assert_language_servers_count(
24674 1,
24675 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24676 cx,
24677 );
24678
24679 cx.update(|_, cx| {
24680 workspace::reload(cx);
24681 });
24682 assert_language_servers_count(
24683 1,
24684 "After reloading the worktree with local and external files opened, only one project should be started",
24685 cx,
24686 );
24687}
24688
24689#[gpui::test]
24690async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24691 init_test(cx, |_| {});
24692
24693 let mut cx = EditorTestContext::new(cx).await;
24694 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24695 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24696
24697 // test cursor move to start of each line on tab
24698 // for `if`, `elif`, `else`, `while`, `with` and `for`
24699 cx.set_state(indoc! {"
24700 def main():
24701 ˇ for item in items:
24702 ˇ while item.active:
24703 ˇ if item.value > 10:
24704 ˇ continue
24705 ˇ elif item.value < 0:
24706 ˇ break
24707 ˇ else:
24708 ˇ with item.context() as ctx:
24709 ˇ yield count
24710 ˇ else:
24711 ˇ log('while else')
24712 ˇ else:
24713 ˇ log('for else')
24714 "});
24715 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24716 cx.assert_editor_state(indoc! {"
24717 def main():
24718 ˇfor item in items:
24719 ˇwhile item.active:
24720 ˇif item.value > 10:
24721 ˇcontinue
24722 ˇelif item.value < 0:
24723 ˇbreak
24724 ˇelse:
24725 ˇwith item.context() as ctx:
24726 ˇyield count
24727 ˇelse:
24728 ˇlog('while else')
24729 ˇelse:
24730 ˇlog('for else')
24731 "});
24732 // test relative indent is preserved when tab
24733 // for `if`, `elif`, `else`, `while`, `with` and `for`
24734 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24735 cx.assert_editor_state(indoc! {"
24736 def main():
24737 ˇfor item in items:
24738 ˇwhile item.active:
24739 ˇif item.value > 10:
24740 ˇcontinue
24741 ˇelif item.value < 0:
24742 ˇbreak
24743 ˇelse:
24744 ˇwith item.context() as ctx:
24745 ˇyield count
24746 ˇelse:
24747 ˇlog('while else')
24748 ˇelse:
24749 ˇlog('for else')
24750 "});
24751
24752 // test cursor move to start of each line on tab
24753 // for `try`, `except`, `else`, `finally`, `match` and `def`
24754 cx.set_state(indoc! {"
24755 def main():
24756 ˇ try:
24757 ˇ fetch()
24758 ˇ except ValueError:
24759 ˇ handle_error()
24760 ˇ else:
24761 ˇ match value:
24762 ˇ case _:
24763 ˇ finally:
24764 ˇ def status():
24765 ˇ return 0
24766 "});
24767 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24768 cx.assert_editor_state(indoc! {"
24769 def main():
24770 ˇtry:
24771 ˇfetch()
24772 ˇexcept ValueError:
24773 ˇhandle_error()
24774 ˇelse:
24775 ˇmatch value:
24776 ˇcase _:
24777 ˇfinally:
24778 ˇdef status():
24779 ˇreturn 0
24780 "});
24781 // test relative indent is preserved when tab
24782 // for `try`, `except`, `else`, `finally`, `match` and `def`
24783 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24784 cx.assert_editor_state(indoc! {"
24785 def main():
24786 ˇtry:
24787 ˇfetch()
24788 ˇexcept ValueError:
24789 ˇhandle_error()
24790 ˇelse:
24791 ˇmatch value:
24792 ˇcase _:
24793 ˇfinally:
24794 ˇdef status():
24795 ˇreturn 0
24796 "});
24797}
24798
24799#[gpui::test]
24800async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24801 init_test(cx, |_| {});
24802
24803 let mut cx = EditorTestContext::new(cx).await;
24804 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24805 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24806
24807 // test `else` auto outdents when typed inside `if` block
24808 cx.set_state(indoc! {"
24809 def main():
24810 if i == 2:
24811 return
24812 ˇ
24813 "});
24814 cx.update_editor(|editor, window, cx| {
24815 editor.handle_input("else:", window, cx);
24816 });
24817 cx.assert_editor_state(indoc! {"
24818 def main():
24819 if i == 2:
24820 return
24821 else:ˇ
24822 "});
24823
24824 // test `except` auto outdents when typed inside `try` block
24825 cx.set_state(indoc! {"
24826 def main():
24827 try:
24828 i = 2
24829 ˇ
24830 "});
24831 cx.update_editor(|editor, window, cx| {
24832 editor.handle_input("except:", window, cx);
24833 });
24834 cx.assert_editor_state(indoc! {"
24835 def main():
24836 try:
24837 i = 2
24838 except:ˇ
24839 "});
24840
24841 // test `else` auto outdents when typed inside `except` block
24842 cx.set_state(indoc! {"
24843 def main():
24844 try:
24845 i = 2
24846 except:
24847 j = 2
24848 ˇ
24849 "});
24850 cx.update_editor(|editor, window, cx| {
24851 editor.handle_input("else:", window, cx);
24852 });
24853 cx.assert_editor_state(indoc! {"
24854 def main():
24855 try:
24856 i = 2
24857 except:
24858 j = 2
24859 else:ˇ
24860 "});
24861
24862 // test `finally` auto outdents when typed inside `else` block
24863 cx.set_state(indoc! {"
24864 def main():
24865 try:
24866 i = 2
24867 except:
24868 j = 2
24869 else:
24870 k = 2
24871 ˇ
24872 "});
24873 cx.update_editor(|editor, window, cx| {
24874 editor.handle_input("finally:", window, cx);
24875 });
24876 cx.assert_editor_state(indoc! {"
24877 def main():
24878 try:
24879 i = 2
24880 except:
24881 j = 2
24882 else:
24883 k = 2
24884 finally:ˇ
24885 "});
24886
24887 // test `else` does not outdents when typed inside `except` block right after for block
24888 cx.set_state(indoc! {"
24889 def main():
24890 try:
24891 i = 2
24892 except:
24893 for i in range(n):
24894 pass
24895 ˇ
24896 "});
24897 cx.update_editor(|editor, window, cx| {
24898 editor.handle_input("else:", window, cx);
24899 });
24900 cx.assert_editor_state(indoc! {"
24901 def main():
24902 try:
24903 i = 2
24904 except:
24905 for i in range(n):
24906 pass
24907 else:ˇ
24908 "});
24909
24910 // test `finally` auto outdents when typed inside `else` block right after for block
24911 cx.set_state(indoc! {"
24912 def main():
24913 try:
24914 i = 2
24915 except:
24916 j = 2
24917 else:
24918 for i in range(n):
24919 pass
24920 ˇ
24921 "});
24922 cx.update_editor(|editor, window, cx| {
24923 editor.handle_input("finally:", window, cx);
24924 });
24925 cx.assert_editor_state(indoc! {"
24926 def main():
24927 try:
24928 i = 2
24929 except:
24930 j = 2
24931 else:
24932 for i in range(n):
24933 pass
24934 finally:ˇ
24935 "});
24936
24937 // test `except` outdents to inner "try" block
24938 cx.set_state(indoc! {"
24939 def main():
24940 try:
24941 i = 2
24942 if i == 2:
24943 try:
24944 i = 3
24945 ˇ
24946 "});
24947 cx.update_editor(|editor, window, cx| {
24948 editor.handle_input("except:", window, cx);
24949 });
24950 cx.assert_editor_state(indoc! {"
24951 def main():
24952 try:
24953 i = 2
24954 if i == 2:
24955 try:
24956 i = 3
24957 except:ˇ
24958 "});
24959
24960 // test `except` outdents to outer "try" block
24961 cx.set_state(indoc! {"
24962 def main():
24963 try:
24964 i = 2
24965 if i == 2:
24966 try:
24967 i = 3
24968 ˇ
24969 "});
24970 cx.update_editor(|editor, window, cx| {
24971 editor.handle_input("except:", window, cx);
24972 });
24973 cx.assert_editor_state(indoc! {"
24974 def main():
24975 try:
24976 i = 2
24977 if i == 2:
24978 try:
24979 i = 3
24980 except:ˇ
24981 "});
24982
24983 // test `else` stays at correct indent when typed after `for` block
24984 cx.set_state(indoc! {"
24985 def main():
24986 for i in range(10):
24987 if i == 3:
24988 break
24989 ˇ
24990 "});
24991 cx.update_editor(|editor, window, cx| {
24992 editor.handle_input("else:", window, cx);
24993 });
24994 cx.assert_editor_state(indoc! {"
24995 def main():
24996 for i in range(10):
24997 if i == 3:
24998 break
24999 else:ˇ
25000 "});
25001
25002 // test does not outdent on typing after line with square brackets
25003 cx.set_state(indoc! {"
25004 def f() -> list[str]:
25005 ˇ
25006 "});
25007 cx.update_editor(|editor, window, cx| {
25008 editor.handle_input("a", window, cx);
25009 });
25010 cx.assert_editor_state(indoc! {"
25011 def f() -> list[str]:
25012 aˇ
25013 "});
25014
25015 // test does not outdent on typing : after case keyword
25016 cx.set_state(indoc! {"
25017 match 1:
25018 caseˇ
25019 "});
25020 cx.update_editor(|editor, window, cx| {
25021 editor.handle_input(":", window, cx);
25022 });
25023 cx.assert_editor_state(indoc! {"
25024 match 1:
25025 case:ˇ
25026 "});
25027}
25028
25029#[gpui::test]
25030async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25031 init_test(cx, |_| {});
25032 update_test_language_settings(cx, |settings| {
25033 settings.defaults.extend_comment_on_newline = Some(false);
25034 });
25035 let mut cx = EditorTestContext::new(cx).await;
25036 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25037 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25038
25039 // test correct indent after newline on comment
25040 cx.set_state(indoc! {"
25041 # COMMENT:ˇ
25042 "});
25043 cx.update_editor(|editor, window, cx| {
25044 editor.newline(&Newline, window, cx);
25045 });
25046 cx.assert_editor_state(indoc! {"
25047 # COMMENT:
25048 ˇ
25049 "});
25050
25051 // test correct indent after newline in brackets
25052 cx.set_state(indoc! {"
25053 {ˇ}
25054 "});
25055 cx.update_editor(|editor, window, cx| {
25056 editor.newline(&Newline, window, cx);
25057 });
25058 cx.run_until_parked();
25059 cx.assert_editor_state(indoc! {"
25060 {
25061 ˇ
25062 }
25063 "});
25064
25065 cx.set_state(indoc! {"
25066 (ˇ)
25067 "});
25068 cx.update_editor(|editor, window, cx| {
25069 editor.newline(&Newline, window, cx);
25070 });
25071 cx.run_until_parked();
25072 cx.assert_editor_state(indoc! {"
25073 (
25074 ˇ
25075 )
25076 "});
25077
25078 // do not indent after empty lists or dictionaries
25079 cx.set_state(indoc! {"
25080 a = []ˇ
25081 "});
25082 cx.update_editor(|editor, window, cx| {
25083 editor.newline(&Newline, window, cx);
25084 });
25085 cx.run_until_parked();
25086 cx.assert_editor_state(indoc! {"
25087 a = []
25088 ˇ
25089 "});
25090}
25091
25092#[gpui::test]
25093async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25094 init_test(cx, |_| {});
25095
25096 let mut cx = EditorTestContext::new(cx).await;
25097 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25098 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25099
25100 // test cursor move to start of each line on tab
25101 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25102 cx.set_state(indoc! {"
25103 function main() {
25104 ˇ for item in $items; do
25105 ˇ while [ -n \"$item\" ]; do
25106 ˇ if [ \"$value\" -gt 10 ]; then
25107 ˇ continue
25108 ˇ elif [ \"$value\" -lt 0 ]; then
25109 ˇ break
25110 ˇ else
25111 ˇ echo \"$item\"
25112 ˇ fi
25113 ˇ done
25114 ˇ done
25115 ˇ}
25116 "});
25117 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25118 cx.assert_editor_state(indoc! {"
25119 function main() {
25120 ˇfor item in $items; do
25121 ˇwhile [ -n \"$item\" ]; do
25122 ˇif [ \"$value\" -gt 10 ]; then
25123 ˇcontinue
25124 ˇelif [ \"$value\" -lt 0 ]; then
25125 ˇbreak
25126 ˇelse
25127 ˇecho \"$item\"
25128 ˇfi
25129 ˇdone
25130 ˇdone
25131 ˇ}
25132 "});
25133 // test relative indent is preserved when tab
25134 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25135 cx.assert_editor_state(indoc! {"
25136 function main() {
25137 ˇfor item in $items; do
25138 ˇwhile [ -n \"$item\" ]; do
25139 ˇif [ \"$value\" -gt 10 ]; then
25140 ˇcontinue
25141 ˇelif [ \"$value\" -lt 0 ]; then
25142 ˇbreak
25143 ˇelse
25144 ˇecho \"$item\"
25145 ˇfi
25146 ˇdone
25147 ˇdone
25148 ˇ}
25149 "});
25150
25151 // test cursor move to start of each line on tab
25152 // for `case` statement with patterns
25153 cx.set_state(indoc! {"
25154 function handle() {
25155 ˇ case \"$1\" in
25156 ˇ start)
25157 ˇ echo \"a\"
25158 ˇ ;;
25159 ˇ stop)
25160 ˇ echo \"b\"
25161 ˇ ;;
25162 ˇ *)
25163 ˇ echo \"c\"
25164 ˇ ;;
25165 ˇ esac
25166 ˇ}
25167 "});
25168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25169 cx.assert_editor_state(indoc! {"
25170 function handle() {
25171 ˇcase \"$1\" in
25172 ˇstart)
25173 ˇecho \"a\"
25174 ˇ;;
25175 ˇstop)
25176 ˇecho \"b\"
25177 ˇ;;
25178 ˇ*)
25179 ˇecho \"c\"
25180 ˇ;;
25181 ˇesac
25182 ˇ}
25183 "});
25184}
25185
25186#[gpui::test]
25187async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25188 init_test(cx, |_| {});
25189
25190 let mut cx = EditorTestContext::new(cx).await;
25191 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25192 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25193
25194 // test indents on comment insert
25195 cx.set_state(indoc! {"
25196 function main() {
25197 ˇ for item in $items; do
25198 ˇ while [ -n \"$item\" ]; do
25199 ˇ if [ \"$value\" -gt 10 ]; then
25200 ˇ continue
25201 ˇ elif [ \"$value\" -lt 0 ]; then
25202 ˇ break
25203 ˇ else
25204 ˇ echo \"$item\"
25205 ˇ fi
25206 ˇ done
25207 ˇ done
25208 ˇ}
25209 "});
25210 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25211 cx.assert_editor_state(indoc! {"
25212 function main() {
25213 #ˇ for item in $items; do
25214 #ˇ while [ -n \"$item\" ]; do
25215 #ˇ if [ \"$value\" -gt 10 ]; then
25216 #ˇ continue
25217 #ˇ elif [ \"$value\" -lt 0 ]; then
25218 #ˇ break
25219 #ˇ else
25220 #ˇ echo \"$item\"
25221 #ˇ fi
25222 #ˇ done
25223 #ˇ done
25224 #ˇ}
25225 "});
25226}
25227
25228#[gpui::test]
25229async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25230 init_test(cx, |_| {});
25231
25232 let mut cx = EditorTestContext::new(cx).await;
25233 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25235
25236 // test `else` auto outdents when typed inside `if` block
25237 cx.set_state(indoc! {"
25238 if [ \"$1\" = \"test\" ]; then
25239 echo \"foo bar\"
25240 ˇ
25241 "});
25242 cx.update_editor(|editor, window, cx| {
25243 editor.handle_input("else", window, cx);
25244 });
25245 cx.assert_editor_state(indoc! {"
25246 if [ \"$1\" = \"test\" ]; then
25247 echo \"foo bar\"
25248 elseˇ
25249 "});
25250
25251 // test `elif` auto outdents when typed inside `if` block
25252 cx.set_state(indoc! {"
25253 if [ \"$1\" = \"test\" ]; then
25254 echo \"foo bar\"
25255 ˇ
25256 "});
25257 cx.update_editor(|editor, window, cx| {
25258 editor.handle_input("elif", window, cx);
25259 });
25260 cx.assert_editor_state(indoc! {"
25261 if [ \"$1\" = \"test\" ]; then
25262 echo \"foo bar\"
25263 elifˇ
25264 "});
25265
25266 // test `fi` auto outdents when typed inside `else` block
25267 cx.set_state(indoc! {"
25268 if [ \"$1\" = \"test\" ]; then
25269 echo \"foo bar\"
25270 else
25271 echo \"bar baz\"
25272 ˇ
25273 "});
25274 cx.update_editor(|editor, window, cx| {
25275 editor.handle_input("fi", window, cx);
25276 });
25277 cx.assert_editor_state(indoc! {"
25278 if [ \"$1\" = \"test\" ]; then
25279 echo \"foo bar\"
25280 else
25281 echo \"bar baz\"
25282 fiˇ
25283 "});
25284
25285 // test `done` auto outdents when typed inside `while` block
25286 cx.set_state(indoc! {"
25287 while read line; do
25288 echo \"$line\"
25289 ˇ
25290 "});
25291 cx.update_editor(|editor, window, cx| {
25292 editor.handle_input("done", window, cx);
25293 });
25294 cx.assert_editor_state(indoc! {"
25295 while read line; do
25296 echo \"$line\"
25297 doneˇ
25298 "});
25299
25300 // test `done` auto outdents when typed inside `for` block
25301 cx.set_state(indoc! {"
25302 for file in *.txt; do
25303 cat \"$file\"
25304 ˇ
25305 "});
25306 cx.update_editor(|editor, window, cx| {
25307 editor.handle_input("done", window, cx);
25308 });
25309 cx.assert_editor_state(indoc! {"
25310 for file in *.txt; do
25311 cat \"$file\"
25312 doneˇ
25313 "});
25314
25315 // test `esac` auto outdents when typed inside `case` block
25316 cx.set_state(indoc! {"
25317 case \"$1\" in
25318 start)
25319 echo \"foo bar\"
25320 ;;
25321 stop)
25322 echo \"bar baz\"
25323 ;;
25324 ˇ
25325 "});
25326 cx.update_editor(|editor, window, cx| {
25327 editor.handle_input("esac", window, cx);
25328 });
25329 cx.assert_editor_state(indoc! {"
25330 case \"$1\" in
25331 start)
25332 echo \"foo bar\"
25333 ;;
25334 stop)
25335 echo \"bar baz\"
25336 ;;
25337 esacˇ
25338 "});
25339
25340 // test `*)` auto outdents when typed inside `case` block
25341 cx.set_state(indoc! {"
25342 case \"$1\" in
25343 start)
25344 echo \"foo bar\"
25345 ;;
25346 ˇ
25347 "});
25348 cx.update_editor(|editor, window, cx| {
25349 editor.handle_input("*)", window, cx);
25350 });
25351 cx.assert_editor_state(indoc! {"
25352 case \"$1\" in
25353 start)
25354 echo \"foo bar\"
25355 ;;
25356 *)ˇ
25357 "});
25358
25359 // test `fi` outdents to correct level with nested if blocks
25360 cx.set_state(indoc! {"
25361 if [ \"$1\" = \"test\" ]; then
25362 echo \"outer if\"
25363 if [ \"$2\" = \"debug\" ]; then
25364 echo \"inner if\"
25365 ˇ
25366 "});
25367 cx.update_editor(|editor, window, cx| {
25368 editor.handle_input("fi", window, cx);
25369 });
25370 cx.assert_editor_state(indoc! {"
25371 if [ \"$1\" = \"test\" ]; then
25372 echo \"outer if\"
25373 if [ \"$2\" = \"debug\" ]; then
25374 echo \"inner if\"
25375 fiˇ
25376 "});
25377}
25378
25379#[gpui::test]
25380async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25381 init_test(cx, |_| {});
25382 update_test_language_settings(cx, |settings| {
25383 settings.defaults.extend_comment_on_newline = Some(false);
25384 });
25385 let mut cx = EditorTestContext::new(cx).await;
25386 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25387 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25388
25389 // test correct indent after newline on comment
25390 cx.set_state(indoc! {"
25391 # COMMENT:ˇ
25392 "});
25393 cx.update_editor(|editor, window, cx| {
25394 editor.newline(&Newline, window, cx);
25395 });
25396 cx.assert_editor_state(indoc! {"
25397 # COMMENT:
25398 ˇ
25399 "});
25400
25401 // test correct indent after newline after `then`
25402 cx.set_state(indoc! {"
25403
25404 if [ \"$1\" = \"test\" ]; thenˇ
25405 "});
25406 cx.update_editor(|editor, window, cx| {
25407 editor.newline(&Newline, window, cx);
25408 });
25409 cx.run_until_parked();
25410 cx.assert_editor_state(indoc! {"
25411
25412 if [ \"$1\" = \"test\" ]; then
25413 ˇ
25414 "});
25415
25416 // test correct indent after newline after `else`
25417 cx.set_state(indoc! {"
25418 if [ \"$1\" = \"test\" ]; then
25419 elseˇ
25420 "});
25421 cx.update_editor(|editor, window, cx| {
25422 editor.newline(&Newline, window, cx);
25423 });
25424 cx.run_until_parked();
25425 cx.assert_editor_state(indoc! {"
25426 if [ \"$1\" = \"test\" ]; then
25427 else
25428 ˇ
25429 "});
25430
25431 // test correct indent after newline after `elif`
25432 cx.set_state(indoc! {"
25433 if [ \"$1\" = \"test\" ]; then
25434 elifˇ
25435 "});
25436 cx.update_editor(|editor, window, cx| {
25437 editor.newline(&Newline, window, cx);
25438 });
25439 cx.run_until_parked();
25440 cx.assert_editor_state(indoc! {"
25441 if [ \"$1\" = \"test\" ]; then
25442 elif
25443 ˇ
25444 "});
25445
25446 // test correct indent after newline after `do`
25447 cx.set_state(indoc! {"
25448 for file in *.txt; doˇ
25449 "});
25450 cx.update_editor(|editor, window, cx| {
25451 editor.newline(&Newline, window, cx);
25452 });
25453 cx.run_until_parked();
25454 cx.assert_editor_state(indoc! {"
25455 for file in *.txt; do
25456 ˇ
25457 "});
25458
25459 // test correct indent after newline after case pattern
25460 cx.set_state(indoc! {"
25461 case \"$1\" in
25462 start)ˇ
25463 "});
25464 cx.update_editor(|editor, window, cx| {
25465 editor.newline(&Newline, window, cx);
25466 });
25467 cx.run_until_parked();
25468 cx.assert_editor_state(indoc! {"
25469 case \"$1\" in
25470 start)
25471 ˇ
25472 "});
25473
25474 // test correct indent after newline after case pattern
25475 cx.set_state(indoc! {"
25476 case \"$1\" in
25477 start)
25478 ;;
25479 *)ˇ
25480 "});
25481 cx.update_editor(|editor, window, cx| {
25482 editor.newline(&Newline, window, cx);
25483 });
25484 cx.run_until_parked();
25485 cx.assert_editor_state(indoc! {"
25486 case \"$1\" in
25487 start)
25488 ;;
25489 *)
25490 ˇ
25491 "});
25492
25493 // test correct indent after newline after function opening brace
25494 cx.set_state(indoc! {"
25495 function test() {ˇ}
25496 "});
25497 cx.update_editor(|editor, window, cx| {
25498 editor.newline(&Newline, window, cx);
25499 });
25500 cx.run_until_parked();
25501 cx.assert_editor_state(indoc! {"
25502 function test() {
25503 ˇ
25504 }
25505 "});
25506
25507 // test no extra indent after semicolon on same line
25508 cx.set_state(indoc! {"
25509 echo \"test\";ˇ
25510 "});
25511 cx.update_editor(|editor, window, cx| {
25512 editor.newline(&Newline, window, cx);
25513 });
25514 cx.run_until_parked();
25515 cx.assert_editor_state(indoc! {"
25516 echo \"test\";
25517 ˇ
25518 "});
25519}
25520
25521fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25522 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25523 point..point
25524}
25525
25526#[track_caller]
25527fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25528 let (text, ranges) = marked_text_ranges(marked_text, true);
25529 assert_eq!(editor.text(cx), text);
25530 assert_eq!(
25531 editor.selections.ranges(&editor.display_snapshot(cx)),
25532 ranges,
25533 "Assert selections are {}",
25534 marked_text
25535 );
25536}
25537
25538pub fn handle_signature_help_request(
25539 cx: &mut EditorLspTestContext,
25540 mocked_response: lsp::SignatureHelp,
25541) -> impl Future<Output = ()> + use<> {
25542 let mut request =
25543 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25544 let mocked_response = mocked_response.clone();
25545 async move { Ok(Some(mocked_response)) }
25546 });
25547
25548 async move {
25549 request.next().await;
25550 }
25551}
25552
25553#[track_caller]
25554pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25555 cx.update_editor(|editor, _, _| {
25556 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25557 let entries = menu.entries.borrow();
25558 let entries = entries
25559 .iter()
25560 .map(|entry| entry.string.as_str())
25561 .collect::<Vec<_>>();
25562 assert_eq!(entries, expected);
25563 } else {
25564 panic!("Expected completions menu");
25565 }
25566 });
25567}
25568
25569/// Handle completion request passing a marked string specifying where the completion
25570/// should be triggered from using '|' character, what range should be replaced, and what completions
25571/// should be returned using '<' and '>' to delimit the range.
25572///
25573/// Also see `handle_completion_request_with_insert_and_replace`.
25574#[track_caller]
25575pub fn handle_completion_request(
25576 marked_string: &str,
25577 completions: Vec<&'static str>,
25578 is_incomplete: bool,
25579 counter: Arc<AtomicUsize>,
25580 cx: &mut EditorLspTestContext,
25581) -> impl Future<Output = ()> {
25582 let complete_from_marker: TextRangeMarker = '|'.into();
25583 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25584 let (_, mut marked_ranges) = marked_text_ranges_by(
25585 marked_string,
25586 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25587 );
25588
25589 let complete_from_position =
25590 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25591 let replace_range =
25592 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25593
25594 let mut request =
25595 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25596 let completions = completions.clone();
25597 counter.fetch_add(1, atomic::Ordering::Release);
25598 async move {
25599 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25600 assert_eq!(
25601 params.text_document_position.position,
25602 complete_from_position
25603 );
25604 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25605 is_incomplete,
25606 item_defaults: None,
25607 items: completions
25608 .iter()
25609 .map(|completion_text| lsp::CompletionItem {
25610 label: completion_text.to_string(),
25611 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25612 range: replace_range,
25613 new_text: completion_text.to_string(),
25614 })),
25615 ..Default::default()
25616 })
25617 .collect(),
25618 })))
25619 }
25620 });
25621
25622 async move {
25623 request.next().await;
25624 }
25625}
25626
25627/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25628/// given instead, which also contains an `insert` range.
25629///
25630/// This function uses markers to define ranges:
25631/// - `|` marks the cursor position
25632/// - `<>` marks the replace range
25633/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25634pub fn handle_completion_request_with_insert_and_replace(
25635 cx: &mut EditorLspTestContext,
25636 marked_string: &str,
25637 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25638 counter: Arc<AtomicUsize>,
25639) -> impl Future<Output = ()> {
25640 let complete_from_marker: TextRangeMarker = '|'.into();
25641 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25642 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25643
25644 let (_, mut marked_ranges) = marked_text_ranges_by(
25645 marked_string,
25646 vec![
25647 complete_from_marker.clone(),
25648 replace_range_marker.clone(),
25649 insert_range_marker.clone(),
25650 ],
25651 );
25652
25653 let complete_from_position =
25654 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25655 let replace_range =
25656 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25657
25658 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25659 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25660 _ => lsp::Range {
25661 start: replace_range.start,
25662 end: complete_from_position,
25663 },
25664 };
25665
25666 let mut request =
25667 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25668 let completions = completions.clone();
25669 counter.fetch_add(1, atomic::Ordering::Release);
25670 async move {
25671 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25672 assert_eq!(
25673 params.text_document_position.position, complete_from_position,
25674 "marker `|` position doesn't match",
25675 );
25676 Ok(Some(lsp::CompletionResponse::Array(
25677 completions
25678 .iter()
25679 .map(|(label, new_text)| lsp::CompletionItem {
25680 label: label.to_string(),
25681 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25682 lsp::InsertReplaceEdit {
25683 insert: insert_range,
25684 replace: replace_range,
25685 new_text: new_text.to_string(),
25686 },
25687 )),
25688 ..Default::default()
25689 })
25690 .collect(),
25691 )))
25692 }
25693 });
25694
25695 async move {
25696 request.next().await;
25697 }
25698}
25699
25700fn handle_resolve_completion_request(
25701 cx: &mut EditorLspTestContext,
25702 edits: Option<Vec<(&'static str, &'static str)>>,
25703) -> impl Future<Output = ()> {
25704 let edits = edits.map(|edits| {
25705 edits
25706 .iter()
25707 .map(|(marked_string, new_text)| {
25708 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25709 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25710 lsp::TextEdit::new(replace_range, new_text.to_string())
25711 })
25712 .collect::<Vec<_>>()
25713 });
25714
25715 let mut request =
25716 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25717 let edits = edits.clone();
25718 async move {
25719 Ok(lsp::CompletionItem {
25720 additional_text_edits: edits,
25721 ..Default::default()
25722 })
25723 }
25724 });
25725
25726 async move {
25727 request.next().await;
25728 }
25729}
25730
25731pub(crate) fn update_test_language_settings(
25732 cx: &mut TestAppContext,
25733 f: impl Fn(&mut AllLanguageSettingsContent),
25734) {
25735 cx.update(|cx| {
25736 SettingsStore::update_global(cx, |store, cx| {
25737 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25738 });
25739 });
25740}
25741
25742pub(crate) fn update_test_project_settings(
25743 cx: &mut TestAppContext,
25744 f: impl Fn(&mut ProjectSettingsContent),
25745) {
25746 cx.update(|cx| {
25747 SettingsStore::update_global(cx, |store, cx| {
25748 store.update_user_settings(cx, |settings| f(&mut settings.project));
25749 });
25750 });
25751}
25752
25753pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25754 cx.update(|cx| {
25755 assets::Assets.load_test_fonts(cx);
25756 let store = SettingsStore::test(cx);
25757 cx.set_global(store);
25758 theme::init(theme::LoadThemes::JustBase, cx);
25759 release_channel::init(SemanticVersion::default(), cx);
25760 client::init_settings(cx);
25761 language::init(cx);
25762 Project::init_settings(cx);
25763 workspace::init_settings(cx);
25764 crate::init(cx);
25765 });
25766 zlog::init_test();
25767 update_test_language_settings(cx, f);
25768}
25769
25770#[track_caller]
25771fn assert_hunk_revert(
25772 not_reverted_text_with_selections: &str,
25773 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25774 expected_reverted_text_with_selections: &str,
25775 base_text: &str,
25776 cx: &mut EditorLspTestContext,
25777) {
25778 cx.set_state(not_reverted_text_with_selections);
25779 cx.set_head_text(base_text);
25780 cx.executor().run_until_parked();
25781
25782 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25783 let snapshot = editor.snapshot(window, cx);
25784 let reverted_hunk_statuses = snapshot
25785 .buffer_snapshot()
25786 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25787 .map(|hunk| hunk.status().kind)
25788 .collect::<Vec<_>>();
25789
25790 editor.git_restore(&Default::default(), window, cx);
25791 reverted_hunk_statuses
25792 });
25793 cx.executor().run_until_parked();
25794 cx.assert_editor_state(expected_reverted_text_with_selections);
25795 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25796}
25797
25798#[gpui::test(iterations = 10)]
25799async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25800 init_test(cx, |_| {});
25801
25802 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25803 let counter = diagnostic_requests.clone();
25804
25805 let fs = FakeFs::new(cx.executor());
25806 fs.insert_tree(
25807 path!("/a"),
25808 json!({
25809 "first.rs": "fn main() { let a = 5; }",
25810 "second.rs": "// Test file",
25811 }),
25812 )
25813 .await;
25814
25815 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25817 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25818
25819 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25820 language_registry.add(rust_lang());
25821 let mut fake_servers = language_registry.register_fake_lsp(
25822 "Rust",
25823 FakeLspAdapter {
25824 capabilities: lsp::ServerCapabilities {
25825 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25826 lsp::DiagnosticOptions {
25827 identifier: None,
25828 inter_file_dependencies: true,
25829 workspace_diagnostics: true,
25830 work_done_progress_options: Default::default(),
25831 },
25832 )),
25833 ..Default::default()
25834 },
25835 ..Default::default()
25836 },
25837 );
25838
25839 let editor = workspace
25840 .update(cx, |workspace, window, cx| {
25841 workspace.open_abs_path(
25842 PathBuf::from(path!("/a/first.rs")),
25843 OpenOptions::default(),
25844 window,
25845 cx,
25846 )
25847 })
25848 .unwrap()
25849 .await
25850 .unwrap()
25851 .downcast::<Editor>()
25852 .unwrap();
25853 let fake_server = fake_servers.next().await.unwrap();
25854 let server_id = fake_server.server.server_id();
25855 let mut first_request = fake_server
25856 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25857 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25858 let result_id = Some(new_result_id.to_string());
25859 assert_eq!(
25860 params.text_document.uri,
25861 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25862 );
25863 async move {
25864 Ok(lsp::DocumentDiagnosticReportResult::Report(
25865 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25866 related_documents: None,
25867 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25868 items: Vec::new(),
25869 result_id,
25870 },
25871 }),
25872 ))
25873 }
25874 });
25875
25876 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25877 project.update(cx, |project, cx| {
25878 let buffer_id = editor
25879 .read(cx)
25880 .buffer()
25881 .read(cx)
25882 .as_singleton()
25883 .expect("created a singleton buffer")
25884 .read(cx)
25885 .remote_id();
25886 let buffer_result_id = project
25887 .lsp_store()
25888 .read(cx)
25889 .result_id(server_id, buffer_id, cx);
25890 assert_eq!(expected, buffer_result_id);
25891 });
25892 };
25893
25894 ensure_result_id(None, cx);
25895 cx.executor().advance_clock(Duration::from_millis(60));
25896 cx.executor().run_until_parked();
25897 assert_eq!(
25898 diagnostic_requests.load(atomic::Ordering::Acquire),
25899 1,
25900 "Opening file should trigger diagnostic request"
25901 );
25902 first_request
25903 .next()
25904 .await
25905 .expect("should have sent the first diagnostics pull request");
25906 ensure_result_id(Some("1".to_string()), cx);
25907
25908 // Editing should trigger diagnostics
25909 editor.update_in(cx, |editor, window, cx| {
25910 editor.handle_input("2", window, cx)
25911 });
25912 cx.executor().advance_clock(Duration::from_millis(60));
25913 cx.executor().run_until_parked();
25914 assert_eq!(
25915 diagnostic_requests.load(atomic::Ordering::Acquire),
25916 2,
25917 "Editing should trigger diagnostic request"
25918 );
25919 ensure_result_id(Some("2".to_string()), cx);
25920
25921 // Moving cursor should not trigger diagnostic request
25922 editor.update_in(cx, |editor, window, cx| {
25923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25924 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25925 });
25926 });
25927 cx.executor().advance_clock(Duration::from_millis(60));
25928 cx.executor().run_until_parked();
25929 assert_eq!(
25930 diagnostic_requests.load(atomic::Ordering::Acquire),
25931 2,
25932 "Cursor movement should not trigger diagnostic request"
25933 );
25934 ensure_result_id(Some("2".to_string()), cx);
25935 // Multiple rapid edits should be debounced
25936 for _ in 0..5 {
25937 editor.update_in(cx, |editor, window, cx| {
25938 editor.handle_input("x", window, cx)
25939 });
25940 }
25941 cx.executor().advance_clock(Duration::from_millis(60));
25942 cx.executor().run_until_parked();
25943
25944 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25945 assert!(
25946 final_requests <= 4,
25947 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25948 );
25949 ensure_result_id(Some(final_requests.to_string()), cx);
25950}
25951
25952#[gpui::test]
25953async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25954 // Regression test for issue #11671
25955 // Previously, adding a cursor after moving multiple cursors would reset
25956 // the cursor count instead of adding to the existing cursors.
25957 init_test(cx, |_| {});
25958 let mut cx = EditorTestContext::new(cx).await;
25959
25960 // Create a simple buffer with cursor at start
25961 cx.set_state(indoc! {"
25962 ˇaaaa
25963 bbbb
25964 cccc
25965 dddd
25966 eeee
25967 ffff
25968 gggg
25969 hhhh"});
25970
25971 // Add 2 cursors below (so we have 3 total)
25972 cx.update_editor(|editor, window, cx| {
25973 editor.add_selection_below(&Default::default(), window, cx);
25974 editor.add_selection_below(&Default::default(), window, cx);
25975 });
25976
25977 // Verify we have 3 cursors
25978 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25979 assert_eq!(
25980 initial_count, 3,
25981 "Should have 3 cursors after adding 2 below"
25982 );
25983
25984 // Move down one line
25985 cx.update_editor(|editor, window, cx| {
25986 editor.move_down(&MoveDown, window, cx);
25987 });
25988
25989 // Add another cursor below
25990 cx.update_editor(|editor, window, cx| {
25991 editor.add_selection_below(&Default::default(), window, cx);
25992 });
25993
25994 // Should now have 4 cursors (3 original + 1 new)
25995 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25996 assert_eq!(
25997 final_count, 4,
25998 "Should have 4 cursors after moving and adding another"
25999 );
26000}
26001
26002#[gpui::test]
26003async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26004 init_test(cx, |_| {});
26005
26006 let mut cx = EditorTestContext::new(cx).await;
26007
26008 cx.set_state(indoc!(
26009 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26010 Second line here"#
26011 ));
26012
26013 cx.update_editor(|editor, window, cx| {
26014 // Enable soft wrapping with a narrow width to force soft wrapping and
26015 // confirm that more than 2 rows are being displayed.
26016 editor.set_wrap_width(Some(100.0.into()), cx);
26017 assert!(editor.display_text(cx).lines().count() > 2);
26018
26019 editor.add_selection_below(
26020 &AddSelectionBelow {
26021 skip_soft_wrap: true,
26022 },
26023 window,
26024 cx,
26025 );
26026
26027 assert_eq!(
26028 editor.selections.display_ranges(cx),
26029 &[
26030 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26031 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26032 ]
26033 );
26034
26035 editor.add_selection_above(
26036 &AddSelectionAbove {
26037 skip_soft_wrap: true,
26038 },
26039 window,
26040 cx,
26041 );
26042
26043 assert_eq!(
26044 editor.selections.display_ranges(cx),
26045 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26046 );
26047
26048 editor.add_selection_below(
26049 &AddSelectionBelow {
26050 skip_soft_wrap: false,
26051 },
26052 window,
26053 cx,
26054 );
26055
26056 assert_eq!(
26057 editor.selections.display_ranges(cx),
26058 &[
26059 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26060 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26061 ]
26062 );
26063
26064 editor.add_selection_above(
26065 &AddSelectionAbove {
26066 skip_soft_wrap: false,
26067 },
26068 window,
26069 cx,
26070 );
26071
26072 assert_eq!(
26073 editor.selections.display_ranges(cx),
26074 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26075 );
26076 });
26077}
26078
26079#[gpui::test(iterations = 10)]
26080async fn test_document_colors(cx: &mut TestAppContext) {
26081 let expected_color = Rgba {
26082 r: 0.33,
26083 g: 0.33,
26084 b: 0.33,
26085 a: 0.33,
26086 };
26087
26088 init_test(cx, |_| {});
26089
26090 let fs = FakeFs::new(cx.executor());
26091 fs.insert_tree(
26092 path!("/a"),
26093 json!({
26094 "first.rs": "fn main() { let a = 5; }",
26095 }),
26096 )
26097 .await;
26098
26099 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26100 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26101 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26102
26103 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26104 language_registry.add(rust_lang());
26105 let mut fake_servers = language_registry.register_fake_lsp(
26106 "Rust",
26107 FakeLspAdapter {
26108 capabilities: lsp::ServerCapabilities {
26109 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26110 ..lsp::ServerCapabilities::default()
26111 },
26112 name: "rust-analyzer",
26113 ..FakeLspAdapter::default()
26114 },
26115 );
26116 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26117 "Rust",
26118 FakeLspAdapter {
26119 capabilities: lsp::ServerCapabilities {
26120 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26121 ..lsp::ServerCapabilities::default()
26122 },
26123 name: "not-rust-analyzer",
26124 ..FakeLspAdapter::default()
26125 },
26126 );
26127
26128 let editor = workspace
26129 .update(cx, |workspace, window, cx| {
26130 workspace.open_abs_path(
26131 PathBuf::from(path!("/a/first.rs")),
26132 OpenOptions::default(),
26133 window,
26134 cx,
26135 )
26136 })
26137 .unwrap()
26138 .await
26139 .unwrap()
26140 .downcast::<Editor>()
26141 .unwrap();
26142 let fake_language_server = fake_servers.next().await.unwrap();
26143 let fake_language_server_without_capabilities =
26144 fake_servers_without_capabilities.next().await.unwrap();
26145 let requests_made = Arc::new(AtomicUsize::new(0));
26146 let closure_requests_made = Arc::clone(&requests_made);
26147 let mut color_request_handle = fake_language_server
26148 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26149 let requests_made = Arc::clone(&closure_requests_made);
26150 async move {
26151 assert_eq!(
26152 params.text_document.uri,
26153 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26154 );
26155 requests_made.fetch_add(1, atomic::Ordering::Release);
26156 Ok(vec![
26157 lsp::ColorInformation {
26158 range: lsp::Range {
26159 start: lsp::Position {
26160 line: 0,
26161 character: 0,
26162 },
26163 end: lsp::Position {
26164 line: 0,
26165 character: 1,
26166 },
26167 },
26168 color: lsp::Color {
26169 red: 0.33,
26170 green: 0.33,
26171 blue: 0.33,
26172 alpha: 0.33,
26173 },
26174 },
26175 lsp::ColorInformation {
26176 range: lsp::Range {
26177 start: lsp::Position {
26178 line: 0,
26179 character: 0,
26180 },
26181 end: lsp::Position {
26182 line: 0,
26183 character: 1,
26184 },
26185 },
26186 color: lsp::Color {
26187 red: 0.33,
26188 green: 0.33,
26189 blue: 0.33,
26190 alpha: 0.33,
26191 },
26192 },
26193 ])
26194 }
26195 });
26196
26197 let _handle = fake_language_server_without_capabilities
26198 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26199 panic!("Should not be called");
26200 });
26201 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26202 color_request_handle.next().await.unwrap();
26203 cx.run_until_parked();
26204 assert_eq!(
26205 1,
26206 requests_made.load(atomic::Ordering::Acquire),
26207 "Should query for colors once per editor open"
26208 );
26209 editor.update_in(cx, |editor, _, cx| {
26210 assert_eq!(
26211 vec![expected_color],
26212 extract_color_inlays(editor, cx),
26213 "Should have an initial inlay"
26214 );
26215 });
26216
26217 // opening another file in a split should not influence the LSP query counter
26218 workspace
26219 .update(cx, |workspace, window, cx| {
26220 assert_eq!(
26221 workspace.panes().len(),
26222 1,
26223 "Should have one pane with one editor"
26224 );
26225 workspace.move_item_to_pane_in_direction(
26226 &MoveItemToPaneInDirection {
26227 direction: SplitDirection::Right,
26228 focus: false,
26229 clone: true,
26230 },
26231 window,
26232 cx,
26233 );
26234 })
26235 .unwrap();
26236 cx.run_until_parked();
26237 workspace
26238 .update(cx, |workspace, _, cx| {
26239 let panes = workspace.panes();
26240 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26241 for pane in panes {
26242 let editor = pane
26243 .read(cx)
26244 .active_item()
26245 .and_then(|item| item.downcast::<Editor>())
26246 .expect("Should have opened an editor in each split");
26247 let editor_file = editor
26248 .read(cx)
26249 .buffer()
26250 .read(cx)
26251 .as_singleton()
26252 .expect("test deals with singleton buffers")
26253 .read(cx)
26254 .file()
26255 .expect("test buffese should have a file")
26256 .path();
26257 assert_eq!(
26258 editor_file.as_ref(),
26259 rel_path("first.rs"),
26260 "Both editors should be opened for the same file"
26261 )
26262 }
26263 })
26264 .unwrap();
26265
26266 cx.executor().advance_clock(Duration::from_millis(500));
26267 let save = editor.update_in(cx, |editor, window, cx| {
26268 editor.move_to_end(&MoveToEnd, window, cx);
26269 editor.handle_input("dirty", window, cx);
26270 editor.save(
26271 SaveOptions {
26272 format: true,
26273 autosave: true,
26274 },
26275 project.clone(),
26276 window,
26277 cx,
26278 )
26279 });
26280 save.await.unwrap();
26281
26282 color_request_handle.next().await.unwrap();
26283 cx.run_until_parked();
26284 assert_eq!(
26285 2,
26286 requests_made.load(atomic::Ordering::Acquire),
26287 "Should query for colors once per save (deduplicated) and once per formatting after save"
26288 );
26289
26290 drop(editor);
26291 let close = workspace
26292 .update(cx, |workspace, window, cx| {
26293 workspace.active_pane().update(cx, |pane, cx| {
26294 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26295 })
26296 })
26297 .unwrap();
26298 close.await.unwrap();
26299 let close = workspace
26300 .update(cx, |workspace, window, cx| {
26301 workspace.active_pane().update(cx, |pane, cx| {
26302 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26303 })
26304 })
26305 .unwrap();
26306 close.await.unwrap();
26307 assert_eq!(
26308 2,
26309 requests_made.load(atomic::Ordering::Acquire),
26310 "After saving and closing all editors, no extra requests should be made"
26311 );
26312 workspace
26313 .update(cx, |workspace, _, cx| {
26314 assert!(
26315 workspace.active_item(cx).is_none(),
26316 "Should close all editors"
26317 )
26318 })
26319 .unwrap();
26320
26321 workspace
26322 .update(cx, |workspace, window, cx| {
26323 workspace.active_pane().update(cx, |pane, cx| {
26324 pane.navigate_backward(&workspace::GoBack, window, cx);
26325 })
26326 })
26327 .unwrap();
26328 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26329 cx.run_until_parked();
26330 let editor = workspace
26331 .update(cx, |workspace, _, cx| {
26332 workspace
26333 .active_item(cx)
26334 .expect("Should have reopened the editor again after navigating back")
26335 .downcast::<Editor>()
26336 .expect("Should be an editor")
26337 })
26338 .unwrap();
26339
26340 assert_eq!(
26341 2,
26342 requests_made.load(atomic::Ordering::Acquire),
26343 "Cache should be reused on buffer close and reopen"
26344 );
26345 editor.update(cx, |editor, cx| {
26346 assert_eq!(
26347 vec![expected_color],
26348 extract_color_inlays(editor, cx),
26349 "Should have an initial inlay"
26350 );
26351 });
26352
26353 drop(color_request_handle);
26354 let closure_requests_made = Arc::clone(&requests_made);
26355 let mut empty_color_request_handle = fake_language_server
26356 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26357 let requests_made = Arc::clone(&closure_requests_made);
26358 async move {
26359 assert_eq!(
26360 params.text_document.uri,
26361 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26362 );
26363 requests_made.fetch_add(1, atomic::Ordering::Release);
26364 Ok(Vec::new())
26365 }
26366 });
26367 let save = editor.update_in(cx, |editor, window, cx| {
26368 editor.move_to_end(&MoveToEnd, window, cx);
26369 editor.handle_input("dirty_again", window, cx);
26370 editor.save(
26371 SaveOptions {
26372 format: false,
26373 autosave: true,
26374 },
26375 project.clone(),
26376 window,
26377 cx,
26378 )
26379 });
26380 save.await.unwrap();
26381
26382 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26383 empty_color_request_handle.next().await.unwrap();
26384 cx.run_until_parked();
26385 assert_eq!(
26386 3,
26387 requests_made.load(atomic::Ordering::Acquire),
26388 "Should query for colors once per save only, as formatting was not requested"
26389 );
26390 editor.update(cx, |editor, cx| {
26391 assert_eq!(
26392 Vec::<Rgba>::new(),
26393 extract_color_inlays(editor, cx),
26394 "Should clear all colors when the server returns an empty response"
26395 );
26396 });
26397}
26398
26399#[gpui::test]
26400async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26401 init_test(cx, |_| {});
26402 let (editor, cx) = cx.add_window_view(Editor::single_line);
26403 editor.update_in(cx, |editor, window, cx| {
26404 editor.set_text("oops\n\nwow\n", window, cx)
26405 });
26406 cx.run_until_parked();
26407 editor.update(cx, |editor, cx| {
26408 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26409 });
26410 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26411 cx.run_until_parked();
26412 editor.update(cx, |editor, cx| {
26413 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26414 });
26415}
26416
26417#[gpui::test]
26418async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26419 init_test(cx, |_| {});
26420
26421 cx.update(|cx| {
26422 register_project_item::<Editor>(cx);
26423 });
26424
26425 let fs = FakeFs::new(cx.executor());
26426 fs.insert_tree("/root1", json!({})).await;
26427 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26428 .await;
26429
26430 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26431 let (workspace, cx) =
26432 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26433
26434 let worktree_id = project.update(cx, |project, cx| {
26435 project.worktrees(cx).next().unwrap().read(cx).id()
26436 });
26437
26438 let handle = workspace
26439 .update_in(cx, |workspace, window, cx| {
26440 let project_path = (worktree_id, rel_path("one.pdf"));
26441 workspace.open_path(project_path, None, true, window, cx)
26442 })
26443 .await
26444 .unwrap();
26445
26446 assert_eq!(
26447 handle.to_any().entity_type(),
26448 TypeId::of::<InvalidItemView>()
26449 );
26450}
26451
26452#[gpui::test]
26453async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26454 init_test(cx, |_| {});
26455
26456 let language = Arc::new(Language::new(
26457 LanguageConfig::default(),
26458 Some(tree_sitter_rust::LANGUAGE.into()),
26459 ));
26460
26461 // Test hierarchical sibling navigation
26462 let text = r#"
26463 fn outer() {
26464 if condition {
26465 let a = 1;
26466 }
26467 let b = 2;
26468 }
26469
26470 fn another() {
26471 let c = 3;
26472 }
26473 "#;
26474
26475 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26476 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26477 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26478
26479 // Wait for parsing to complete
26480 editor
26481 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26482 .await;
26483
26484 editor.update_in(cx, |editor, window, cx| {
26485 // Start by selecting "let a = 1;" inside the if block
26486 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26487 s.select_display_ranges([
26488 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26489 ]);
26490 });
26491
26492 let initial_selection = editor.selections.display_ranges(cx);
26493 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26494
26495 // Test select next sibling - should move up levels to find the next sibling
26496 // Since "let a = 1;" has no siblings in the if block, it should move up
26497 // to find "let b = 2;" which is a sibling of the if block
26498 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26499 let next_selection = editor.selections.display_ranges(cx);
26500
26501 // Should have a selection and it should be different from the initial
26502 assert_eq!(
26503 next_selection.len(),
26504 1,
26505 "Should have one selection after next"
26506 );
26507 assert_ne!(
26508 next_selection[0], initial_selection[0],
26509 "Next sibling selection should be different"
26510 );
26511
26512 // Test hierarchical navigation by going to the end of the current function
26513 // and trying to navigate to the next function
26514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26515 s.select_display_ranges([
26516 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26517 ]);
26518 });
26519
26520 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26521 let function_next_selection = editor.selections.display_ranges(cx);
26522
26523 // Should move to the next function
26524 assert_eq!(
26525 function_next_selection.len(),
26526 1,
26527 "Should have one selection after function next"
26528 );
26529
26530 // Test select previous sibling navigation
26531 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26532 let prev_selection = editor.selections.display_ranges(cx);
26533
26534 // Should have a selection and it should be different
26535 assert_eq!(
26536 prev_selection.len(),
26537 1,
26538 "Should have one selection after prev"
26539 );
26540 assert_ne!(
26541 prev_selection[0], function_next_selection[0],
26542 "Previous sibling selection should be different from next"
26543 );
26544 });
26545}
26546
26547#[gpui::test]
26548async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26549 init_test(cx, |_| {});
26550
26551 let mut cx = EditorTestContext::new(cx).await;
26552 cx.set_state(
26553 "let ˇvariable = 42;
26554let another = variable + 1;
26555let result = variable * 2;",
26556 );
26557
26558 // Set up document highlights manually (simulating LSP response)
26559 cx.update_editor(|editor, _window, cx| {
26560 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26561
26562 // Create highlights for "variable" occurrences
26563 let highlight_ranges = [
26564 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26565 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26566 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26567 ];
26568
26569 let anchor_ranges: Vec<_> = highlight_ranges
26570 .iter()
26571 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26572 .collect();
26573
26574 editor.highlight_background::<DocumentHighlightRead>(
26575 &anchor_ranges,
26576 |theme| theme.colors().editor_document_highlight_read_background,
26577 cx,
26578 );
26579 });
26580
26581 // Go to next highlight - should move to second "variable"
26582 cx.update_editor(|editor, window, cx| {
26583 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26584 });
26585 cx.assert_editor_state(
26586 "let variable = 42;
26587let another = ˇvariable + 1;
26588let result = variable * 2;",
26589 );
26590
26591 // Go to next highlight - should move to third "variable"
26592 cx.update_editor(|editor, window, cx| {
26593 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26594 });
26595 cx.assert_editor_state(
26596 "let variable = 42;
26597let another = variable + 1;
26598let result = ˇvariable * 2;",
26599 );
26600
26601 // Go to next highlight - should stay at third "variable" (no wrap-around)
26602 cx.update_editor(|editor, window, cx| {
26603 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26604 });
26605 cx.assert_editor_state(
26606 "let variable = 42;
26607let another = variable + 1;
26608let result = ˇvariable * 2;",
26609 );
26610
26611 // Now test going backwards from third position
26612 cx.update_editor(|editor, window, cx| {
26613 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26614 });
26615 cx.assert_editor_state(
26616 "let variable = 42;
26617let another = ˇvariable + 1;
26618let result = variable * 2;",
26619 );
26620
26621 // Go to previous highlight - should move to first "variable"
26622 cx.update_editor(|editor, window, cx| {
26623 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26624 });
26625 cx.assert_editor_state(
26626 "let ˇvariable = 42;
26627let another = variable + 1;
26628let result = variable * 2;",
26629 );
26630
26631 // Go to previous highlight - should stay on first "variable"
26632 cx.update_editor(|editor, window, cx| {
26633 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26634 });
26635 cx.assert_editor_state(
26636 "let ˇvariable = 42;
26637let another = variable + 1;
26638let result = variable * 2;",
26639 );
26640}
26641
26642#[gpui::test]
26643async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26644 cx: &mut gpui::TestAppContext,
26645) {
26646 init_test(cx, |_| {});
26647
26648 let url = "https://zed.dev";
26649
26650 let markdown_language = Arc::new(Language::new(
26651 LanguageConfig {
26652 name: "Markdown".into(),
26653 ..LanguageConfig::default()
26654 },
26655 None,
26656 ));
26657
26658 let mut cx = EditorTestContext::new(cx).await;
26659 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26660 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26661
26662 cx.update_editor(|editor, window, cx| {
26663 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26664 editor.paste(&Paste, window, cx);
26665 });
26666
26667 cx.assert_editor_state(&format!(
26668 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26669 ));
26670}
26671
26672#[gpui::test]
26673async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26674 cx: &mut gpui::TestAppContext,
26675) {
26676 init_test(cx, |_| {});
26677
26678 let url = "https://zed.dev";
26679
26680 let markdown_language = Arc::new(Language::new(
26681 LanguageConfig {
26682 name: "Markdown".into(),
26683 ..LanguageConfig::default()
26684 },
26685 None,
26686 ));
26687
26688 let mut cx = EditorTestContext::new(cx).await;
26689 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26690 cx.set_state(&format!(
26691 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26692 ));
26693
26694 cx.update_editor(|editor, window, cx| {
26695 editor.copy(&Copy, window, cx);
26696 });
26697
26698 cx.set_state(&format!(
26699 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26700 ));
26701
26702 cx.update_editor(|editor, window, cx| {
26703 editor.paste(&Paste, window, cx);
26704 });
26705
26706 cx.assert_editor_state(&format!(
26707 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26708 ));
26709}
26710
26711#[gpui::test]
26712async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26713 cx: &mut gpui::TestAppContext,
26714) {
26715 init_test(cx, |_| {});
26716
26717 let url = "https://zed.dev";
26718
26719 let markdown_language = Arc::new(Language::new(
26720 LanguageConfig {
26721 name: "Markdown".into(),
26722 ..LanguageConfig::default()
26723 },
26724 None,
26725 ));
26726
26727 let mut cx = EditorTestContext::new(cx).await;
26728 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26729 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26730
26731 cx.update_editor(|editor, window, cx| {
26732 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26733 editor.paste(&Paste, window, cx);
26734 });
26735
26736 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26737}
26738
26739#[gpui::test]
26740async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26741 cx: &mut gpui::TestAppContext,
26742) {
26743 init_test(cx, |_| {});
26744
26745 let text = "Awesome";
26746
26747 let markdown_language = Arc::new(Language::new(
26748 LanguageConfig {
26749 name: "Markdown".into(),
26750 ..LanguageConfig::default()
26751 },
26752 None,
26753 ));
26754
26755 let mut cx = EditorTestContext::new(cx).await;
26756 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26757 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26758
26759 cx.update_editor(|editor, window, cx| {
26760 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26761 editor.paste(&Paste, window, cx);
26762 });
26763
26764 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26765}
26766
26767#[gpui::test]
26768async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26769 cx: &mut gpui::TestAppContext,
26770) {
26771 init_test(cx, |_| {});
26772
26773 let url = "https://zed.dev";
26774
26775 let markdown_language = Arc::new(Language::new(
26776 LanguageConfig {
26777 name: "Rust".into(),
26778 ..LanguageConfig::default()
26779 },
26780 None,
26781 ));
26782
26783 let mut cx = EditorTestContext::new(cx).await;
26784 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26785 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26786
26787 cx.update_editor(|editor, window, cx| {
26788 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26789 editor.paste(&Paste, window, cx);
26790 });
26791
26792 cx.assert_editor_state(&format!(
26793 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26794 ));
26795}
26796
26797#[gpui::test]
26798async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26799 cx: &mut TestAppContext,
26800) {
26801 init_test(cx, |_| {});
26802
26803 let url = "https://zed.dev";
26804
26805 let markdown_language = Arc::new(Language::new(
26806 LanguageConfig {
26807 name: "Markdown".into(),
26808 ..LanguageConfig::default()
26809 },
26810 None,
26811 ));
26812
26813 let (editor, cx) = cx.add_window_view(|window, cx| {
26814 let multi_buffer = MultiBuffer::build_multi(
26815 [
26816 ("this will embed -> link", vec![Point::row_range(0..1)]),
26817 ("this will replace -> link", vec![Point::row_range(0..1)]),
26818 ],
26819 cx,
26820 );
26821 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26823 s.select_ranges(vec![
26824 Point::new(0, 19)..Point::new(0, 23),
26825 Point::new(1, 21)..Point::new(1, 25),
26826 ])
26827 });
26828 let first_buffer_id = multi_buffer
26829 .read(cx)
26830 .excerpt_buffer_ids()
26831 .into_iter()
26832 .next()
26833 .unwrap();
26834 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26835 first_buffer.update(cx, |buffer, cx| {
26836 buffer.set_language(Some(markdown_language.clone()), cx);
26837 });
26838
26839 editor
26840 });
26841 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26842
26843 cx.update_editor(|editor, window, cx| {
26844 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26845 editor.paste(&Paste, window, cx);
26846 });
26847
26848 cx.assert_editor_state(&format!(
26849 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26850 ));
26851}
26852
26853#[gpui::test]
26854async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26855 init_test(cx, |_| {});
26856
26857 let fs = FakeFs::new(cx.executor());
26858 fs.insert_tree(
26859 path!("/project"),
26860 json!({
26861 "first.rs": "# First Document\nSome content here.",
26862 "second.rs": "Plain text content for second file.",
26863 }),
26864 )
26865 .await;
26866
26867 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26868 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26869 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26870
26871 let language = rust_lang();
26872 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26873 language_registry.add(language.clone());
26874 let mut fake_servers = language_registry.register_fake_lsp(
26875 "Rust",
26876 FakeLspAdapter {
26877 ..FakeLspAdapter::default()
26878 },
26879 );
26880
26881 let buffer1 = project
26882 .update(cx, |project, cx| {
26883 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26884 })
26885 .await
26886 .unwrap();
26887 let buffer2 = project
26888 .update(cx, |project, cx| {
26889 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26890 })
26891 .await
26892 .unwrap();
26893
26894 let multi_buffer = cx.new(|cx| {
26895 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26896 multi_buffer.set_excerpts_for_path(
26897 PathKey::for_buffer(&buffer1, cx),
26898 buffer1.clone(),
26899 [Point::zero()..buffer1.read(cx).max_point()],
26900 3,
26901 cx,
26902 );
26903 multi_buffer.set_excerpts_for_path(
26904 PathKey::for_buffer(&buffer2, cx),
26905 buffer2.clone(),
26906 [Point::zero()..buffer1.read(cx).max_point()],
26907 3,
26908 cx,
26909 );
26910 multi_buffer
26911 });
26912
26913 let (editor, cx) = cx.add_window_view(|window, cx| {
26914 Editor::new(
26915 EditorMode::full(),
26916 multi_buffer,
26917 Some(project.clone()),
26918 window,
26919 cx,
26920 )
26921 });
26922
26923 let fake_language_server = fake_servers.next().await.unwrap();
26924
26925 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26926
26927 let save = editor.update_in(cx, |editor, window, cx| {
26928 assert!(editor.is_dirty(cx));
26929
26930 editor.save(
26931 SaveOptions {
26932 format: true,
26933 autosave: true,
26934 },
26935 project,
26936 window,
26937 cx,
26938 )
26939 });
26940 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26941 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26942 let mut done_edit_rx = Some(done_edit_rx);
26943 let mut start_edit_tx = Some(start_edit_tx);
26944
26945 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26946 start_edit_tx.take().unwrap().send(()).unwrap();
26947 let done_edit_rx = done_edit_rx.take().unwrap();
26948 async move {
26949 done_edit_rx.await.unwrap();
26950 Ok(None)
26951 }
26952 });
26953
26954 start_edit_rx.await.unwrap();
26955 buffer2
26956 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26957 .unwrap();
26958
26959 done_edit_tx.send(()).unwrap();
26960
26961 save.await.unwrap();
26962 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26963}
26964
26965#[track_caller]
26966fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26967 editor
26968 .all_inlays(cx)
26969 .into_iter()
26970 .filter_map(|inlay| inlay.get_color())
26971 .map(Rgba::from)
26972 .collect()
26973}
26974
26975#[gpui::test]
26976fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26977 init_test(cx, |_| {});
26978
26979 let editor = cx.add_window(|window, cx| {
26980 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26981 build_editor(buffer, window, cx)
26982 });
26983
26984 editor
26985 .update(cx, |editor, window, cx| {
26986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26987 s.select_display_ranges([
26988 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26989 ])
26990 });
26991
26992 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26993
26994 assert_eq!(
26995 editor.display_text(cx),
26996 "line1\nline2\nline2",
26997 "Duplicating last line upward should create duplicate above, not on same line"
26998 );
26999
27000 assert_eq!(
27001 editor.selections.display_ranges(cx),
27002 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27003 "Selection should move to the duplicated line"
27004 );
27005 })
27006 .unwrap();
27007}
27008
27009#[gpui::test]
27010async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27011 init_test(cx, |_| {});
27012
27013 let mut cx = EditorTestContext::new(cx).await;
27014
27015 cx.set_state("line1\nline2ˇ");
27016
27017 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27018
27019 let clipboard_text = cx
27020 .read_from_clipboard()
27021 .and_then(|item| item.text().as_deref().map(str::to_string));
27022
27023 assert_eq!(
27024 clipboard_text,
27025 Some("line2\n".to_string()),
27026 "Copying a line without trailing newline should include a newline"
27027 );
27028
27029 cx.set_state("line1\nˇ");
27030
27031 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27032
27033 cx.assert_editor_state("line1\nline2\nˇ");
27034}
27035
27036#[gpui::test]
27037async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27038 init_test(cx, |_| {});
27039
27040 let mut cx = EditorTestContext::new(cx).await;
27041
27042 cx.set_state("line1\nline2ˇ");
27043 cx.update_editor(|e, window, cx| {
27044 e.set_mode(EditorMode::SingleLine);
27045 assert!(e.key_context(window, cx).contains("end_of_input"));
27046 });
27047 cx.set_state("ˇline1\nline2");
27048 cx.update_editor(|e, window, cx| {
27049 assert!(!e.key_context(window, cx).contains("end_of_input"));
27050 });
27051 cx.set_state("line1ˇ\nline2");
27052 cx.update_editor(|e, window, cx| {
27053 assert!(!e.key_context(window, cx).contains("end_of_input"));
27054 });
27055}
27056
27057#[gpui::test]
27058async fn test_next_prev_reference(cx: &mut TestAppContext) {
27059 const CYCLE_POSITIONS: &[&'static str] = &[
27060 indoc! {"
27061 fn foo() {
27062 let ˇabc = 123;
27063 let x = abc + 1;
27064 let y = abc + 2;
27065 let z = abc + 2;
27066 }
27067 "},
27068 indoc! {"
27069 fn foo() {
27070 let abc = 123;
27071 let x = ˇabc + 1;
27072 let y = abc + 2;
27073 let z = abc + 2;
27074 }
27075 "},
27076 indoc! {"
27077 fn foo() {
27078 let abc = 123;
27079 let x = abc + 1;
27080 let y = ˇabc + 2;
27081 let z = abc + 2;
27082 }
27083 "},
27084 indoc! {"
27085 fn foo() {
27086 let abc = 123;
27087 let x = abc + 1;
27088 let y = abc + 2;
27089 let z = ˇabc + 2;
27090 }
27091 "},
27092 ];
27093
27094 init_test(cx, |_| {});
27095
27096 let mut cx = EditorLspTestContext::new_rust(
27097 lsp::ServerCapabilities {
27098 references_provider: Some(lsp::OneOf::Left(true)),
27099 ..Default::default()
27100 },
27101 cx,
27102 )
27103 .await;
27104
27105 // importantly, the cursor is in the middle
27106 cx.set_state(indoc! {"
27107 fn foo() {
27108 let aˇbc = 123;
27109 let x = abc + 1;
27110 let y = abc + 2;
27111 let z = abc + 2;
27112 }
27113 "});
27114
27115 let reference_ranges = [
27116 lsp::Position::new(1, 8),
27117 lsp::Position::new(2, 12),
27118 lsp::Position::new(3, 12),
27119 lsp::Position::new(4, 12),
27120 ]
27121 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27122
27123 cx.lsp
27124 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27125 Ok(Some(
27126 reference_ranges
27127 .map(|range| lsp::Location {
27128 uri: params.text_document_position.text_document.uri.clone(),
27129 range,
27130 })
27131 .to_vec(),
27132 ))
27133 });
27134
27135 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27136 cx.update_editor(|editor, window, cx| {
27137 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27138 })
27139 .unwrap()
27140 .await
27141 .unwrap()
27142 };
27143
27144 _move(Direction::Next, 1, &mut cx).await;
27145 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27146
27147 _move(Direction::Next, 1, &mut cx).await;
27148 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27149
27150 _move(Direction::Next, 1, &mut cx).await;
27151 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27152
27153 // loops back to the start
27154 _move(Direction::Next, 1, &mut cx).await;
27155 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27156
27157 // loops back to the end
27158 _move(Direction::Prev, 1, &mut cx).await;
27159 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27160
27161 _move(Direction::Prev, 1, &mut cx).await;
27162 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27163
27164 _move(Direction::Prev, 1, &mut cx).await;
27165 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27166
27167 _move(Direction::Prev, 1, &mut cx).await;
27168 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27169
27170 _move(Direction::Next, 3, &mut cx).await;
27171 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27172
27173 _move(Direction::Prev, 2, &mut cx).await;
27174 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27175}