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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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.trigger_completion_on_input("n", true, window, cx)
15412 });
15413 cx.executor().run_until_parked();
15414
15415 cx.update_editor(|editor, window, cx| {
15416 editor.confirm_completion(&Default::default(), window, cx)
15417 });
15418 cx.executor().run_until_parked();
15419 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15420}
15421
15422#[gpui::test]
15423async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15424 init_test(cx, |_| {});
15425 let language =
15426 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15427 let mut cx = EditorLspTestContext::new(
15428 language,
15429 lsp::ServerCapabilities {
15430 completion_provider: Some(lsp::CompletionOptions {
15431 ..lsp::CompletionOptions::default()
15432 }),
15433 ..lsp::ServerCapabilities::default()
15434 },
15435 cx,
15436 )
15437 .await;
15438
15439 cx.set_state(
15440 "#ifndef BAR_H
15441#define BAR_H
15442
15443#include <stdbool.h>
15444
15445int fn_branch(bool do_branch1, bool do_branch2);
15446
15447#endif // BAR_H
15448ˇ",
15449 );
15450 cx.executor().run_until_parked();
15451 cx.update_editor(|editor, window, cx| {
15452 editor.handle_input("#", window, cx);
15453 });
15454 cx.executor().run_until_parked();
15455 cx.update_editor(|editor, window, cx| {
15456 editor.handle_input("i", window, cx);
15457 });
15458 cx.executor().run_until_parked();
15459 cx.update_editor(|editor, window, cx| {
15460 editor.handle_input("n", window, cx);
15461 });
15462 cx.executor().run_until_parked();
15463 cx.assert_editor_state(
15464 "#ifndef BAR_H
15465#define BAR_H
15466
15467#include <stdbool.h>
15468
15469int fn_branch(bool do_branch1, bool do_branch2);
15470
15471#endif // BAR_H
15472#inˇ",
15473 );
15474
15475 cx.lsp
15476 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15477 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15478 is_incomplete: false,
15479 item_defaults: None,
15480 items: vec![lsp::CompletionItem {
15481 kind: Some(lsp::CompletionItemKind::SNIPPET),
15482 label_details: Some(lsp::CompletionItemLabelDetails {
15483 detail: Some("header".to_string()),
15484 description: None,
15485 }),
15486 label: " include".to_string(),
15487 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15488 range: lsp::Range {
15489 start: lsp::Position {
15490 line: 8,
15491 character: 1,
15492 },
15493 end: lsp::Position {
15494 line: 8,
15495 character: 1,
15496 },
15497 },
15498 new_text: "include \"$0\"".to_string(),
15499 })),
15500 sort_text: Some("40b67681include".to_string()),
15501 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15502 filter_text: Some("include".to_string()),
15503 insert_text: Some("include \"$0\"".to_string()),
15504 ..lsp::CompletionItem::default()
15505 }],
15506 })))
15507 });
15508 cx.update_editor(|editor, window, cx| {
15509 editor.show_completions(&ShowCompletions, window, cx);
15510 });
15511 cx.executor().run_until_parked();
15512 cx.update_editor(|editor, window, cx| {
15513 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15514 });
15515 cx.executor().run_until_parked();
15516 cx.assert_editor_state(
15517 "#ifndef BAR_H
15518#define BAR_H
15519
15520#include <stdbool.h>
15521
15522int fn_branch(bool do_branch1, bool do_branch2);
15523
15524#endif // BAR_H
15525#include \"ˇ\"",
15526 );
15527
15528 cx.lsp
15529 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15530 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15531 is_incomplete: true,
15532 item_defaults: None,
15533 items: vec![lsp::CompletionItem {
15534 kind: Some(lsp::CompletionItemKind::FILE),
15535 label: "AGL/".to_string(),
15536 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15537 range: lsp::Range {
15538 start: lsp::Position {
15539 line: 8,
15540 character: 10,
15541 },
15542 end: lsp::Position {
15543 line: 8,
15544 character: 11,
15545 },
15546 },
15547 new_text: "AGL/".to_string(),
15548 })),
15549 sort_text: Some("40b67681AGL/".to_string()),
15550 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15551 filter_text: Some("AGL/".to_string()),
15552 insert_text: Some("AGL/".to_string()),
15553 ..lsp::CompletionItem::default()
15554 }],
15555 })))
15556 });
15557 cx.update_editor(|editor, window, cx| {
15558 editor.show_completions(&ShowCompletions, window, cx);
15559 });
15560 cx.executor().run_until_parked();
15561 cx.update_editor(|editor, window, cx| {
15562 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15563 });
15564 cx.executor().run_until_parked();
15565 cx.assert_editor_state(
15566 r##"#ifndef BAR_H
15567#define BAR_H
15568
15569#include <stdbool.h>
15570
15571int fn_branch(bool do_branch1, bool do_branch2);
15572
15573#endif // BAR_H
15574#include "AGL/ˇ"##,
15575 );
15576
15577 cx.update_editor(|editor, window, cx| {
15578 editor.handle_input("\"", window, cx);
15579 });
15580 cx.executor().run_until_parked();
15581 cx.assert_editor_state(
15582 r##"#ifndef BAR_H
15583#define BAR_H
15584
15585#include <stdbool.h>
15586
15587int fn_branch(bool do_branch1, bool do_branch2);
15588
15589#endif // BAR_H
15590#include "AGL/"ˇ"##,
15591 );
15592}
15593
15594#[gpui::test]
15595async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15596 init_test(cx, |_| {});
15597
15598 let mut cx = EditorLspTestContext::new_rust(
15599 lsp::ServerCapabilities {
15600 completion_provider: Some(lsp::CompletionOptions {
15601 trigger_characters: Some(vec![".".to_string()]),
15602 resolve_provider: Some(true),
15603 ..Default::default()
15604 }),
15605 ..Default::default()
15606 },
15607 cx,
15608 )
15609 .await;
15610
15611 cx.set_state("fn main() { let a = 2ˇ; }");
15612 cx.simulate_keystroke(".");
15613 let completion_item = lsp::CompletionItem {
15614 label: "Some".into(),
15615 kind: Some(lsp::CompletionItemKind::SNIPPET),
15616 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15617 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15618 kind: lsp::MarkupKind::Markdown,
15619 value: "```rust\nSome(2)\n```".to_string(),
15620 })),
15621 deprecated: Some(false),
15622 sort_text: Some("Some".to_string()),
15623 filter_text: Some("Some".to_string()),
15624 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15625 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15626 range: lsp::Range {
15627 start: lsp::Position {
15628 line: 0,
15629 character: 22,
15630 },
15631 end: lsp::Position {
15632 line: 0,
15633 character: 22,
15634 },
15635 },
15636 new_text: "Some(2)".to_string(),
15637 })),
15638 additional_text_edits: Some(vec![lsp::TextEdit {
15639 range: lsp::Range {
15640 start: lsp::Position {
15641 line: 0,
15642 character: 20,
15643 },
15644 end: lsp::Position {
15645 line: 0,
15646 character: 22,
15647 },
15648 },
15649 new_text: "".to_string(),
15650 }]),
15651 ..Default::default()
15652 };
15653
15654 let closure_completion_item = completion_item.clone();
15655 let counter = Arc::new(AtomicUsize::new(0));
15656 let counter_clone = counter.clone();
15657 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15658 let task_completion_item = closure_completion_item.clone();
15659 counter_clone.fetch_add(1, atomic::Ordering::Release);
15660 async move {
15661 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15662 is_incomplete: true,
15663 item_defaults: None,
15664 items: vec![task_completion_item],
15665 })))
15666 }
15667 });
15668
15669 cx.condition(|editor, _| editor.context_menu_visible())
15670 .await;
15671 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15672 assert!(request.next().await.is_some());
15673 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15674
15675 cx.simulate_keystrokes("S o m");
15676 cx.condition(|editor, _| editor.context_menu_visible())
15677 .await;
15678 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15679 assert!(request.next().await.is_some());
15680 assert!(request.next().await.is_some());
15681 assert!(request.next().await.is_some());
15682 request.close();
15683 assert!(request.next().await.is_none());
15684 assert_eq!(
15685 counter.load(atomic::Ordering::Acquire),
15686 4,
15687 "With the completions menu open, only one LSP request should happen per input"
15688 );
15689}
15690
15691#[gpui::test]
15692async fn test_toggle_comment(cx: &mut TestAppContext) {
15693 init_test(cx, |_| {});
15694 let mut cx = EditorTestContext::new(cx).await;
15695 let language = Arc::new(Language::new(
15696 LanguageConfig {
15697 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15698 ..Default::default()
15699 },
15700 Some(tree_sitter_rust::LANGUAGE.into()),
15701 ));
15702 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15703
15704 // If multiple selections intersect a line, the line is only toggled once.
15705 cx.set_state(indoc! {"
15706 fn a() {
15707 «//b();
15708 ˇ»// «c();
15709 //ˇ» d();
15710 }
15711 "});
15712
15713 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15714
15715 cx.assert_editor_state(indoc! {"
15716 fn a() {
15717 «b();
15718 c();
15719 ˇ» d();
15720 }
15721 "});
15722
15723 // The comment prefix is inserted at the same column for every line in a
15724 // selection.
15725 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15726
15727 cx.assert_editor_state(indoc! {"
15728 fn a() {
15729 // «b();
15730 // c();
15731 ˇ»// d();
15732 }
15733 "});
15734
15735 // If a selection ends at the beginning of a line, that line is not toggled.
15736 cx.set_selections_state(indoc! {"
15737 fn a() {
15738 // b();
15739 «// c();
15740 ˇ» // d();
15741 }
15742 "});
15743
15744 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15745
15746 cx.assert_editor_state(indoc! {"
15747 fn a() {
15748 // b();
15749 «c();
15750 ˇ» // d();
15751 }
15752 "});
15753
15754 // If a selection span a single line and is empty, the line is toggled.
15755 cx.set_state(indoc! {"
15756 fn a() {
15757 a();
15758 b();
15759 ˇ
15760 }
15761 "});
15762
15763 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15764
15765 cx.assert_editor_state(indoc! {"
15766 fn a() {
15767 a();
15768 b();
15769 //•ˇ
15770 }
15771 "});
15772
15773 // If a selection span multiple lines, empty lines are not toggled.
15774 cx.set_state(indoc! {"
15775 fn a() {
15776 «a();
15777
15778 c();ˇ»
15779 }
15780 "});
15781
15782 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15783
15784 cx.assert_editor_state(indoc! {"
15785 fn a() {
15786 // «a();
15787
15788 // c();ˇ»
15789 }
15790 "});
15791
15792 // If a selection includes multiple comment prefixes, all lines are uncommented.
15793 cx.set_state(indoc! {"
15794 fn a() {
15795 «// a();
15796 /// b();
15797 //! c();ˇ»
15798 }
15799 "});
15800
15801 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15802
15803 cx.assert_editor_state(indoc! {"
15804 fn a() {
15805 «a();
15806 b();
15807 c();ˇ»
15808 }
15809 "});
15810}
15811
15812#[gpui::test]
15813async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15814 init_test(cx, |_| {});
15815 let mut cx = EditorTestContext::new(cx).await;
15816 let language = Arc::new(Language::new(
15817 LanguageConfig {
15818 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15819 ..Default::default()
15820 },
15821 Some(tree_sitter_rust::LANGUAGE.into()),
15822 ));
15823 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15824
15825 let toggle_comments = &ToggleComments {
15826 advance_downwards: false,
15827 ignore_indent: true,
15828 };
15829
15830 // If multiple selections intersect a line, the line is only toggled once.
15831 cx.set_state(indoc! {"
15832 fn a() {
15833 // «b();
15834 // c();
15835 // ˇ» d();
15836 }
15837 "});
15838
15839 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15840
15841 cx.assert_editor_state(indoc! {"
15842 fn a() {
15843 «b();
15844 c();
15845 ˇ» d();
15846 }
15847 "});
15848
15849 // The comment prefix is inserted at the beginning of each line
15850 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15851
15852 cx.assert_editor_state(indoc! {"
15853 fn a() {
15854 // «b();
15855 // c();
15856 // ˇ» d();
15857 }
15858 "});
15859
15860 // If a selection ends at the beginning of a line, that line is not toggled.
15861 cx.set_selections_state(indoc! {"
15862 fn a() {
15863 // b();
15864 // «c();
15865 ˇ»// d();
15866 }
15867 "});
15868
15869 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15870
15871 cx.assert_editor_state(indoc! {"
15872 fn a() {
15873 // b();
15874 «c();
15875 ˇ»// d();
15876 }
15877 "});
15878
15879 // If a selection span a single line and is empty, the line is toggled.
15880 cx.set_state(indoc! {"
15881 fn a() {
15882 a();
15883 b();
15884 ˇ
15885 }
15886 "});
15887
15888 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15889
15890 cx.assert_editor_state(indoc! {"
15891 fn a() {
15892 a();
15893 b();
15894 //ˇ
15895 }
15896 "});
15897
15898 // If a selection span multiple lines, empty lines are not toggled.
15899 cx.set_state(indoc! {"
15900 fn a() {
15901 «a();
15902
15903 c();ˇ»
15904 }
15905 "});
15906
15907 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15908
15909 cx.assert_editor_state(indoc! {"
15910 fn a() {
15911 // «a();
15912
15913 // c();ˇ»
15914 }
15915 "});
15916
15917 // If a selection includes multiple comment prefixes, all lines are uncommented.
15918 cx.set_state(indoc! {"
15919 fn a() {
15920 // «a();
15921 /// b();
15922 //! c();ˇ»
15923 }
15924 "});
15925
15926 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15927
15928 cx.assert_editor_state(indoc! {"
15929 fn a() {
15930 «a();
15931 b();
15932 c();ˇ»
15933 }
15934 "});
15935}
15936
15937#[gpui::test]
15938async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15939 init_test(cx, |_| {});
15940
15941 let language = Arc::new(Language::new(
15942 LanguageConfig {
15943 line_comments: vec!["// ".into()],
15944 ..Default::default()
15945 },
15946 Some(tree_sitter_rust::LANGUAGE.into()),
15947 ));
15948
15949 let mut cx = EditorTestContext::new(cx).await;
15950
15951 cx.language_registry().add(language.clone());
15952 cx.update_buffer(|buffer, cx| {
15953 buffer.set_language(Some(language), cx);
15954 });
15955
15956 let toggle_comments = &ToggleComments {
15957 advance_downwards: true,
15958 ignore_indent: false,
15959 };
15960
15961 // Single cursor on one line -> advance
15962 // Cursor moves horizontally 3 characters as well on non-blank line
15963 cx.set_state(indoc!(
15964 "fn a() {
15965 ˇdog();
15966 cat();
15967 }"
15968 ));
15969 cx.update_editor(|editor, window, cx| {
15970 editor.toggle_comments(toggle_comments, window, cx);
15971 });
15972 cx.assert_editor_state(indoc!(
15973 "fn a() {
15974 // dog();
15975 catˇ();
15976 }"
15977 ));
15978
15979 // Single selection on one line -> don't advance
15980 cx.set_state(indoc!(
15981 "fn a() {
15982 «dog()ˇ»;
15983 cat();
15984 }"
15985 ));
15986 cx.update_editor(|editor, window, cx| {
15987 editor.toggle_comments(toggle_comments, window, cx);
15988 });
15989 cx.assert_editor_state(indoc!(
15990 "fn a() {
15991 // «dog()ˇ»;
15992 cat();
15993 }"
15994 ));
15995
15996 // Multiple cursors on one line -> advance
15997 cx.set_state(indoc!(
15998 "fn a() {
15999 ˇdˇog();
16000 cat();
16001 }"
16002 ));
16003 cx.update_editor(|editor, window, cx| {
16004 editor.toggle_comments(toggle_comments, window, cx);
16005 });
16006 cx.assert_editor_state(indoc!(
16007 "fn a() {
16008 // dog();
16009 catˇ(ˇ);
16010 }"
16011 ));
16012
16013 // Multiple cursors on one line, with selection -> don't advance
16014 cx.set_state(indoc!(
16015 "fn a() {
16016 ˇdˇog«()ˇ»;
16017 cat();
16018 }"
16019 ));
16020 cx.update_editor(|editor, window, cx| {
16021 editor.toggle_comments(toggle_comments, window, cx);
16022 });
16023 cx.assert_editor_state(indoc!(
16024 "fn a() {
16025 // ˇdˇog«()ˇ»;
16026 cat();
16027 }"
16028 ));
16029
16030 // Single cursor on one line -> advance
16031 // Cursor moves to column 0 on blank line
16032 cx.set_state(indoc!(
16033 "fn a() {
16034 ˇdog();
16035
16036 cat();
16037 }"
16038 ));
16039 cx.update_editor(|editor, window, cx| {
16040 editor.toggle_comments(toggle_comments, window, cx);
16041 });
16042 cx.assert_editor_state(indoc!(
16043 "fn a() {
16044 // dog();
16045 ˇ
16046 cat();
16047 }"
16048 ));
16049
16050 // Single cursor on one line -> advance
16051 // Cursor starts and ends at column 0
16052 cx.set_state(indoc!(
16053 "fn a() {
16054 ˇ dog();
16055 cat();
16056 }"
16057 ));
16058 cx.update_editor(|editor, window, cx| {
16059 editor.toggle_comments(toggle_comments, window, cx);
16060 });
16061 cx.assert_editor_state(indoc!(
16062 "fn a() {
16063 // dog();
16064 ˇ cat();
16065 }"
16066 ));
16067}
16068
16069#[gpui::test]
16070async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16071 init_test(cx, |_| {});
16072
16073 let mut cx = EditorTestContext::new(cx).await;
16074
16075 let html_language = Arc::new(
16076 Language::new(
16077 LanguageConfig {
16078 name: "HTML".into(),
16079 block_comment: Some(BlockCommentConfig {
16080 start: "<!-- ".into(),
16081 prefix: "".into(),
16082 end: " -->".into(),
16083 tab_size: 0,
16084 }),
16085 ..Default::default()
16086 },
16087 Some(tree_sitter_html::LANGUAGE.into()),
16088 )
16089 .with_injection_query(
16090 r#"
16091 (script_element
16092 (raw_text) @injection.content
16093 (#set! injection.language "javascript"))
16094 "#,
16095 )
16096 .unwrap(),
16097 );
16098
16099 let javascript_language = Arc::new(Language::new(
16100 LanguageConfig {
16101 name: "JavaScript".into(),
16102 line_comments: vec!["// ".into()],
16103 ..Default::default()
16104 },
16105 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16106 ));
16107
16108 cx.language_registry().add(html_language.clone());
16109 cx.language_registry().add(javascript_language);
16110 cx.update_buffer(|buffer, cx| {
16111 buffer.set_language(Some(html_language), cx);
16112 });
16113
16114 // Toggle comments for empty selections
16115 cx.set_state(
16116 &r#"
16117 <p>A</p>ˇ
16118 <p>B</p>ˇ
16119 <p>C</p>ˇ
16120 "#
16121 .unindent(),
16122 );
16123 cx.update_editor(|editor, window, cx| {
16124 editor.toggle_comments(&ToggleComments::default(), window, cx)
16125 });
16126 cx.assert_editor_state(
16127 &r#"
16128 <!-- <p>A</p>ˇ -->
16129 <!-- <p>B</p>ˇ -->
16130 <!-- <p>C</p>ˇ -->
16131 "#
16132 .unindent(),
16133 );
16134 cx.update_editor(|editor, window, cx| {
16135 editor.toggle_comments(&ToggleComments::default(), window, cx)
16136 });
16137 cx.assert_editor_state(
16138 &r#"
16139 <p>A</p>ˇ
16140 <p>B</p>ˇ
16141 <p>C</p>ˇ
16142 "#
16143 .unindent(),
16144 );
16145
16146 // Toggle comments for mixture of empty and non-empty selections, where
16147 // multiple selections occupy a given line.
16148 cx.set_state(
16149 &r#"
16150 <p>A«</p>
16151 <p>ˇ»B</p>ˇ
16152 <p>C«</p>
16153 <p>ˇ»D</p>ˇ
16154 "#
16155 .unindent(),
16156 );
16157
16158 cx.update_editor(|editor, window, cx| {
16159 editor.toggle_comments(&ToggleComments::default(), window, cx)
16160 });
16161 cx.assert_editor_state(
16162 &r#"
16163 <!-- <p>A«</p>
16164 <p>ˇ»B</p>ˇ -->
16165 <!-- <p>C«</p>
16166 <p>ˇ»D</p>ˇ -->
16167 "#
16168 .unindent(),
16169 );
16170 cx.update_editor(|editor, window, cx| {
16171 editor.toggle_comments(&ToggleComments::default(), window, cx)
16172 });
16173 cx.assert_editor_state(
16174 &r#"
16175 <p>A«</p>
16176 <p>ˇ»B</p>ˇ
16177 <p>C«</p>
16178 <p>ˇ»D</p>ˇ
16179 "#
16180 .unindent(),
16181 );
16182
16183 // Toggle comments when different languages are active for different
16184 // selections.
16185 cx.set_state(
16186 &r#"
16187 ˇ<script>
16188 ˇvar x = new Y();
16189 ˇ</script>
16190 "#
16191 .unindent(),
16192 );
16193 cx.executor().run_until_parked();
16194 cx.update_editor(|editor, window, cx| {
16195 editor.toggle_comments(&ToggleComments::default(), window, cx)
16196 });
16197 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16198 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16199 cx.assert_editor_state(
16200 &r#"
16201 <!-- ˇ<script> -->
16202 // ˇvar x = new Y();
16203 <!-- ˇ</script> -->
16204 "#
16205 .unindent(),
16206 );
16207}
16208
16209#[gpui::test]
16210fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16211 init_test(cx, |_| {});
16212
16213 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16214 let multibuffer = cx.new(|cx| {
16215 let mut multibuffer = MultiBuffer::new(ReadWrite);
16216 multibuffer.push_excerpts(
16217 buffer.clone(),
16218 [
16219 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16220 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16221 ],
16222 cx,
16223 );
16224 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16225 multibuffer
16226 });
16227
16228 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16229 editor.update_in(cx, |editor, window, cx| {
16230 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16231 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16232 s.select_ranges([
16233 Point::new(0, 0)..Point::new(0, 0),
16234 Point::new(1, 0)..Point::new(1, 0),
16235 ])
16236 });
16237
16238 editor.handle_input("X", window, cx);
16239 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16240 assert_eq!(
16241 editor.selections.ranges(&editor.display_snapshot(cx)),
16242 [
16243 Point::new(0, 1)..Point::new(0, 1),
16244 Point::new(1, 1)..Point::new(1, 1),
16245 ]
16246 );
16247
16248 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16250 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16251 });
16252 editor.backspace(&Default::default(), window, cx);
16253 assert_eq!(editor.text(cx), "Xa\nbbb");
16254 assert_eq!(
16255 editor.selections.ranges(&editor.display_snapshot(cx)),
16256 [Point::new(1, 0)..Point::new(1, 0)]
16257 );
16258
16259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16260 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16261 });
16262 editor.backspace(&Default::default(), window, cx);
16263 assert_eq!(editor.text(cx), "X\nbb");
16264 assert_eq!(
16265 editor.selections.ranges(&editor.display_snapshot(cx)),
16266 [Point::new(0, 1)..Point::new(0, 1)]
16267 );
16268 });
16269}
16270
16271#[gpui::test]
16272fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16273 init_test(cx, |_| {});
16274
16275 let markers = vec![('[', ']').into(), ('(', ')').into()];
16276 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16277 indoc! {"
16278 [aaaa
16279 (bbbb]
16280 cccc)",
16281 },
16282 markers.clone(),
16283 );
16284 let excerpt_ranges = markers.into_iter().map(|marker| {
16285 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16286 ExcerptRange::new(context)
16287 });
16288 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16289 let multibuffer = cx.new(|cx| {
16290 let mut multibuffer = MultiBuffer::new(ReadWrite);
16291 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16292 multibuffer
16293 });
16294
16295 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16296 editor.update_in(cx, |editor, window, cx| {
16297 let (expected_text, selection_ranges) = marked_text_ranges(
16298 indoc! {"
16299 aaaa
16300 bˇbbb
16301 bˇbbˇb
16302 cccc"
16303 },
16304 true,
16305 );
16306 assert_eq!(editor.text(cx), expected_text);
16307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16308 s.select_ranges(selection_ranges)
16309 });
16310
16311 editor.handle_input("X", window, cx);
16312
16313 let (expected_text, expected_selections) = marked_text_ranges(
16314 indoc! {"
16315 aaaa
16316 bXˇbbXb
16317 bXˇbbXˇb
16318 cccc"
16319 },
16320 false,
16321 );
16322 assert_eq!(editor.text(cx), expected_text);
16323 assert_eq!(
16324 editor.selections.ranges(&editor.display_snapshot(cx)),
16325 expected_selections
16326 );
16327
16328 editor.newline(&Newline, window, cx);
16329 let (expected_text, expected_selections) = marked_text_ranges(
16330 indoc! {"
16331 aaaa
16332 bX
16333 ˇbbX
16334 b
16335 bX
16336 ˇbbX
16337 ˇb
16338 cccc"
16339 },
16340 false,
16341 );
16342 assert_eq!(editor.text(cx), expected_text);
16343 assert_eq!(
16344 editor.selections.ranges(&editor.display_snapshot(cx)),
16345 expected_selections
16346 );
16347 });
16348}
16349
16350#[gpui::test]
16351fn test_refresh_selections(cx: &mut TestAppContext) {
16352 init_test(cx, |_| {});
16353
16354 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16355 let mut excerpt1_id = None;
16356 let multibuffer = cx.new(|cx| {
16357 let mut multibuffer = MultiBuffer::new(ReadWrite);
16358 excerpt1_id = multibuffer
16359 .push_excerpts(
16360 buffer.clone(),
16361 [
16362 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16363 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16364 ],
16365 cx,
16366 )
16367 .into_iter()
16368 .next();
16369 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16370 multibuffer
16371 });
16372
16373 let editor = cx.add_window(|window, cx| {
16374 let mut editor = build_editor(multibuffer.clone(), window, cx);
16375 let snapshot = editor.snapshot(window, cx);
16376 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16377 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16378 });
16379 editor.begin_selection(
16380 Point::new(2, 1).to_display_point(&snapshot),
16381 true,
16382 1,
16383 window,
16384 cx,
16385 );
16386 assert_eq!(
16387 editor.selections.ranges(&editor.display_snapshot(cx)),
16388 [
16389 Point::new(1, 3)..Point::new(1, 3),
16390 Point::new(2, 1)..Point::new(2, 1),
16391 ]
16392 );
16393 editor
16394 });
16395
16396 // Refreshing selections is a no-op when excerpts haven't changed.
16397 _ = editor.update(cx, |editor, window, cx| {
16398 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16399 assert_eq!(
16400 editor.selections.ranges(&editor.display_snapshot(cx)),
16401 [
16402 Point::new(1, 3)..Point::new(1, 3),
16403 Point::new(2, 1)..Point::new(2, 1),
16404 ]
16405 );
16406 });
16407
16408 multibuffer.update(cx, |multibuffer, cx| {
16409 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16410 });
16411 _ = editor.update(cx, |editor, window, cx| {
16412 // Removing an excerpt causes the first selection to become degenerate.
16413 assert_eq!(
16414 editor.selections.ranges(&editor.display_snapshot(cx)),
16415 [
16416 Point::new(0, 0)..Point::new(0, 0),
16417 Point::new(0, 1)..Point::new(0, 1)
16418 ]
16419 );
16420
16421 // Refreshing selections will relocate the first selection to the original buffer
16422 // location.
16423 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16424 assert_eq!(
16425 editor.selections.ranges(&editor.display_snapshot(cx)),
16426 [
16427 Point::new(0, 1)..Point::new(0, 1),
16428 Point::new(0, 3)..Point::new(0, 3)
16429 ]
16430 );
16431 assert!(editor.selections.pending_anchor().is_some());
16432 });
16433}
16434
16435#[gpui::test]
16436fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16437 init_test(cx, |_| {});
16438
16439 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16440 let mut excerpt1_id = None;
16441 let multibuffer = cx.new(|cx| {
16442 let mut multibuffer = MultiBuffer::new(ReadWrite);
16443 excerpt1_id = multibuffer
16444 .push_excerpts(
16445 buffer.clone(),
16446 [
16447 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16448 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16449 ],
16450 cx,
16451 )
16452 .into_iter()
16453 .next();
16454 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16455 multibuffer
16456 });
16457
16458 let editor = cx.add_window(|window, cx| {
16459 let mut editor = build_editor(multibuffer.clone(), window, cx);
16460 let snapshot = editor.snapshot(window, cx);
16461 editor.begin_selection(
16462 Point::new(1, 3).to_display_point(&snapshot),
16463 false,
16464 1,
16465 window,
16466 cx,
16467 );
16468 assert_eq!(
16469 editor.selections.ranges(&editor.display_snapshot(cx)),
16470 [Point::new(1, 3)..Point::new(1, 3)]
16471 );
16472 editor
16473 });
16474
16475 multibuffer.update(cx, |multibuffer, cx| {
16476 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16477 });
16478 _ = editor.update(cx, |editor, window, cx| {
16479 assert_eq!(
16480 editor.selections.ranges(&editor.display_snapshot(cx)),
16481 [Point::new(0, 0)..Point::new(0, 0)]
16482 );
16483
16484 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16485 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16486 assert_eq!(
16487 editor.selections.ranges(&editor.display_snapshot(cx)),
16488 [Point::new(0, 3)..Point::new(0, 3)]
16489 );
16490 assert!(editor.selections.pending_anchor().is_some());
16491 });
16492}
16493
16494#[gpui::test]
16495async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16496 init_test(cx, |_| {});
16497
16498 let language = Arc::new(
16499 Language::new(
16500 LanguageConfig {
16501 brackets: BracketPairConfig {
16502 pairs: vec![
16503 BracketPair {
16504 start: "{".to_string(),
16505 end: "}".to_string(),
16506 close: true,
16507 surround: true,
16508 newline: true,
16509 },
16510 BracketPair {
16511 start: "/* ".to_string(),
16512 end: " */".to_string(),
16513 close: true,
16514 surround: true,
16515 newline: true,
16516 },
16517 ],
16518 ..Default::default()
16519 },
16520 ..Default::default()
16521 },
16522 Some(tree_sitter_rust::LANGUAGE.into()),
16523 )
16524 .with_indents_query("")
16525 .unwrap(),
16526 );
16527
16528 let text = concat!(
16529 "{ }\n", //
16530 " x\n", //
16531 " /* */\n", //
16532 "x\n", //
16533 "{{} }\n", //
16534 );
16535
16536 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16537 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16538 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16539 editor
16540 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16541 .await;
16542
16543 editor.update_in(cx, |editor, window, cx| {
16544 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16545 s.select_display_ranges([
16546 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16547 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16548 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16549 ])
16550 });
16551 editor.newline(&Newline, window, cx);
16552
16553 assert_eq!(
16554 editor.buffer().read(cx).read(cx).text(),
16555 concat!(
16556 "{ \n", // Suppress rustfmt
16557 "\n", //
16558 "}\n", //
16559 " x\n", //
16560 " /* \n", //
16561 " \n", //
16562 " */\n", //
16563 "x\n", //
16564 "{{} \n", //
16565 "}\n", //
16566 )
16567 );
16568 });
16569}
16570
16571#[gpui::test]
16572fn test_highlighted_ranges(cx: &mut TestAppContext) {
16573 init_test(cx, |_| {});
16574
16575 let editor = cx.add_window(|window, cx| {
16576 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16577 build_editor(buffer, window, cx)
16578 });
16579
16580 _ = editor.update(cx, |editor, window, cx| {
16581 struct Type1;
16582 struct Type2;
16583
16584 let buffer = editor.buffer.read(cx).snapshot(cx);
16585
16586 let anchor_range =
16587 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16588
16589 editor.highlight_background::<Type1>(
16590 &[
16591 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16592 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16593 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16594 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16595 ],
16596 |_| Hsla::red(),
16597 cx,
16598 );
16599 editor.highlight_background::<Type2>(
16600 &[
16601 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16602 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16603 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16604 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16605 ],
16606 |_| Hsla::green(),
16607 cx,
16608 );
16609
16610 let snapshot = editor.snapshot(window, cx);
16611 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16612 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16613 &snapshot,
16614 cx.theme(),
16615 );
16616 assert_eq!(
16617 highlighted_ranges,
16618 &[
16619 (
16620 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16621 Hsla::green(),
16622 ),
16623 (
16624 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16625 Hsla::red(),
16626 ),
16627 (
16628 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16629 Hsla::green(),
16630 ),
16631 (
16632 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16633 Hsla::red(),
16634 ),
16635 ]
16636 );
16637 assert_eq!(
16638 editor.sorted_background_highlights_in_range(
16639 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16640 &snapshot,
16641 cx.theme(),
16642 ),
16643 &[(
16644 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16645 Hsla::red(),
16646 )]
16647 );
16648 });
16649}
16650
16651#[gpui::test]
16652async fn test_following(cx: &mut TestAppContext) {
16653 init_test(cx, |_| {});
16654
16655 let fs = FakeFs::new(cx.executor());
16656 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16657
16658 let buffer = project.update(cx, |project, cx| {
16659 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16660 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16661 });
16662 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16663 let follower = cx.update(|cx| {
16664 cx.open_window(
16665 WindowOptions {
16666 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16667 gpui::Point::new(px(0.), px(0.)),
16668 gpui::Point::new(px(10.), px(80.)),
16669 ))),
16670 ..Default::default()
16671 },
16672 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16673 )
16674 .unwrap()
16675 });
16676
16677 let is_still_following = Rc::new(RefCell::new(true));
16678 let follower_edit_event_count = Rc::new(RefCell::new(0));
16679 let pending_update = Rc::new(RefCell::new(None));
16680 let leader_entity = leader.root(cx).unwrap();
16681 let follower_entity = follower.root(cx).unwrap();
16682 _ = follower.update(cx, {
16683 let update = pending_update.clone();
16684 let is_still_following = is_still_following.clone();
16685 let follower_edit_event_count = follower_edit_event_count.clone();
16686 |_, window, cx| {
16687 cx.subscribe_in(
16688 &leader_entity,
16689 window,
16690 move |_, leader, event, window, cx| {
16691 leader.read(cx).add_event_to_update_proto(
16692 event,
16693 &mut update.borrow_mut(),
16694 window,
16695 cx,
16696 );
16697 },
16698 )
16699 .detach();
16700
16701 cx.subscribe_in(
16702 &follower_entity,
16703 window,
16704 move |_, _, event: &EditorEvent, _window, _cx| {
16705 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16706 *is_still_following.borrow_mut() = false;
16707 }
16708
16709 if let EditorEvent::BufferEdited = event {
16710 *follower_edit_event_count.borrow_mut() += 1;
16711 }
16712 },
16713 )
16714 .detach();
16715 }
16716 });
16717
16718 // Update the selections only
16719 _ = leader.update(cx, |leader, window, cx| {
16720 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16721 s.select_ranges([1..1])
16722 });
16723 });
16724 follower
16725 .update(cx, |follower, window, cx| {
16726 follower.apply_update_proto(
16727 &project,
16728 pending_update.borrow_mut().take().unwrap(),
16729 window,
16730 cx,
16731 )
16732 })
16733 .unwrap()
16734 .await
16735 .unwrap();
16736 _ = follower.update(cx, |follower, _, cx| {
16737 assert_eq!(
16738 follower.selections.ranges(&follower.display_snapshot(cx)),
16739 vec![1..1]
16740 );
16741 });
16742 assert!(*is_still_following.borrow());
16743 assert_eq!(*follower_edit_event_count.borrow(), 0);
16744
16745 // Update the scroll position only
16746 _ = leader.update(cx, |leader, window, cx| {
16747 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16748 });
16749 follower
16750 .update(cx, |follower, window, cx| {
16751 follower.apply_update_proto(
16752 &project,
16753 pending_update.borrow_mut().take().unwrap(),
16754 window,
16755 cx,
16756 )
16757 })
16758 .unwrap()
16759 .await
16760 .unwrap();
16761 assert_eq!(
16762 follower
16763 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16764 .unwrap(),
16765 gpui::Point::new(1.5, 3.5)
16766 );
16767 assert!(*is_still_following.borrow());
16768 assert_eq!(*follower_edit_event_count.borrow(), 0);
16769
16770 // Update the selections and scroll position. The follower's scroll position is updated
16771 // via autoscroll, not via the leader's exact scroll position.
16772 _ = leader.update(cx, |leader, window, cx| {
16773 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16774 s.select_ranges([0..0])
16775 });
16776 leader.request_autoscroll(Autoscroll::newest(), cx);
16777 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16778 });
16779 follower
16780 .update(cx, |follower, window, cx| {
16781 follower.apply_update_proto(
16782 &project,
16783 pending_update.borrow_mut().take().unwrap(),
16784 window,
16785 cx,
16786 )
16787 })
16788 .unwrap()
16789 .await
16790 .unwrap();
16791 _ = follower.update(cx, |follower, _, cx| {
16792 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16793 assert_eq!(
16794 follower.selections.ranges(&follower.display_snapshot(cx)),
16795 vec![0..0]
16796 );
16797 });
16798 assert!(*is_still_following.borrow());
16799
16800 // Creating a pending selection that precedes another selection
16801 _ = leader.update(cx, |leader, window, cx| {
16802 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16803 s.select_ranges([1..1])
16804 });
16805 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16806 });
16807 follower
16808 .update(cx, |follower, window, cx| {
16809 follower.apply_update_proto(
16810 &project,
16811 pending_update.borrow_mut().take().unwrap(),
16812 window,
16813 cx,
16814 )
16815 })
16816 .unwrap()
16817 .await
16818 .unwrap();
16819 _ = follower.update(cx, |follower, _, cx| {
16820 assert_eq!(
16821 follower.selections.ranges(&follower.display_snapshot(cx)),
16822 vec![0..0, 1..1]
16823 );
16824 });
16825 assert!(*is_still_following.borrow());
16826
16827 // Extend the pending selection so that it surrounds another selection
16828 _ = leader.update(cx, |leader, window, cx| {
16829 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16830 });
16831 follower
16832 .update(cx, |follower, window, cx| {
16833 follower.apply_update_proto(
16834 &project,
16835 pending_update.borrow_mut().take().unwrap(),
16836 window,
16837 cx,
16838 )
16839 })
16840 .unwrap()
16841 .await
16842 .unwrap();
16843 _ = follower.update(cx, |follower, _, cx| {
16844 assert_eq!(
16845 follower.selections.ranges(&follower.display_snapshot(cx)),
16846 vec![0..2]
16847 );
16848 });
16849
16850 // Scrolling locally breaks the follow
16851 _ = follower.update(cx, |follower, window, cx| {
16852 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16853 follower.set_scroll_anchor(
16854 ScrollAnchor {
16855 anchor: top_anchor,
16856 offset: gpui::Point::new(0.0, 0.5),
16857 },
16858 window,
16859 cx,
16860 );
16861 });
16862 assert!(!(*is_still_following.borrow()));
16863}
16864
16865#[gpui::test]
16866async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16867 init_test(cx, |_| {});
16868
16869 let fs = FakeFs::new(cx.executor());
16870 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16871 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16872 let pane = workspace
16873 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16874 .unwrap();
16875
16876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16877
16878 let leader = pane.update_in(cx, |_, window, cx| {
16879 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16880 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16881 });
16882
16883 // Start following the editor when it has no excerpts.
16884 let mut state_message =
16885 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16886 let workspace_entity = workspace.root(cx).unwrap();
16887 let follower_1 = cx
16888 .update_window(*workspace.deref(), |_, window, cx| {
16889 Editor::from_state_proto(
16890 workspace_entity,
16891 ViewId {
16892 creator: CollaboratorId::PeerId(PeerId::default()),
16893 id: 0,
16894 },
16895 &mut state_message,
16896 window,
16897 cx,
16898 )
16899 })
16900 .unwrap()
16901 .unwrap()
16902 .await
16903 .unwrap();
16904
16905 let update_message = Rc::new(RefCell::new(None));
16906 follower_1.update_in(cx, {
16907 let update = update_message.clone();
16908 |_, window, cx| {
16909 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16910 leader.read(cx).add_event_to_update_proto(
16911 event,
16912 &mut update.borrow_mut(),
16913 window,
16914 cx,
16915 );
16916 })
16917 .detach();
16918 }
16919 });
16920
16921 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16922 (
16923 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16924 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16925 )
16926 });
16927
16928 // Insert some excerpts.
16929 leader.update(cx, |leader, cx| {
16930 leader.buffer.update(cx, |multibuffer, cx| {
16931 multibuffer.set_excerpts_for_path(
16932 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16933 buffer_1.clone(),
16934 vec![
16935 Point::row_range(0..3),
16936 Point::row_range(1..6),
16937 Point::row_range(12..15),
16938 ],
16939 0,
16940 cx,
16941 );
16942 multibuffer.set_excerpts_for_path(
16943 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16944 buffer_2.clone(),
16945 vec![Point::row_range(0..6), Point::row_range(8..12)],
16946 0,
16947 cx,
16948 );
16949 });
16950 });
16951
16952 // Apply the update of adding the excerpts.
16953 follower_1
16954 .update_in(cx, |follower, window, cx| {
16955 follower.apply_update_proto(
16956 &project,
16957 update_message.borrow().clone().unwrap(),
16958 window,
16959 cx,
16960 )
16961 })
16962 .await
16963 .unwrap();
16964 assert_eq!(
16965 follower_1.update(cx, |editor, cx| editor.text(cx)),
16966 leader.update(cx, |editor, cx| editor.text(cx))
16967 );
16968 update_message.borrow_mut().take();
16969
16970 // Start following separately after it already has excerpts.
16971 let mut state_message =
16972 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16973 let workspace_entity = workspace.root(cx).unwrap();
16974 let follower_2 = cx
16975 .update_window(*workspace.deref(), |_, window, cx| {
16976 Editor::from_state_proto(
16977 workspace_entity,
16978 ViewId {
16979 creator: CollaboratorId::PeerId(PeerId::default()),
16980 id: 0,
16981 },
16982 &mut state_message,
16983 window,
16984 cx,
16985 )
16986 })
16987 .unwrap()
16988 .unwrap()
16989 .await
16990 .unwrap();
16991 assert_eq!(
16992 follower_2.update(cx, |editor, cx| editor.text(cx)),
16993 leader.update(cx, |editor, cx| editor.text(cx))
16994 );
16995
16996 // Remove some excerpts.
16997 leader.update(cx, |leader, cx| {
16998 leader.buffer.update(cx, |multibuffer, cx| {
16999 let excerpt_ids = multibuffer.excerpt_ids();
17000 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17001 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17002 });
17003 });
17004
17005 // Apply the update of removing the excerpts.
17006 follower_1
17007 .update_in(cx, |follower, window, cx| {
17008 follower.apply_update_proto(
17009 &project,
17010 update_message.borrow().clone().unwrap(),
17011 window,
17012 cx,
17013 )
17014 })
17015 .await
17016 .unwrap();
17017 follower_2
17018 .update_in(cx, |follower, window, cx| {
17019 follower.apply_update_proto(
17020 &project,
17021 update_message.borrow().clone().unwrap(),
17022 window,
17023 cx,
17024 )
17025 })
17026 .await
17027 .unwrap();
17028 update_message.borrow_mut().take();
17029 assert_eq!(
17030 follower_1.update(cx, |editor, cx| editor.text(cx)),
17031 leader.update(cx, |editor, cx| editor.text(cx))
17032 );
17033}
17034
17035#[gpui::test]
17036async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17037 init_test(cx, |_| {});
17038
17039 let mut cx = EditorTestContext::new(cx).await;
17040 let lsp_store =
17041 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17042
17043 cx.set_state(indoc! {"
17044 ˇfn func(abc def: i32) -> u32 {
17045 }
17046 "});
17047
17048 cx.update(|_, cx| {
17049 lsp_store.update(cx, |lsp_store, cx| {
17050 lsp_store
17051 .update_diagnostics(
17052 LanguageServerId(0),
17053 lsp::PublishDiagnosticsParams {
17054 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17055 version: None,
17056 diagnostics: vec![
17057 lsp::Diagnostic {
17058 range: lsp::Range::new(
17059 lsp::Position::new(0, 11),
17060 lsp::Position::new(0, 12),
17061 ),
17062 severity: Some(lsp::DiagnosticSeverity::ERROR),
17063 ..Default::default()
17064 },
17065 lsp::Diagnostic {
17066 range: lsp::Range::new(
17067 lsp::Position::new(0, 12),
17068 lsp::Position::new(0, 15),
17069 ),
17070 severity: Some(lsp::DiagnosticSeverity::ERROR),
17071 ..Default::default()
17072 },
17073 lsp::Diagnostic {
17074 range: lsp::Range::new(
17075 lsp::Position::new(0, 25),
17076 lsp::Position::new(0, 28),
17077 ),
17078 severity: Some(lsp::DiagnosticSeverity::ERROR),
17079 ..Default::default()
17080 },
17081 ],
17082 },
17083 None,
17084 DiagnosticSourceKind::Pushed,
17085 &[],
17086 cx,
17087 )
17088 .unwrap()
17089 });
17090 });
17091
17092 executor.run_until_parked();
17093
17094 cx.update_editor(|editor, window, cx| {
17095 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17096 });
17097
17098 cx.assert_editor_state(indoc! {"
17099 fn func(abc def: i32) -> ˇu32 {
17100 }
17101 "});
17102
17103 cx.update_editor(|editor, window, cx| {
17104 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17105 });
17106
17107 cx.assert_editor_state(indoc! {"
17108 fn func(abc ˇdef: i32) -> u32 {
17109 }
17110 "});
17111
17112 cx.update_editor(|editor, window, cx| {
17113 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17114 });
17115
17116 cx.assert_editor_state(indoc! {"
17117 fn func(abcˇ def: i32) -> u32 {
17118 }
17119 "});
17120
17121 cx.update_editor(|editor, window, cx| {
17122 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17123 });
17124
17125 cx.assert_editor_state(indoc! {"
17126 fn func(abc def: i32) -> ˇu32 {
17127 }
17128 "});
17129}
17130
17131#[gpui::test]
17132async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17133 init_test(cx, |_| {});
17134
17135 let mut cx = EditorTestContext::new(cx).await;
17136
17137 let diff_base = r#"
17138 use some::mod;
17139
17140 const A: u32 = 42;
17141
17142 fn main() {
17143 println!("hello");
17144
17145 println!("world");
17146 }
17147 "#
17148 .unindent();
17149
17150 // Edits are modified, removed, modified, added
17151 cx.set_state(
17152 &r#"
17153 use some::modified;
17154
17155 ˇ
17156 fn main() {
17157 println!("hello there");
17158
17159 println!("around the");
17160 println!("world");
17161 }
17162 "#
17163 .unindent(),
17164 );
17165
17166 cx.set_head_text(&diff_base);
17167 executor.run_until_parked();
17168
17169 cx.update_editor(|editor, window, cx| {
17170 //Wrap around the bottom of the buffer
17171 for _ in 0..3 {
17172 editor.go_to_next_hunk(&GoToHunk, window, cx);
17173 }
17174 });
17175
17176 cx.assert_editor_state(
17177 &r#"
17178 ˇuse some::modified;
17179
17180
17181 fn main() {
17182 println!("hello there");
17183
17184 println!("around the");
17185 println!("world");
17186 }
17187 "#
17188 .unindent(),
17189 );
17190
17191 cx.update_editor(|editor, window, cx| {
17192 //Wrap around the top of the buffer
17193 for _ in 0..2 {
17194 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17195 }
17196 });
17197
17198 cx.assert_editor_state(
17199 &r#"
17200 use some::modified;
17201
17202
17203 fn main() {
17204 ˇ println!("hello there");
17205
17206 println!("around the");
17207 println!("world");
17208 }
17209 "#
17210 .unindent(),
17211 );
17212
17213 cx.update_editor(|editor, window, cx| {
17214 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17215 });
17216
17217 cx.assert_editor_state(
17218 &r#"
17219 use some::modified;
17220
17221 ˇ
17222 fn main() {
17223 println!("hello there");
17224
17225 println!("around the");
17226 println!("world");
17227 }
17228 "#
17229 .unindent(),
17230 );
17231
17232 cx.update_editor(|editor, window, cx| {
17233 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17234 });
17235
17236 cx.assert_editor_state(
17237 &r#"
17238 ˇuse some::modified;
17239
17240
17241 fn main() {
17242 println!("hello there");
17243
17244 println!("around the");
17245 println!("world");
17246 }
17247 "#
17248 .unindent(),
17249 );
17250
17251 cx.update_editor(|editor, window, cx| {
17252 for _ in 0..2 {
17253 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17254 }
17255 });
17256
17257 cx.assert_editor_state(
17258 &r#"
17259 use some::modified;
17260
17261
17262 fn main() {
17263 ˇ println!("hello there");
17264
17265 println!("around the");
17266 println!("world");
17267 }
17268 "#
17269 .unindent(),
17270 );
17271
17272 cx.update_editor(|editor, window, cx| {
17273 editor.fold(&Fold, window, cx);
17274 });
17275
17276 cx.update_editor(|editor, window, cx| {
17277 editor.go_to_next_hunk(&GoToHunk, window, cx);
17278 });
17279
17280 cx.assert_editor_state(
17281 &r#"
17282 ˇuse some::modified;
17283
17284
17285 fn main() {
17286 println!("hello there");
17287
17288 println!("around the");
17289 println!("world");
17290 }
17291 "#
17292 .unindent(),
17293 );
17294}
17295
17296#[test]
17297fn test_split_words() {
17298 fn split(text: &str) -> Vec<&str> {
17299 split_words(text).collect()
17300 }
17301
17302 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17303 assert_eq!(split("hello_world"), &["hello_", "world"]);
17304 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17305 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17306 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17307 assert_eq!(split("helloworld"), &["helloworld"]);
17308
17309 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17310}
17311
17312#[gpui::test]
17313async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17314 init_test(cx, |_| {});
17315
17316 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17317 let mut assert = |before, after| {
17318 let _state_context = cx.set_state(before);
17319 cx.run_until_parked();
17320 cx.update_editor(|editor, window, cx| {
17321 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17322 });
17323 cx.run_until_parked();
17324 cx.assert_editor_state(after);
17325 };
17326
17327 // Outside bracket jumps to outside of matching bracket
17328 assert("console.logˇ(var);", "console.log(var)ˇ;");
17329 assert("console.log(var)ˇ;", "console.logˇ(var);");
17330
17331 // Inside bracket jumps to inside of matching bracket
17332 assert("console.log(ˇvar);", "console.log(varˇ);");
17333 assert("console.log(varˇ);", "console.log(ˇvar);");
17334
17335 // When outside a bracket and inside, favor jumping to the inside bracket
17336 assert(
17337 "console.log('foo', [1, 2, 3]ˇ);",
17338 "console.log(ˇ'foo', [1, 2, 3]);",
17339 );
17340 assert(
17341 "console.log(ˇ'foo', [1, 2, 3]);",
17342 "console.log('foo', [1, 2, 3]ˇ);",
17343 );
17344
17345 // Bias forward if two options are equally likely
17346 assert(
17347 "let result = curried_fun()ˇ();",
17348 "let result = curried_fun()()ˇ;",
17349 );
17350
17351 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17352 assert(
17353 indoc! {"
17354 function test() {
17355 console.log('test')ˇ
17356 }"},
17357 indoc! {"
17358 function test() {
17359 console.logˇ('test')
17360 }"},
17361 );
17362}
17363
17364#[gpui::test]
17365async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17366 init_test(cx, |_| {});
17367
17368 let fs = FakeFs::new(cx.executor());
17369 fs.insert_tree(
17370 path!("/a"),
17371 json!({
17372 "main.rs": "fn main() { let a = 5; }",
17373 "other.rs": "// Test file",
17374 }),
17375 )
17376 .await;
17377 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17378
17379 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17380 language_registry.add(Arc::new(Language::new(
17381 LanguageConfig {
17382 name: "Rust".into(),
17383 matcher: LanguageMatcher {
17384 path_suffixes: vec!["rs".to_string()],
17385 ..Default::default()
17386 },
17387 brackets: BracketPairConfig {
17388 pairs: vec![BracketPair {
17389 start: "{".to_string(),
17390 end: "}".to_string(),
17391 close: true,
17392 surround: true,
17393 newline: true,
17394 }],
17395 disabled_scopes_by_bracket_ix: Vec::new(),
17396 },
17397 ..Default::default()
17398 },
17399 Some(tree_sitter_rust::LANGUAGE.into()),
17400 )));
17401 let mut fake_servers = language_registry.register_fake_lsp(
17402 "Rust",
17403 FakeLspAdapter {
17404 capabilities: lsp::ServerCapabilities {
17405 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17406 first_trigger_character: "{".to_string(),
17407 more_trigger_character: None,
17408 }),
17409 ..Default::default()
17410 },
17411 ..Default::default()
17412 },
17413 );
17414
17415 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17416
17417 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17418
17419 let worktree_id = workspace
17420 .update(cx, |workspace, _, cx| {
17421 workspace.project().update(cx, |project, cx| {
17422 project.worktrees(cx).next().unwrap().read(cx).id()
17423 })
17424 })
17425 .unwrap();
17426
17427 let buffer = project
17428 .update(cx, |project, cx| {
17429 project.open_local_buffer(path!("/a/main.rs"), cx)
17430 })
17431 .await
17432 .unwrap();
17433 let editor_handle = workspace
17434 .update(cx, |workspace, window, cx| {
17435 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17436 })
17437 .unwrap()
17438 .await
17439 .unwrap()
17440 .downcast::<Editor>()
17441 .unwrap();
17442
17443 cx.executor().start_waiting();
17444 let fake_server = fake_servers.next().await.unwrap();
17445
17446 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17447 |params, _| async move {
17448 assert_eq!(
17449 params.text_document_position.text_document.uri,
17450 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17451 );
17452 assert_eq!(
17453 params.text_document_position.position,
17454 lsp::Position::new(0, 21),
17455 );
17456
17457 Ok(Some(vec![lsp::TextEdit {
17458 new_text: "]".to_string(),
17459 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17460 }]))
17461 },
17462 );
17463
17464 editor_handle.update_in(cx, |editor, window, cx| {
17465 window.focus(&editor.focus_handle(cx));
17466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17467 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17468 });
17469 editor.handle_input("{", window, cx);
17470 });
17471
17472 cx.executor().run_until_parked();
17473
17474 buffer.update(cx, |buffer, _| {
17475 assert_eq!(
17476 buffer.text(),
17477 "fn main() { let a = {5}; }",
17478 "No extra braces from on type formatting should appear in the buffer"
17479 )
17480 });
17481}
17482
17483#[gpui::test(iterations = 20, seeds(31))]
17484async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17485 init_test(cx, |_| {});
17486
17487 let mut cx = EditorLspTestContext::new_rust(
17488 lsp::ServerCapabilities {
17489 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17490 first_trigger_character: ".".to_string(),
17491 more_trigger_character: None,
17492 }),
17493 ..Default::default()
17494 },
17495 cx,
17496 )
17497 .await;
17498
17499 cx.update_buffer(|buffer, _| {
17500 // This causes autoindent to be async.
17501 buffer.set_sync_parse_timeout(Duration::ZERO)
17502 });
17503
17504 cx.set_state("fn c() {\n d()ˇ\n}\n");
17505 cx.simulate_keystroke("\n");
17506 cx.run_until_parked();
17507
17508 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17509 let mut request =
17510 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17511 let buffer_cloned = buffer_cloned.clone();
17512 async move {
17513 buffer_cloned.update(&mut cx, |buffer, _| {
17514 assert_eq!(
17515 buffer.text(),
17516 "fn c() {\n d()\n .\n}\n",
17517 "OnTypeFormatting should triggered after autoindent applied"
17518 )
17519 })?;
17520
17521 Ok(Some(vec![]))
17522 }
17523 });
17524
17525 cx.simulate_keystroke(".");
17526 cx.run_until_parked();
17527
17528 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17529 assert!(request.next().await.is_some());
17530 request.close();
17531 assert!(request.next().await.is_none());
17532}
17533
17534#[gpui::test]
17535async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17536 init_test(cx, |_| {});
17537
17538 let fs = FakeFs::new(cx.executor());
17539 fs.insert_tree(
17540 path!("/a"),
17541 json!({
17542 "main.rs": "fn main() { let a = 5; }",
17543 "other.rs": "// Test file",
17544 }),
17545 )
17546 .await;
17547
17548 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17549
17550 let server_restarts = Arc::new(AtomicUsize::new(0));
17551 let closure_restarts = Arc::clone(&server_restarts);
17552 let language_server_name = "test language server";
17553 let language_name: LanguageName = "Rust".into();
17554
17555 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17556 language_registry.add(Arc::new(Language::new(
17557 LanguageConfig {
17558 name: language_name.clone(),
17559 matcher: LanguageMatcher {
17560 path_suffixes: vec!["rs".to_string()],
17561 ..Default::default()
17562 },
17563 ..Default::default()
17564 },
17565 Some(tree_sitter_rust::LANGUAGE.into()),
17566 )));
17567 let mut fake_servers = language_registry.register_fake_lsp(
17568 "Rust",
17569 FakeLspAdapter {
17570 name: language_server_name,
17571 initialization_options: Some(json!({
17572 "testOptionValue": true
17573 })),
17574 initializer: Some(Box::new(move |fake_server| {
17575 let task_restarts = Arc::clone(&closure_restarts);
17576 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17577 task_restarts.fetch_add(1, atomic::Ordering::Release);
17578 futures::future::ready(Ok(()))
17579 });
17580 })),
17581 ..Default::default()
17582 },
17583 );
17584
17585 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17586 let _buffer = project
17587 .update(cx, |project, cx| {
17588 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17589 })
17590 .await
17591 .unwrap();
17592 let _fake_server = fake_servers.next().await.unwrap();
17593 update_test_language_settings(cx, |language_settings| {
17594 language_settings.languages.0.insert(
17595 language_name.clone().0,
17596 LanguageSettingsContent {
17597 tab_size: NonZeroU32::new(8),
17598 ..Default::default()
17599 },
17600 );
17601 });
17602 cx.executor().run_until_parked();
17603 assert_eq!(
17604 server_restarts.load(atomic::Ordering::Acquire),
17605 0,
17606 "Should not restart LSP server on an unrelated change"
17607 );
17608
17609 update_test_project_settings(cx, |project_settings| {
17610 project_settings.lsp.insert(
17611 "Some other server name".into(),
17612 LspSettings {
17613 binary: None,
17614 settings: None,
17615 initialization_options: Some(json!({
17616 "some other init value": false
17617 })),
17618 enable_lsp_tasks: false,
17619 fetch: None,
17620 },
17621 );
17622 });
17623 cx.executor().run_until_parked();
17624 assert_eq!(
17625 server_restarts.load(atomic::Ordering::Acquire),
17626 0,
17627 "Should not restart LSP server on an unrelated LSP settings change"
17628 );
17629
17630 update_test_project_settings(cx, |project_settings| {
17631 project_settings.lsp.insert(
17632 language_server_name.into(),
17633 LspSettings {
17634 binary: None,
17635 settings: None,
17636 initialization_options: Some(json!({
17637 "anotherInitValue": false
17638 })),
17639 enable_lsp_tasks: false,
17640 fetch: None,
17641 },
17642 );
17643 });
17644 cx.executor().run_until_parked();
17645 assert_eq!(
17646 server_restarts.load(atomic::Ordering::Acquire),
17647 1,
17648 "Should restart LSP server on a related LSP settings change"
17649 );
17650
17651 update_test_project_settings(cx, |project_settings| {
17652 project_settings.lsp.insert(
17653 language_server_name.into(),
17654 LspSettings {
17655 binary: None,
17656 settings: None,
17657 initialization_options: Some(json!({
17658 "anotherInitValue": false
17659 })),
17660 enable_lsp_tasks: false,
17661 fetch: None,
17662 },
17663 );
17664 });
17665 cx.executor().run_until_parked();
17666 assert_eq!(
17667 server_restarts.load(atomic::Ordering::Acquire),
17668 1,
17669 "Should not restart LSP server on a related LSP settings change that is the same"
17670 );
17671
17672 update_test_project_settings(cx, |project_settings| {
17673 project_settings.lsp.insert(
17674 language_server_name.into(),
17675 LspSettings {
17676 binary: None,
17677 settings: None,
17678 initialization_options: None,
17679 enable_lsp_tasks: false,
17680 fetch: None,
17681 },
17682 );
17683 });
17684 cx.executor().run_until_parked();
17685 assert_eq!(
17686 server_restarts.load(atomic::Ordering::Acquire),
17687 2,
17688 "Should restart LSP server on another related LSP settings change"
17689 );
17690}
17691
17692#[gpui::test]
17693async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17694 init_test(cx, |_| {});
17695
17696 let mut cx = EditorLspTestContext::new_rust(
17697 lsp::ServerCapabilities {
17698 completion_provider: Some(lsp::CompletionOptions {
17699 trigger_characters: Some(vec![".".to_string()]),
17700 resolve_provider: Some(true),
17701 ..Default::default()
17702 }),
17703 ..Default::default()
17704 },
17705 cx,
17706 )
17707 .await;
17708
17709 cx.set_state("fn main() { let a = 2ˇ; }");
17710 cx.simulate_keystroke(".");
17711 let completion_item = lsp::CompletionItem {
17712 label: "some".into(),
17713 kind: Some(lsp::CompletionItemKind::SNIPPET),
17714 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17715 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17716 kind: lsp::MarkupKind::Markdown,
17717 value: "```rust\nSome(2)\n```".to_string(),
17718 })),
17719 deprecated: Some(false),
17720 sort_text: Some("fffffff2".to_string()),
17721 filter_text: Some("some".to_string()),
17722 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17723 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17724 range: lsp::Range {
17725 start: lsp::Position {
17726 line: 0,
17727 character: 22,
17728 },
17729 end: lsp::Position {
17730 line: 0,
17731 character: 22,
17732 },
17733 },
17734 new_text: "Some(2)".to_string(),
17735 })),
17736 additional_text_edits: Some(vec![lsp::TextEdit {
17737 range: lsp::Range {
17738 start: lsp::Position {
17739 line: 0,
17740 character: 20,
17741 },
17742 end: lsp::Position {
17743 line: 0,
17744 character: 22,
17745 },
17746 },
17747 new_text: "".to_string(),
17748 }]),
17749 ..Default::default()
17750 };
17751
17752 let closure_completion_item = completion_item.clone();
17753 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17754 let task_completion_item = closure_completion_item.clone();
17755 async move {
17756 Ok(Some(lsp::CompletionResponse::Array(vec![
17757 task_completion_item,
17758 ])))
17759 }
17760 });
17761
17762 request.next().await;
17763
17764 cx.condition(|editor, _| editor.context_menu_visible())
17765 .await;
17766 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17767 editor
17768 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17769 .unwrap()
17770 });
17771 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17772
17773 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17774 let task_completion_item = completion_item.clone();
17775 async move { Ok(task_completion_item) }
17776 })
17777 .next()
17778 .await
17779 .unwrap();
17780 apply_additional_edits.await.unwrap();
17781 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17782}
17783
17784#[gpui::test]
17785async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17786 init_test(cx, |_| {});
17787
17788 let mut cx = EditorLspTestContext::new_rust(
17789 lsp::ServerCapabilities {
17790 completion_provider: Some(lsp::CompletionOptions {
17791 trigger_characters: Some(vec![".".to_string()]),
17792 resolve_provider: Some(true),
17793 ..Default::default()
17794 }),
17795 ..Default::default()
17796 },
17797 cx,
17798 )
17799 .await;
17800
17801 cx.set_state("fn main() { let a = 2ˇ; }");
17802 cx.simulate_keystroke(".");
17803
17804 let item1 = lsp::CompletionItem {
17805 label: "method id()".to_string(),
17806 filter_text: Some("id".to_string()),
17807 detail: None,
17808 documentation: None,
17809 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17810 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17811 new_text: ".id".to_string(),
17812 })),
17813 ..lsp::CompletionItem::default()
17814 };
17815
17816 let item2 = lsp::CompletionItem {
17817 label: "other".to_string(),
17818 filter_text: Some("other".to_string()),
17819 detail: None,
17820 documentation: None,
17821 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17822 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17823 new_text: ".other".to_string(),
17824 })),
17825 ..lsp::CompletionItem::default()
17826 };
17827
17828 let item1 = item1.clone();
17829 cx.set_request_handler::<lsp::request::Completion, _, _>({
17830 let item1 = item1.clone();
17831 move |_, _, _| {
17832 let item1 = item1.clone();
17833 let item2 = item2.clone();
17834 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17835 }
17836 })
17837 .next()
17838 .await;
17839
17840 cx.condition(|editor, _| editor.context_menu_visible())
17841 .await;
17842 cx.update_editor(|editor, _, _| {
17843 let context_menu = editor.context_menu.borrow_mut();
17844 let context_menu = context_menu
17845 .as_ref()
17846 .expect("Should have the context menu deployed");
17847 match context_menu {
17848 CodeContextMenu::Completions(completions_menu) => {
17849 let completions = completions_menu.completions.borrow_mut();
17850 assert_eq!(
17851 completions
17852 .iter()
17853 .map(|completion| &completion.label.text)
17854 .collect::<Vec<_>>(),
17855 vec!["method id()", "other"]
17856 )
17857 }
17858 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17859 }
17860 });
17861
17862 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17863 let item1 = item1.clone();
17864 move |_, item_to_resolve, _| {
17865 let item1 = item1.clone();
17866 async move {
17867 if item1 == item_to_resolve {
17868 Ok(lsp::CompletionItem {
17869 label: "method id()".to_string(),
17870 filter_text: Some("id".to_string()),
17871 detail: Some("Now resolved!".to_string()),
17872 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17873 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17874 range: lsp::Range::new(
17875 lsp::Position::new(0, 22),
17876 lsp::Position::new(0, 22),
17877 ),
17878 new_text: ".id".to_string(),
17879 })),
17880 ..lsp::CompletionItem::default()
17881 })
17882 } else {
17883 Ok(item_to_resolve)
17884 }
17885 }
17886 }
17887 })
17888 .next()
17889 .await
17890 .unwrap();
17891 cx.run_until_parked();
17892
17893 cx.update_editor(|editor, window, cx| {
17894 editor.context_menu_next(&Default::default(), window, cx);
17895 });
17896
17897 cx.update_editor(|editor, _, _| {
17898 let context_menu = editor.context_menu.borrow_mut();
17899 let context_menu = context_menu
17900 .as_ref()
17901 .expect("Should have the context menu deployed");
17902 match context_menu {
17903 CodeContextMenu::Completions(completions_menu) => {
17904 let completions = completions_menu.completions.borrow_mut();
17905 assert_eq!(
17906 completions
17907 .iter()
17908 .map(|completion| &completion.label.text)
17909 .collect::<Vec<_>>(),
17910 vec!["method id() Now resolved!", "other"],
17911 "Should update first completion label, but not second as the filter text did not match."
17912 );
17913 }
17914 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17915 }
17916 });
17917}
17918
17919#[gpui::test]
17920async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17921 init_test(cx, |_| {});
17922 let mut cx = EditorLspTestContext::new_rust(
17923 lsp::ServerCapabilities {
17924 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17925 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17926 completion_provider: Some(lsp::CompletionOptions {
17927 resolve_provider: Some(true),
17928 ..Default::default()
17929 }),
17930 ..Default::default()
17931 },
17932 cx,
17933 )
17934 .await;
17935 cx.set_state(indoc! {"
17936 struct TestStruct {
17937 field: i32
17938 }
17939
17940 fn mainˇ() {
17941 let unused_var = 42;
17942 let test_struct = TestStruct { field: 42 };
17943 }
17944 "});
17945 let symbol_range = cx.lsp_range(indoc! {"
17946 struct TestStruct {
17947 field: i32
17948 }
17949
17950 «fn main»() {
17951 let unused_var = 42;
17952 let test_struct = TestStruct { field: 42 };
17953 }
17954 "});
17955 let mut hover_requests =
17956 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17957 Ok(Some(lsp::Hover {
17958 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17959 kind: lsp::MarkupKind::Markdown,
17960 value: "Function documentation".to_string(),
17961 }),
17962 range: Some(symbol_range),
17963 }))
17964 });
17965
17966 // Case 1: Test that code action menu hide hover popover
17967 cx.dispatch_action(Hover);
17968 hover_requests.next().await;
17969 cx.condition(|editor, _| editor.hover_state.visible()).await;
17970 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17971 move |_, _, _| async move {
17972 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17973 lsp::CodeAction {
17974 title: "Remove unused variable".to_string(),
17975 kind: Some(CodeActionKind::QUICKFIX),
17976 edit: Some(lsp::WorkspaceEdit {
17977 changes: Some(
17978 [(
17979 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17980 vec![lsp::TextEdit {
17981 range: lsp::Range::new(
17982 lsp::Position::new(5, 4),
17983 lsp::Position::new(5, 27),
17984 ),
17985 new_text: "".to_string(),
17986 }],
17987 )]
17988 .into_iter()
17989 .collect(),
17990 ),
17991 ..Default::default()
17992 }),
17993 ..Default::default()
17994 },
17995 )]))
17996 },
17997 );
17998 cx.update_editor(|editor, window, cx| {
17999 editor.toggle_code_actions(
18000 &ToggleCodeActions {
18001 deployed_from: None,
18002 quick_launch: false,
18003 },
18004 window,
18005 cx,
18006 );
18007 });
18008 code_action_requests.next().await;
18009 cx.run_until_parked();
18010 cx.condition(|editor, _| editor.context_menu_visible())
18011 .await;
18012 cx.update_editor(|editor, _, _| {
18013 assert!(
18014 !editor.hover_state.visible(),
18015 "Hover popover should be hidden when code action menu is shown"
18016 );
18017 // Hide code actions
18018 editor.context_menu.take();
18019 });
18020
18021 // Case 2: Test that code completions hide hover popover
18022 cx.dispatch_action(Hover);
18023 hover_requests.next().await;
18024 cx.condition(|editor, _| editor.hover_state.visible()).await;
18025 let counter = Arc::new(AtomicUsize::new(0));
18026 let mut completion_requests =
18027 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18028 let counter = counter.clone();
18029 async move {
18030 counter.fetch_add(1, atomic::Ordering::Release);
18031 Ok(Some(lsp::CompletionResponse::Array(vec![
18032 lsp::CompletionItem {
18033 label: "main".into(),
18034 kind: Some(lsp::CompletionItemKind::FUNCTION),
18035 detail: Some("() -> ()".to_string()),
18036 ..Default::default()
18037 },
18038 lsp::CompletionItem {
18039 label: "TestStruct".into(),
18040 kind: Some(lsp::CompletionItemKind::STRUCT),
18041 detail: Some("struct TestStruct".to_string()),
18042 ..Default::default()
18043 },
18044 ])))
18045 }
18046 });
18047 cx.update_editor(|editor, window, cx| {
18048 editor.show_completions(&ShowCompletions, window, cx);
18049 });
18050 completion_requests.next().await;
18051 cx.condition(|editor, _| editor.context_menu_visible())
18052 .await;
18053 cx.update_editor(|editor, _, _| {
18054 assert!(
18055 !editor.hover_state.visible(),
18056 "Hover popover should be hidden when completion menu is shown"
18057 );
18058 });
18059}
18060
18061#[gpui::test]
18062async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18063 init_test(cx, |_| {});
18064
18065 let mut cx = EditorLspTestContext::new_rust(
18066 lsp::ServerCapabilities {
18067 completion_provider: Some(lsp::CompletionOptions {
18068 trigger_characters: Some(vec![".".to_string()]),
18069 resolve_provider: Some(true),
18070 ..Default::default()
18071 }),
18072 ..Default::default()
18073 },
18074 cx,
18075 )
18076 .await;
18077
18078 cx.set_state("fn main() { let a = 2ˇ; }");
18079 cx.simulate_keystroke(".");
18080
18081 let unresolved_item_1 = lsp::CompletionItem {
18082 label: "id".to_string(),
18083 filter_text: Some("id".to_string()),
18084 detail: None,
18085 documentation: None,
18086 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18087 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18088 new_text: ".id".to_string(),
18089 })),
18090 ..lsp::CompletionItem::default()
18091 };
18092 let resolved_item_1 = lsp::CompletionItem {
18093 additional_text_edits: Some(vec![lsp::TextEdit {
18094 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18095 new_text: "!!".to_string(),
18096 }]),
18097 ..unresolved_item_1.clone()
18098 };
18099 let unresolved_item_2 = lsp::CompletionItem {
18100 label: "other".to_string(),
18101 filter_text: Some("other".to_string()),
18102 detail: None,
18103 documentation: None,
18104 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18105 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18106 new_text: ".other".to_string(),
18107 })),
18108 ..lsp::CompletionItem::default()
18109 };
18110 let resolved_item_2 = lsp::CompletionItem {
18111 additional_text_edits: Some(vec![lsp::TextEdit {
18112 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18113 new_text: "??".to_string(),
18114 }]),
18115 ..unresolved_item_2.clone()
18116 };
18117
18118 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18119 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18120 cx.lsp
18121 .server
18122 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18123 let unresolved_item_1 = unresolved_item_1.clone();
18124 let resolved_item_1 = resolved_item_1.clone();
18125 let unresolved_item_2 = unresolved_item_2.clone();
18126 let resolved_item_2 = resolved_item_2.clone();
18127 let resolve_requests_1 = resolve_requests_1.clone();
18128 let resolve_requests_2 = resolve_requests_2.clone();
18129 move |unresolved_request, _| {
18130 let unresolved_item_1 = unresolved_item_1.clone();
18131 let resolved_item_1 = resolved_item_1.clone();
18132 let unresolved_item_2 = unresolved_item_2.clone();
18133 let resolved_item_2 = resolved_item_2.clone();
18134 let resolve_requests_1 = resolve_requests_1.clone();
18135 let resolve_requests_2 = resolve_requests_2.clone();
18136 async move {
18137 if unresolved_request == unresolved_item_1 {
18138 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18139 Ok(resolved_item_1.clone())
18140 } else if unresolved_request == unresolved_item_2 {
18141 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18142 Ok(resolved_item_2.clone())
18143 } else {
18144 panic!("Unexpected completion item {unresolved_request:?}")
18145 }
18146 }
18147 }
18148 })
18149 .detach();
18150
18151 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18152 let unresolved_item_1 = unresolved_item_1.clone();
18153 let unresolved_item_2 = unresolved_item_2.clone();
18154 async move {
18155 Ok(Some(lsp::CompletionResponse::Array(vec![
18156 unresolved_item_1,
18157 unresolved_item_2,
18158 ])))
18159 }
18160 })
18161 .next()
18162 .await;
18163
18164 cx.condition(|editor, _| editor.context_menu_visible())
18165 .await;
18166 cx.update_editor(|editor, _, _| {
18167 let context_menu = editor.context_menu.borrow_mut();
18168 let context_menu = context_menu
18169 .as_ref()
18170 .expect("Should have the context menu deployed");
18171 match context_menu {
18172 CodeContextMenu::Completions(completions_menu) => {
18173 let completions = completions_menu.completions.borrow_mut();
18174 assert_eq!(
18175 completions
18176 .iter()
18177 .map(|completion| &completion.label.text)
18178 .collect::<Vec<_>>(),
18179 vec!["id", "other"]
18180 )
18181 }
18182 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18183 }
18184 });
18185 cx.run_until_parked();
18186
18187 cx.update_editor(|editor, window, cx| {
18188 editor.context_menu_next(&ContextMenuNext, window, cx);
18189 });
18190 cx.run_until_parked();
18191 cx.update_editor(|editor, window, cx| {
18192 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18193 });
18194 cx.run_until_parked();
18195 cx.update_editor(|editor, window, cx| {
18196 editor.context_menu_next(&ContextMenuNext, window, cx);
18197 });
18198 cx.run_until_parked();
18199 cx.update_editor(|editor, window, cx| {
18200 editor
18201 .compose_completion(&ComposeCompletion::default(), window, cx)
18202 .expect("No task returned")
18203 })
18204 .await
18205 .expect("Completion failed");
18206 cx.run_until_parked();
18207
18208 cx.update_editor(|editor, _, cx| {
18209 assert_eq!(
18210 resolve_requests_1.load(atomic::Ordering::Acquire),
18211 1,
18212 "Should always resolve once despite multiple selections"
18213 );
18214 assert_eq!(
18215 resolve_requests_2.load(atomic::Ordering::Acquire),
18216 1,
18217 "Should always resolve once after multiple selections and applying the completion"
18218 );
18219 assert_eq!(
18220 editor.text(cx),
18221 "fn main() { let a = ??.other; }",
18222 "Should use resolved data when applying the completion"
18223 );
18224 });
18225}
18226
18227#[gpui::test]
18228async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18229 init_test(cx, |_| {});
18230
18231 let item_0 = lsp::CompletionItem {
18232 label: "abs".into(),
18233 insert_text: Some("abs".into()),
18234 data: Some(json!({ "very": "special"})),
18235 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18236 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18237 lsp::InsertReplaceEdit {
18238 new_text: "abs".to_string(),
18239 insert: lsp::Range::default(),
18240 replace: lsp::Range::default(),
18241 },
18242 )),
18243 ..lsp::CompletionItem::default()
18244 };
18245 let items = iter::once(item_0.clone())
18246 .chain((11..51).map(|i| lsp::CompletionItem {
18247 label: format!("item_{}", i),
18248 insert_text: Some(format!("item_{}", i)),
18249 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18250 ..lsp::CompletionItem::default()
18251 }))
18252 .collect::<Vec<_>>();
18253
18254 let default_commit_characters = vec!["?".to_string()];
18255 let default_data = json!({ "default": "data"});
18256 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18257 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18258 let default_edit_range = lsp::Range {
18259 start: lsp::Position {
18260 line: 0,
18261 character: 5,
18262 },
18263 end: lsp::Position {
18264 line: 0,
18265 character: 5,
18266 },
18267 };
18268
18269 let mut cx = EditorLspTestContext::new_rust(
18270 lsp::ServerCapabilities {
18271 completion_provider: Some(lsp::CompletionOptions {
18272 trigger_characters: Some(vec![".".to_string()]),
18273 resolve_provider: Some(true),
18274 ..Default::default()
18275 }),
18276 ..Default::default()
18277 },
18278 cx,
18279 )
18280 .await;
18281
18282 cx.set_state("fn main() { let a = 2ˇ; }");
18283 cx.simulate_keystroke(".");
18284
18285 let completion_data = default_data.clone();
18286 let completion_characters = default_commit_characters.clone();
18287 let completion_items = items.clone();
18288 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18289 let default_data = completion_data.clone();
18290 let default_commit_characters = completion_characters.clone();
18291 let items = completion_items.clone();
18292 async move {
18293 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18294 items,
18295 item_defaults: Some(lsp::CompletionListItemDefaults {
18296 data: Some(default_data.clone()),
18297 commit_characters: Some(default_commit_characters.clone()),
18298 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18299 default_edit_range,
18300 )),
18301 insert_text_format: Some(default_insert_text_format),
18302 insert_text_mode: Some(default_insert_text_mode),
18303 }),
18304 ..lsp::CompletionList::default()
18305 })))
18306 }
18307 })
18308 .next()
18309 .await;
18310
18311 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18312 cx.lsp
18313 .server
18314 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18315 let closure_resolved_items = resolved_items.clone();
18316 move |item_to_resolve, _| {
18317 let closure_resolved_items = closure_resolved_items.clone();
18318 async move {
18319 closure_resolved_items.lock().push(item_to_resolve.clone());
18320 Ok(item_to_resolve)
18321 }
18322 }
18323 })
18324 .detach();
18325
18326 cx.condition(|editor, _| editor.context_menu_visible())
18327 .await;
18328 cx.run_until_parked();
18329 cx.update_editor(|editor, _, _| {
18330 let menu = editor.context_menu.borrow_mut();
18331 match menu.as_ref().expect("should have the completions menu") {
18332 CodeContextMenu::Completions(completions_menu) => {
18333 assert_eq!(
18334 completions_menu
18335 .entries
18336 .borrow()
18337 .iter()
18338 .map(|mat| mat.string.clone())
18339 .collect::<Vec<String>>(),
18340 items
18341 .iter()
18342 .map(|completion| completion.label.clone())
18343 .collect::<Vec<String>>()
18344 );
18345 }
18346 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18347 }
18348 });
18349 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18350 // with 4 from the end.
18351 assert_eq!(
18352 *resolved_items.lock(),
18353 [&items[0..16], &items[items.len() - 4..items.len()]]
18354 .concat()
18355 .iter()
18356 .cloned()
18357 .map(|mut item| {
18358 if item.data.is_none() {
18359 item.data = Some(default_data.clone());
18360 }
18361 item
18362 })
18363 .collect::<Vec<lsp::CompletionItem>>(),
18364 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18365 );
18366 resolved_items.lock().clear();
18367
18368 cx.update_editor(|editor, window, cx| {
18369 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18370 });
18371 cx.run_until_parked();
18372 // Completions that have already been resolved are skipped.
18373 assert_eq!(
18374 *resolved_items.lock(),
18375 items[items.len() - 17..items.len() - 4]
18376 .iter()
18377 .cloned()
18378 .map(|mut item| {
18379 if item.data.is_none() {
18380 item.data = Some(default_data.clone());
18381 }
18382 item
18383 })
18384 .collect::<Vec<lsp::CompletionItem>>()
18385 );
18386 resolved_items.lock().clear();
18387}
18388
18389#[gpui::test]
18390async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18391 init_test(cx, |_| {});
18392
18393 let mut cx = EditorLspTestContext::new(
18394 Language::new(
18395 LanguageConfig {
18396 matcher: LanguageMatcher {
18397 path_suffixes: vec!["jsx".into()],
18398 ..Default::default()
18399 },
18400 overrides: [(
18401 "element".into(),
18402 LanguageConfigOverride {
18403 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18404 ..Default::default()
18405 },
18406 )]
18407 .into_iter()
18408 .collect(),
18409 ..Default::default()
18410 },
18411 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18412 )
18413 .with_override_query("(jsx_self_closing_element) @element")
18414 .unwrap(),
18415 lsp::ServerCapabilities {
18416 completion_provider: Some(lsp::CompletionOptions {
18417 trigger_characters: Some(vec![":".to_string()]),
18418 ..Default::default()
18419 }),
18420 ..Default::default()
18421 },
18422 cx,
18423 )
18424 .await;
18425
18426 cx.lsp
18427 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18428 Ok(Some(lsp::CompletionResponse::Array(vec![
18429 lsp::CompletionItem {
18430 label: "bg-blue".into(),
18431 ..Default::default()
18432 },
18433 lsp::CompletionItem {
18434 label: "bg-red".into(),
18435 ..Default::default()
18436 },
18437 lsp::CompletionItem {
18438 label: "bg-yellow".into(),
18439 ..Default::default()
18440 },
18441 ])))
18442 });
18443
18444 cx.set_state(r#"<p class="bgˇ" />"#);
18445
18446 // Trigger completion when typing a dash, because the dash is an extra
18447 // word character in the 'element' scope, which contains the cursor.
18448 cx.simulate_keystroke("-");
18449 cx.executor().run_until_parked();
18450 cx.update_editor(|editor, _, _| {
18451 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18452 {
18453 assert_eq!(
18454 completion_menu_entries(menu),
18455 &["bg-blue", "bg-red", "bg-yellow"]
18456 );
18457 } else {
18458 panic!("expected completion menu to be open");
18459 }
18460 });
18461
18462 cx.simulate_keystroke("l");
18463 cx.executor().run_until_parked();
18464 cx.update_editor(|editor, _, _| {
18465 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18466 {
18467 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18468 } else {
18469 panic!("expected completion menu to be open");
18470 }
18471 });
18472
18473 // When filtering completions, consider the character after the '-' to
18474 // be the start of a subword.
18475 cx.set_state(r#"<p class="yelˇ" />"#);
18476 cx.simulate_keystroke("l");
18477 cx.executor().run_until_parked();
18478 cx.update_editor(|editor, _, _| {
18479 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18480 {
18481 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18482 } else {
18483 panic!("expected completion menu to be open");
18484 }
18485 });
18486}
18487
18488fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18489 let entries = menu.entries.borrow();
18490 entries.iter().map(|mat| mat.string.clone()).collect()
18491}
18492
18493#[gpui::test]
18494async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18495 init_test(cx, |settings| {
18496 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18497 });
18498
18499 let fs = FakeFs::new(cx.executor());
18500 fs.insert_file(path!("/file.ts"), Default::default()).await;
18501
18502 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18503 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18504
18505 language_registry.add(Arc::new(Language::new(
18506 LanguageConfig {
18507 name: "TypeScript".into(),
18508 matcher: LanguageMatcher {
18509 path_suffixes: vec!["ts".to_string()],
18510 ..Default::default()
18511 },
18512 ..Default::default()
18513 },
18514 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18515 )));
18516 update_test_language_settings(cx, |settings| {
18517 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18518 });
18519
18520 let test_plugin = "test_plugin";
18521 let _ = language_registry.register_fake_lsp(
18522 "TypeScript",
18523 FakeLspAdapter {
18524 prettier_plugins: vec![test_plugin],
18525 ..Default::default()
18526 },
18527 );
18528
18529 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18530 let buffer = project
18531 .update(cx, |project, cx| {
18532 project.open_local_buffer(path!("/file.ts"), cx)
18533 })
18534 .await
18535 .unwrap();
18536
18537 let buffer_text = "one\ntwo\nthree\n";
18538 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18539 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18540 editor.update_in(cx, |editor, window, cx| {
18541 editor.set_text(buffer_text, window, cx)
18542 });
18543
18544 editor
18545 .update_in(cx, |editor, window, cx| {
18546 editor.perform_format(
18547 project.clone(),
18548 FormatTrigger::Manual,
18549 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18550 window,
18551 cx,
18552 )
18553 })
18554 .unwrap()
18555 .await;
18556 assert_eq!(
18557 editor.update(cx, |editor, cx| editor.text(cx)),
18558 buffer_text.to_string() + prettier_format_suffix,
18559 "Test prettier formatting was not applied to the original buffer text",
18560 );
18561
18562 update_test_language_settings(cx, |settings| {
18563 settings.defaults.formatter = Some(FormatterList::default())
18564 });
18565 let format = editor.update_in(cx, |editor, window, cx| {
18566 editor.perform_format(
18567 project.clone(),
18568 FormatTrigger::Manual,
18569 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18570 window,
18571 cx,
18572 )
18573 });
18574 format.await.unwrap();
18575 assert_eq!(
18576 editor.update(cx, |editor, cx| editor.text(cx)),
18577 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18578 "Autoformatting (via test prettier) was not applied to the original buffer text",
18579 );
18580}
18581
18582#[gpui::test]
18583async fn test_addition_reverts(cx: &mut TestAppContext) {
18584 init_test(cx, |_| {});
18585 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18586 let base_text = indoc! {r#"
18587 struct Row;
18588 struct Row1;
18589 struct Row2;
18590
18591 struct Row4;
18592 struct Row5;
18593 struct Row6;
18594
18595 struct Row8;
18596 struct Row9;
18597 struct Row10;"#};
18598
18599 // When addition hunks are not adjacent to carets, no hunk revert is performed
18600 assert_hunk_revert(
18601 indoc! {r#"struct Row;
18602 struct Row1;
18603 struct Row1.1;
18604 struct Row1.2;
18605 struct Row2;ˇ
18606
18607 struct Row4;
18608 struct Row5;
18609 struct Row6;
18610
18611 struct Row8;
18612 ˇstruct Row9;
18613 struct Row9.1;
18614 struct Row9.2;
18615 struct Row9.3;
18616 struct Row10;"#},
18617 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18618 indoc! {r#"struct Row;
18619 struct Row1;
18620 struct Row1.1;
18621 struct Row1.2;
18622 struct Row2;ˇ
18623
18624 struct Row4;
18625 struct Row5;
18626 struct Row6;
18627
18628 struct Row8;
18629 ˇstruct Row9;
18630 struct Row9.1;
18631 struct Row9.2;
18632 struct Row9.3;
18633 struct Row10;"#},
18634 base_text,
18635 &mut cx,
18636 );
18637 // Same for selections
18638 assert_hunk_revert(
18639 indoc! {r#"struct Row;
18640 struct Row1;
18641 struct Row2;
18642 struct Row2.1;
18643 struct Row2.2;
18644 «ˇ
18645 struct Row4;
18646 struct» Row5;
18647 «struct Row6;
18648 ˇ»
18649 struct Row9.1;
18650 struct Row9.2;
18651 struct Row9.3;
18652 struct Row8;
18653 struct Row9;
18654 struct Row10;"#},
18655 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18656 indoc! {r#"struct Row;
18657 struct Row1;
18658 struct Row2;
18659 struct Row2.1;
18660 struct Row2.2;
18661 «ˇ
18662 struct Row4;
18663 struct» Row5;
18664 «struct Row6;
18665 ˇ»
18666 struct Row9.1;
18667 struct Row9.2;
18668 struct Row9.3;
18669 struct Row8;
18670 struct Row9;
18671 struct Row10;"#},
18672 base_text,
18673 &mut cx,
18674 );
18675
18676 // When carets and selections intersect the addition hunks, those are reverted.
18677 // Adjacent carets got merged.
18678 assert_hunk_revert(
18679 indoc! {r#"struct Row;
18680 ˇ// something on the top
18681 struct Row1;
18682 struct Row2;
18683 struct Roˇw3.1;
18684 struct Row2.2;
18685 struct Row2.3;ˇ
18686
18687 struct Row4;
18688 struct ˇRow5.1;
18689 struct Row5.2;
18690 struct «Rowˇ»5.3;
18691 struct Row5;
18692 struct Row6;
18693 ˇ
18694 struct Row9.1;
18695 struct «Rowˇ»9.2;
18696 struct «ˇRow»9.3;
18697 struct Row8;
18698 struct Row9;
18699 «ˇ// something on bottom»
18700 struct Row10;"#},
18701 vec![
18702 DiffHunkStatusKind::Added,
18703 DiffHunkStatusKind::Added,
18704 DiffHunkStatusKind::Added,
18705 DiffHunkStatusKind::Added,
18706 DiffHunkStatusKind::Added,
18707 ],
18708 indoc! {r#"struct Row;
18709 ˇstruct Row1;
18710 struct Row2;
18711 ˇ
18712 struct Row4;
18713 ˇstruct Row5;
18714 struct Row6;
18715 ˇ
18716 ˇstruct Row8;
18717 struct Row9;
18718 ˇstruct Row10;"#},
18719 base_text,
18720 &mut cx,
18721 );
18722}
18723
18724#[gpui::test]
18725async fn test_modification_reverts(cx: &mut TestAppContext) {
18726 init_test(cx, |_| {});
18727 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18728 let base_text = indoc! {r#"
18729 struct Row;
18730 struct Row1;
18731 struct Row2;
18732
18733 struct Row4;
18734 struct Row5;
18735 struct Row6;
18736
18737 struct Row8;
18738 struct Row9;
18739 struct Row10;"#};
18740
18741 // Modification hunks behave the same as the addition ones.
18742 assert_hunk_revert(
18743 indoc! {r#"struct Row;
18744 struct Row1;
18745 struct Row33;
18746 ˇ
18747 struct Row4;
18748 struct Row5;
18749 struct Row6;
18750 ˇ
18751 struct Row99;
18752 struct Row9;
18753 struct Row10;"#},
18754 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18755 indoc! {r#"struct Row;
18756 struct Row1;
18757 struct Row33;
18758 ˇ
18759 struct Row4;
18760 struct Row5;
18761 struct Row6;
18762 ˇ
18763 struct Row99;
18764 struct Row9;
18765 struct Row10;"#},
18766 base_text,
18767 &mut cx,
18768 );
18769 assert_hunk_revert(
18770 indoc! {r#"struct Row;
18771 struct Row1;
18772 struct Row33;
18773 «ˇ
18774 struct Row4;
18775 struct» Row5;
18776 «struct Row6;
18777 ˇ»
18778 struct Row99;
18779 struct Row9;
18780 struct Row10;"#},
18781 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18782 indoc! {r#"struct Row;
18783 struct Row1;
18784 struct Row33;
18785 «ˇ
18786 struct Row4;
18787 struct» Row5;
18788 «struct Row6;
18789 ˇ»
18790 struct Row99;
18791 struct Row9;
18792 struct Row10;"#},
18793 base_text,
18794 &mut cx,
18795 );
18796
18797 assert_hunk_revert(
18798 indoc! {r#"ˇstruct Row1.1;
18799 struct Row1;
18800 «ˇstr»uct Row22;
18801
18802 struct ˇRow44;
18803 struct Row5;
18804 struct «Rˇ»ow66;ˇ
18805
18806 «struˇ»ct Row88;
18807 struct Row9;
18808 struct Row1011;ˇ"#},
18809 vec![
18810 DiffHunkStatusKind::Modified,
18811 DiffHunkStatusKind::Modified,
18812 DiffHunkStatusKind::Modified,
18813 DiffHunkStatusKind::Modified,
18814 DiffHunkStatusKind::Modified,
18815 DiffHunkStatusKind::Modified,
18816 ],
18817 indoc! {r#"struct Row;
18818 ˇstruct Row1;
18819 struct Row2;
18820 ˇ
18821 struct Row4;
18822 ˇstruct Row5;
18823 struct Row6;
18824 ˇ
18825 struct Row8;
18826 ˇstruct Row9;
18827 struct Row10;ˇ"#},
18828 base_text,
18829 &mut cx,
18830 );
18831}
18832
18833#[gpui::test]
18834async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18835 init_test(cx, |_| {});
18836 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18837 let base_text = indoc! {r#"
18838 one
18839
18840 two
18841 three
18842 "#};
18843
18844 cx.set_head_text(base_text);
18845 cx.set_state("\nˇ\n");
18846 cx.executor().run_until_parked();
18847 cx.update_editor(|editor, _window, cx| {
18848 editor.expand_selected_diff_hunks(cx);
18849 });
18850 cx.executor().run_until_parked();
18851 cx.update_editor(|editor, window, cx| {
18852 editor.backspace(&Default::default(), window, cx);
18853 });
18854 cx.run_until_parked();
18855 cx.assert_state_with_diff(
18856 indoc! {r#"
18857
18858 - two
18859 - threeˇ
18860 +
18861 "#}
18862 .to_string(),
18863 );
18864}
18865
18866#[gpui::test]
18867async fn test_deletion_reverts(cx: &mut TestAppContext) {
18868 init_test(cx, |_| {});
18869 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18870 let base_text = indoc! {r#"struct Row;
18871struct Row1;
18872struct Row2;
18873
18874struct Row4;
18875struct Row5;
18876struct Row6;
18877
18878struct Row8;
18879struct Row9;
18880struct Row10;"#};
18881
18882 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18883 assert_hunk_revert(
18884 indoc! {r#"struct Row;
18885 struct Row2;
18886
18887 ˇstruct Row4;
18888 struct Row5;
18889 struct Row6;
18890 ˇ
18891 struct Row8;
18892 struct Row10;"#},
18893 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18894 indoc! {r#"struct Row;
18895 struct Row2;
18896
18897 ˇstruct Row4;
18898 struct Row5;
18899 struct Row6;
18900 ˇ
18901 struct Row8;
18902 struct Row10;"#},
18903 base_text,
18904 &mut cx,
18905 );
18906 assert_hunk_revert(
18907 indoc! {r#"struct Row;
18908 struct Row2;
18909
18910 «ˇstruct Row4;
18911 struct» Row5;
18912 «struct Row6;
18913 ˇ»
18914 struct Row8;
18915 struct Row10;"#},
18916 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18917 indoc! {r#"struct Row;
18918 struct Row2;
18919
18920 «ˇstruct Row4;
18921 struct» Row5;
18922 «struct Row6;
18923 ˇ»
18924 struct Row8;
18925 struct Row10;"#},
18926 base_text,
18927 &mut cx,
18928 );
18929
18930 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18931 assert_hunk_revert(
18932 indoc! {r#"struct Row;
18933 ˇstruct Row2;
18934
18935 struct Row4;
18936 struct Row5;
18937 struct Row6;
18938
18939 struct Row8;ˇ
18940 struct Row10;"#},
18941 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18942 indoc! {r#"struct Row;
18943 struct Row1;
18944 ˇstruct Row2;
18945
18946 struct Row4;
18947 struct Row5;
18948 struct Row6;
18949
18950 struct Row8;ˇ
18951 struct Row9;
18952 struct Row10;"#},
18953 base_text,
18954 &mut cx,
18955 );
18956 assert_hunk_revert(
18957 indoc! {r#"struct Row;
18958 struct Row2«ˇ;
18959 struct Row4;
18960 struct» Row5;
18961 «struct Row6;
18962
18963 struct Row8;ˇ»
18964 struct Row10;"#},
18965 vec![
18966 DiffHunkStatusKind::Deleted,
18967 DiffHunkStatusKind::Deleted,
18968 DiffHunkStatusKind::Deleted,
18969 ],
18970 indoc! {r#"struct Row;
18971 struct Row1;
18972 struct Row2«ˇ;
18973
18974 struct Row4;
18975 struct» Row5;
18976 «struct Row6;
18977
18978 struct Row8;ˇ»
18979 struct Row9;
18980 struct Row10;"#},
18981 base_text,
18982 &mut cx,
18983 );
18984}
18985
18986#[gpui::test]
18987async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18988 init_test(cx, |_| {});
18989
18990 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18991 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18992 let base_text_3 =
18993 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18994
18995 let text_1 = edit_first_char_of_every_line(base_text_1);
18996 let text_2 = edit_first_char_of_every_line(base_text_2);
18997 let text_3 = edit_first_char_of_every_line(base_text_3);
18998
18999 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19000 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19001 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19002
19003 let multibuffer = cx.new(|cx| {
19004 let mut multibuffer = MultiBuffer::new(ReadWrite);
19005 multibuffer.push_excerpts(
19006 buffer_1.clone(),
19007 [
19008 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19009 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19010 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19011 ],
19012 cx,
19013 );
19014 multibuffer.push_excerpts(
19015 buffer_2.clone(),
19016 [
19017 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19018 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19019 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19020 ],
19021 cx,
19022 );
19023 multibuffer.push_excerpts(
19024 buffer_3.clone(),
19025 [
19026 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19027 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19028 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19029 ],
19030 cx,
19031 );
19032 multibuffer
19033 });
19034
19035 let fs = FakeFs::new(cx.executor());
19036 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19037 let (editor, cx) = cx
19038 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19039 editor.update_in(cx, |editor, _window, cx| {
19040 for (buffer, diff_base) in [
19041 (buffer_1.clone(), base_text_1),
19042 (buffer_2.clone(), base_text_2),
19043 (buffer_3.clone(), base_text_3),
19044 ] {
19045 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19046 editor
19047 .buffer
19048 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19049 }
19050 });
19051 cx.executor().run_until_parked();
19052
19053 editor.update_in(cx, |editor, window, cx| {
19054 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}");
19055 editor.select_all(&SelectAll, window, cx);
19056 editor.git_restore(&Default::default(), window, cx);
19057 });
19058 cx.executor().run_until_parked();
19059
19060 // When all ranges are selected, all buffer hunks are reverted.
19061 editor.update(cx, |editor, cx| {
19062 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");
19063 });
19064 buffer_1.update(cx, |buffer, _| {
19065 assert_eq!(buffer.text(), base_text_1);
19066 });
19067 buffer_2.update(cx, |buffer, _| {
19068 assert_eq!(buffer.text(), base_text_2);
19069 });
19070 buffer_3.update(cx, |buffer, _| {
19071 assert_eq!(buffer.text(), base_text_3);
19072 });
19073
19074 editor.update_in(cx, |editor, window, cx| {
19075 editor.undo(&Default::default(), window, cx);
19076 });
19077
19078 editor.update_in(cx, |editor, window, cx| {
19079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19080 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19081 });
19082 editor.git_restore(&Default::default(), window, cx);
19083 });
19084
19085 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19086 // but not affect buffer_2 and its related excerpts.
19087 editor.update(cx, |editor, cx| {
19088 assert_eq!(
19089 editor.text(cx),
19090 "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}"
19091 );
19092 });
19093 buffer_1.update(cx, |buffer, _| {
19094 assert_eq!(buffer.text(), base_text_1);
19095 });
19096 buffer_2.update(cx, |buffer, _| {
19097 assert_eq!(
19098 buffer.text(),
19099 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19100 );
19101 });
19102 buffer_3.update(cx, |buffer, _| {
19103 assert_eq!(
19104 buffer.text(),
19105 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19106 );
19107 });
19108
19109 fn edit_first_char_of_every_line(text: &str) -> String {
19110 text.split('\n')
19111 .map(|line| format!("X{}", &line[1..]))
19112 .collect::<Vec<_>>()
19113 .join("\n")
19114 }
19115}
19116
19117#[gpui::test]
19118async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19119 init_test(cx, |_| {});
19120
19121 let cols = 4;
19122 let rows = 10;
19123 let sample_text_1 = sample_text(rows, cols, 'a');
19124 assert_eq!(
19125 sample_text_1,
19126 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19127 );
19128 let sample_text_2 = sample_text(rows, cols, 'l');
19129 assert_eq!(
19130 sample_text_2,
19131 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19132 );
19133 let sample_text_3 = sample_text(rows, cols, 'v');
19134 assert_eq!(
19135 sample_text_3,
19136 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19137 );
19138
19139 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19140 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19141 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19142
19143 let multi_buffer = cx.new(|cx| {
19144 let mut multibuffer = MultiBuffer::new(ReadWrite);
19145 multibuffer.push_excerpts(
19146 buffer_1.clone(),
19147 [
19148 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19149 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19150 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19151 ],
19152 cx,
19153 );
19154 multibuffer.push_excerpts(
19155 buffer_2.clone(),
19156 [
19157 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19158 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19159 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19160 ],
19161 cx,
19162 );
19163 multibuffer.push_excerpts(
19164 buffer_3.clone(),
19165 [
19166 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19167 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19168 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19169 ],
19170 cx,
19171 );
19172 multibuffer
19173 });
19174
19175 let fs = FakeFs::new(cx.executor());
19176 fs.insert_tree(
19177 "/a",
19178 json!({
19179 "main.rs": sample_text_1,
19180 "other.rs": sample_text_2,
19181 "lib.rs": sample_text_3,
19182 }),
19183 )
19184 .await;
19185 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19186 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19187 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19188 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19189 Editor::new(
19190 EditorMode::full(),
19191 multi_buffer,
19192 Some(project.clone()),
19193 window,
19194 cx,
19195 )
19196 });
19197 let multibuffer_item_id = workspace
19198 .update(cx, |workspace, window, cx| {
19199 assert!(
19200 workspace.active_item(cx).is_none(),
19201 "active item should be None before the first item is added"
19202 );
19203 workspace.add_item_to_active_pane(
19204 Box::new(multi_buffer_editor.clone()),
19205 None,
19206 true,
19207 window,
19208 cx,
19209 );
19210 let active_item = workspace
19211 .active_item(cx)
19212 .expect("should have an active item after adding the multi buffer");
19213 assert_eq!(
19214 active_item.buffer_kind(cx),
19215 ItemBufferKind::Multibuffer,
19216 "A multi buffer was expected to active after adding"
19217 );
19218 active_item.item_id()
19219 })
19220 .unwrap();
19221 cx.executor().run_until_parked();
19222
19223 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19224 editor.change_selections(
19225 SelectionEffects::scroll(Autoscroll::Next),
19226 window,
19227 cx,
19228 |s| s.select_ranges(Some(1..2)),
19229 );
19230 editor.open_excerpts(&OpenExcerpts, window, cx);
19231 });
19232 cx.executor().run_until_parked();
19233 let first_item_id = workspace
19234 .update(cx, |workspace, window, cx| {
19235 let active_item = workspace
19236 .active_item(cx)
19237 .expect("should have an active item after navigating into the 1st buffer");
19238 let first_item_id = active_item.item_id();
19239 assert_ne!(
19240 first_item_id, multibuffer_item_id,
19241 "Should navigate into the 1st buffer and activate it"
19242 );
19243 assert_eq!(
19244 active_item.buffer_kind(cx),
19245 ItemBufferKind::Singleton,
19246 "New active item should be a singleton buffer"
19247 );
19248 assert_eq!(
19249 active_item
19250 .act_as::<Editor>(cx)
19251 .expect("should have navigated into an editor for the 1st buffer")
19252 .read(cx)
19253 .text(cx),
19254 sample_text_1
19255 );
19256
19257 workspace
19258 .go_back(workspace.active_pane().downgrade(), window, cx)
19259 .detach_and_log_err(cx);
19260
19261 first_item_id
19262 })
19263 .unwrap();
19264 cx.executor().run_until_parked();
19265 workspace
19266 .update(cx, |workspace, _, cx| {
19267 let active_item = workspace
19268 .active_item(cx)
19269 .expect("should have an active item after navigating back");
19270 assert_eq!(
19271 active_item.item_id(),
19272 multibuffer_item_id,
19273 "Should navigate back to the multi buffer"
19274 );
19275 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19276 })
19277 .unwrap();
19278
19279 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19280 editor.change_selections(
19281 SelectionEffects::scroll(Autoscroll::Next),
19282 window,
19283 cx,
19284 |s| s.select_ranges(Some(39..40)),
19285 );
19286 editor.open_excerpts(&OpenExcerpts, window, cx);
19287 });
19288 cx.executor().run_until_parked();
19289 let second_item_id = workspace
19290 .update(cx, |workspace, window, cx| {
19291 let active_item = workspace
19292 .active_item(cx)
19293 .expect("should have an active item after navigating into the 2nd buffer");
19294 let second_item_id = active_item.item_id();
19295 assert_ne!(
19296 second_item_id, multibuffer_item_id,
19297 "Should navigate away from the multibuffer"
19298 );
19299 assert_ne!(
19300 second_item_id, first_item_id,
19301 "Should navigate into the 2nd buffer and activate it"
19302 );
19303 assert_eq!(
19304 active_item.buffer_kind(cx),
19305 ItemBufferKind::Singleton,
19306 "New active item should be a singleton buffer"
19307 );
19308 assert_eq!(
19309 active_item
19310 .act_as::<Editor>(cx)
19311 .expect("should have navigated into an editor")
19312 .read(cx)
19313 .text(cx),
19314 sample_text_2
19315 );
19316
19317 workspace
19318 .go_back(workspace.active_pane().downgrade(), window, cx)
19319 .detach_and_log_err(cx);
19320
19321 second_item_id
19322 })
19323 .unwrap();
19324 cx.executor().run_until_parked();
19325 workspace
19326 .update(cx, |workspace, _, cx| {
19327 let active_item = workspace
19328 .active_item(cx)
19329 .expect("should have an active item after navigating back from the 2nd buffer");
19330 assert_eq!(
19331 active_item.item_id(),
19332 multibuffer_item_id,
19333 "Should navigate back from the 2nd buffer to the multi buffer"
19334 );
19335 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19336 })
19337 .unwrap();
19338
19339 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19340 editor.change_selections(
19341 SelectionEffects::scroll(Autoscroll::Next),
19342 window,
19343 cx,
19344 |s| s.select_ranges(Some(70..70)),
19345 );
19346 editor.open_excerpts(&OpenExcerpts, window, cx);
19347 });
19348 cx.executor().run_until_parked();
19349 workspace
19350 .update(cx, |workspace, window, cx| {
19351 let active_item = workspace
19352 .active_item(cx)
19353 .expect("should have an active item after navigating into the 3rd buffer");
19354 let third_item_id = active_item.item_id();
19355 assert_ne!(
19356 third_item_id, multibuffer_item_id,
19357 "Should navigate into the 3rd buffer and activate it"
19358 );
19359 assert_ne!(third_item_id, first_item_id);
19360 assert_ne!(third_item_id, second_item_id);
19361 assert_eq!(
19362 active_item.buffer_kind(cx),
19363 ItemBufferKind::Singleton,
19364 "New active item should be a singleton buffer"
19365 );
19366 assert_eq!(
19367 active_item
19368 .act_as::<Editor>(cx)
19369 .expect("should have navigated into an editor")
19370 .read(cx)
19371 .text(cx),
19372 sample_text_3
19373 );
19374
19375 workspace
19376 .go_back(workspace.active_pane().downgrade(), window, cx)
19377 .detach_and_log_err(cx);
19378 })
19379 .unwrap();
19380 cx.executor().run_until_parked();
19381 workspace
19382 .update(cx, |workspace, _, cx| {
19383 let active_item = workspace
19384 .active_item(cx)
19385 .expect("should have an active item after navigating back from the 3rd buffer");
19386 assert_eq!(
19387 active_item.item_id(),
19388 multibuffer_item_id,
19389 "Should navigate back from the 3rd buffer to the multi buffer"
19390 );
19391 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19392 })
19393 .unwrap();
19394}
19395
19396#[gpui::test]
19397async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19398 init_test(cx, |_| {});
19399
19400 let mut cx = EditorTestContext::new(cx).await;
19401
19402 let diff_base = r#"
19403 use some::mod;
19404
19405 const A: u32 = 42;
19406
19407 fn main() {
19408 println!("hello");
19409
19410 println!("world");
19411 }
19412 "#
19413 .unindent();
19414
19415 cx.set_state(
19416 &r#"
19417 use some::modified;
19418
19419 ˇ
19420 fn main() {
19421 println!("hello there");
19422
19423 println!("around the");
19424 println!("world");
19425 }
19426 "#
19427 .unindent(),
19428 );
19429
19430 cx.set_head_text(&diff_base);
19431 executor.run_until_parked();
19432
19433 cx.update_editor(|editor, window, cx| {
19434 editor.go_to_next_hunk(&GoToHunk, window, cx);
19435 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19436 });
19437 executor.run_until_parked();
19438 cx.assert_state_with_diff(
19439 r#"
19440 use some::modified;
19441
19442
19443 fn main() {
19444 - println!("hello");
19445 + ˇ println!("hello there");
19446
19447 println!("around the");
19448 println!("world");
19449 }
19450 "#
19451 .unindent(),
19452 );
19453
19454 cx.update_editor(|editor, window, cx| {
19455 for _ in 0..2 {
19456 editor.go_to_next_hunk(&GoToHunk, window, cx);
19457 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19458 }
19459 });
19460 executor.run_until_parked();
19461 cx.assert_state_with_diff(
19462 r#"
19463 - use some::mod;
19464 + ˇuse some::modified;
19465
19466
19467 fn main() {
19468 - println!("hello");
19469 + println!("hello there");
19470
19471 + println!("around the");
19472 println!("world");
19473 }
19474 "#
19475 .unindent(),
19476 );
19477
19478 cx.update_editor(|editor, window, cx| {
19479 editor.go_to_next_hunk(&GoToHunk, window, cx);
19480 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19481 });
19482 executor.run_until_parked();
19483 cx.assert_state_with_diff(
19484 r#"
19485 - use some::mod;
19486 + use some::modified;
19487
19488 - const A: u32 = 42;
19489 ˇ
19490 fn main() {
19491 - println!("hello");
19492 + println!("hello there");
19493
19494 + println!("around the");
19495 println!("world");
19496 }
19497 "#
19498 .unindent(),
19499 );
19500
19501 cx.update_editor(|editor, window, cx| {
19502 editor.cancel(&Cancel, window, cx);
19503 });
19504
19505 cx.assert_state_with_diff(
19506 r#"
19507 use some::modified;
19508
19509 ˇ
19510 fn main() {
19511 println!("hello there");
19512
19513 println!("around the");
19514 println!("world");
19515 }
19516 "#
19517 .unindent(),
19518 );
19519}
19520
19521#[gpui::test]
19522async fn test_diff_base_change_with_expanded_diff_hunks(
19523 executor: BackgroundExecutor,
19524 cx: &mut TestAppContext,
19525) {
19526 init_test(cx, |_| {});
19527
19528 let mut cx = EditorTestContext::new(cx).await;
19529
19530 let diff_base = r#"
19531 use some::mod1;
19532 use some::mod2;
19533
19534 const A: u32 = 42;
19535 const B: u32 = 42;
19536 const C: u32 = 42;
19537
19538 fn main() {
19539 println!("hello");
19540
19541 println!("world");
19542 }
19543 "#
19544 .unindent();
19545
19546 cx.set_state(
19547 &r#"
19548 use some::mod2;
19549
19550 const A: u32 = 42;
19551 const C: u32 = 42;
19552
19553 fn main(ˇ) {
19554 //println!("hello");
19555
19556 println!("world");
19557 //
19558 //
19559 }
19560 "#
19561 .unindent(),
19562 );
19563
19564 cx.set_head_text(&diff_base);
19565 executor.run_until_parked();
19566
19567 cx.update_editor(|editor, window, cx| {
19568 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19569 });
19570 executor.run_until_parked();
19571 cx.assert_state_with_diff(
19572 r#"
19573 - use some::mod1;
19574 use some::mod2;
19575
19576 const A: u32 = 42;
19577 - const B: u32 = 42;
19578 const C: u32 = 42;
19579
19580 fn main(ˇ) {
19581 - println!("hello");
19582 + //println!("hello");
19583
19584 println!("world");
19585 + //
19586 + //
19587 }
19588 "#
19589 .unindent(),
19590 );
19591
19592 cx.set_head_text("new diff base!");
19593 executor.run_until_parked();
19594 cx.assert_state_with_diff(
19595 r#"
19596 - new diff base!
19597 + use some::mod2;
19598 +
19599 + const A: u32 = 42;
19600 + const C: u32 = 42;
19601 +
19602 + fn main(ˇ) {
19603 + //println!("hello");
19604 +
19605 + println!("world");
19606 + //
19607 + //
19608 + }
19609 "#
19610 .unindent(),
19611 );
19612}
19613
19614#[gpui::test]
19615async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19616 init_test(cx, |_| {});
19617
19618 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19619 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19620 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19621 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19622 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19623 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19624
19625 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19626 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19627 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19628
19629 let multi_buffer = cx.new(|cx| {
19630 let mut multibuffer = MultiBuffer::new(ReadWrite);
19631 multibuffer.push_excerpts(
19632 buffer_1.clone(),
19633 [
19634 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19635 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19636 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19637 ],
19638 cx,
19639 );
19640 multibuffer.push_excerpts(
19641 buffer_2.clone(),
19642 [
19643 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19644 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19645 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19646 ],
19647 cx,
19648 );
19649 multibuffer.push_excerpts(
19650 buffer_3.clone(),
19651 [
19652 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19653 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19654 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19655 ],
19656 cx,
19657 );
19658 multibuffer
19659 });
19660
19661 let editor =
19662 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19663 editor
19664 .update(cx, |editor, _window, cx| {
19665 for (buffer, diff_base) in [
19666 (buffer_1.clone(), file_1_old),
19667 (buffer_2.clone(), file_2_old),
19668 (buffer_3.clone(), file_3_old),
19669 ] {
19670 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19671 editor
19672 .buffer
19673 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19674 }
19675 })
19676 .unwrap();
19677
19678 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19679 cx.run_until_parked();
19680
19681 cx.assert_editor_state(
19682 &"
19683 ˇaaa
19684 ccc
19685 ddd
19686
19687 ggg
19688 hhh
19689
19690
19691 lll
19692 mmm
19693 NNN
19694
19695 qqq
19696 rrr
19697
19698 uuu
19699 111
19700 222
19701 333
19702
19703 666
19704 777
19705
19706 000
19707 !!!"
19708 .unindent(),
19709 );
19710
19711 cx.update_editor(|editor, window, cx| {
19712 editor.select_all(&SelectAll, window, cx);
19713 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19714 });
19715 cx.executor().run_until_parked();
19716
19717 cx.assert_state_with_diff(
19718 "
19719 «aaa
19720 - bbb
19721 ccc
19722 ddd
19723
19724 ggg
19725 hhh
19726
19727
19728 lll
19729 mmm
19730 - nnn
19731 + NNN
19732
19733 qqq
19734 rrr
19735
19736 uuu
19737 111
19738 222
19739 333
19740
19741 + 666
19742 777
19743
19744 000
19745 !!!ˇ»"
19746 .unindent(),
19747 );
19748}
19749
19750#[gpui::test]
19751async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19752 init_test(cx, |_| {});
19753
19754 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19755 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19756
19757 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19758 let multi_buffer = cx.new(|cx| {
19759 let mut multibuffer = MultiBuffer::new(ReadWrite);
19760 multibuffer.push_excerpts(
19761 buffer.clone(),
19762 [
19763 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19764 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19765 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19766 ],
19767 cx,
19768 );
19769 multibuffer
19770 });
19771
19772 let editor =
19773 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19774 editor
19775 .update(cx, |editor, _window, cx| {
19776 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19777 editor
19778 .buffer
19779 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19780 })
19781 .unwrap();
19782
19783 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19784 cx.run_until_parked();
19785
19786 cx.update_editor(|editor, window, cx| {
19787 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19788 });
19789 cx.executor().run_until_parked();
19790
19791 // When the start of a hunk coincides with the start of its excerpt,
19792 // the hunk is expanded. When the start of a hunk is earlier than
19793 // the start of its excerpt, the hunk is not expanded.
19794 cx.assert_state_with_diff(
19795 "
19796 ˇaaa
19797 - bbb
19798 + BBB
19799
19800 - ddd
19801 - eee
19802 + DDD
19803 + EEE
19804 fff
19805
19806 iii
19807 "
19808 .unindent(),
19809 );
19810}
19811
19812#[gpui::test]
19813async fn test_edits_around_expanded_insertion_hunks(
19814 executor: BackgroundExecutor,
19815 cx: &mut TestAppContext,
19816) {
19817 init_test(cx, |_| {});
19818
19819 let mut cx = EditorTestContext::new(cx).await;
19820
19821 let diff_base = r#"
19822 use some::mod1;
19823 use some::mod2;
19824
19825 const A: u32 = 42;
19826
19827 fn main() {
19828 println!("hello");
19829
19830 println!("world");
19831 }
19832 "#
19833 .unindent();
19834 executor.run_until_parked();
19835 cx.set_state(
19836 &r#"
19837 use some::mod1;
19838 use some::mod2;
19839
19840 const A: u32 = 42;
19841 const B: u32 = 42;
19842 const C: u32 = 42;
19843 ˇ
19844
19845 fn main() {
19846 println!("hello");
19847
19848 println!("world");
19849 }
19850 "#
19851 .unindent(),
19852 );
19853
19854 cx.set_head_text(&diff_base);
19855 executor.run_until_parked();
19856
19857 cx.update_editor(|editor, window, cx| {
19858 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19859 });
19860 executor.run_until_parked();
19861
19862 cx.assert_state_with_diff(
19863 r#"
19864 use some::mod1;
19865 use some::mod2;
19866
19867 const A: u32 = 42;
19868 + const B: u32 = 42;
19869 + const C: u32 = 42;
19870 + ˇ
19871
19872 fn main() {
19873 println!("hello");
19874
19875 println!("world");
19876 }
19877 "#
19878 .unindent(),
19879 );
19880
19881 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19882 executor.run_until_parked();
19883
19884 cx.assert_state_with_diff(
19885 r#"
19886 use some::mod1;
19887 use some::mod2;
19888
19889 const A: u32 = 42;
19890 + const B: u32 = 42;
19891 + const C: u32 = 42;
19892 + const D: u32 = 42;
19893 + ˇ
19894
19895 fn main() {
19896 println!("hello");
19897
19898 println!("world");
19899 }
19900 "#
19901 .unindent(),
19902 );
19903
19904 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19905 executor.run_until_parked();
19906
19907 cx.assert_state_with_diff(
19908 r#"
19909 use some::mod1;
19910 use some::mod2;
19911
19912 const A: u32 = 42;
19913 + const B: u32 = 42;
19914 + const C: u32 = 42;
19915 + const D: u32 = 42;
19916 + const E: u32 = 42;
19917 + ˇ
19918
19919 fn main() {
19920 println!("hello");
19921
19922 println!("world");
19923 }
19924 "#
19925 .unindent(),
19926 );
19927
19928 cx.update_editor(|editor, window, cx| {
19929 editor.delete_line(&DeleteLine, window, cx);
19930 });
19931 executor.run_until_parked();
19932
19933 cx.assert_state_with_diff(
19934 r#"
19935 use some::mod1;
19936 use some::mod2;
19937
19938 const A: u32 = 42;
19939 + const B: u32 = 42;
19940 + const C: u32 = 42;
19941 + const D: u32 = 42;
19942 + const E: u32 = 42;
19943 ˇ
19944 fn main() {
19945 println!("hello");
19946
19947 println!("world");
19948 }
19949 "#
19950 .unindent(),
19951 );
19952
19953 cx.update_editor(|editor, window, cx| {
19954 editor.move_up(&MoveUp, window, cx);
19955 editor.delete_line(&DeleteLine, window, cx);
19956 editor.move_up(&MoveUp, window, cx);
19957 editor.delete_line(&DeleteLine, window, cx);
19958 editor.move_up(&MoveUp, window, cx);
19959 editor.delete_line(&DeleteLine, window, cx);
19960 });
19961 executor.run_until_parked();
19962 cx.assert_state_with_diff(
19963 r#"
19964 use some::mod1;
19965 use some::mod2;
19966
19967 const A: u32 = 42;
19968 + const B: u32 = 42;
19969 ˇ
19970 fn main() {
19971 println!("hello");
19972
19973 println!("world");
19974 }
19975 "#
19976 .unindent(),
19977 );
19978
19979 cx.update_editor(|editor, window, cx| {
19980 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19981 editor.delete_line(&DeleteLine, window, cx);
19982 });
19983 executor.run_until_parked();
19984 cx.assert_state_with_diff(
19985 r#"
19986 ˇ
19987 fn main() {
19988 println!("hello");
19989
19990 println!("world");
19991 }
19992 "#
19993 .unindent(),
19994 );
19995}
19996
19997#[gpui::test]
19998async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19999 init_test(cx, |_| {});
20000
20001 let mut cx = EditorTestContext::new(cx).await;
20002 cx.set_head_text(indoc! { "
20003 one
20004 two
20005 three
20006 four
20007 five
20008 "
20009 });
20010 cx.set_state(indoc! { "
20011 one
20012 ˇthree
20013 five
20014 "});
20015 cx.run_until_parked();
20016 cx.update_editor(|editor, window, cx| {
20017 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20018 });
20019 cx.assert_state_with_diff(
20020 indoc! { "
20021 one
20022 - two
20023 ˇthree
20024 - four
20025 five
20026 "}
20027 .to_string(),
20028 );
20029 cx.update_editor(|editor, window, cx| {
20030 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20031 });
20032
20033 cx.assert_state_with_diff(
20034 indoc! { "
20035 one
20036 ˇthree
20037 five
20038 "}
20039 .to_string(),
20040 );
20041
20042 cx.set_state(indoc! { "
20043 one
20044 ˇTWO
20045 three
20046 four
20047 five
20048 "});
20049 cx.run_until_parked();
20050 cx.update_editor(|editor, window, cx| {
20051 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20052 });
20053
20054 cx.assert_state_with_diff(
20055 indoc! { "
20056 one
20057 - two
20058 + ˇTWO
20059 three
20060 four
20061 five
20062 "}
20063 .to_string(),
20064 );
20065 cx.update_editor(|editor, window, cx| {
20066 editor.move_up(&Default::default(), window, cx);
20067 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20068 });
20069 cx.assert_state_with_diff(
20070 indoc! { "
20071 one
20072 ˇTWO
20073 three
20074 four
20075 five
20076 "}
20077 .to_string(),
20078 );
20079}
20080
20081#[gpui::test]
20082async fn test_edits_around_expanded_deletion_hunks(
20083 executor: BackgroundExecutor,
20084 cx: &mut TestAppContext,
20085) {
20086 init_test(cx, |_| {});
20087
20088 let mut cx = EditorTestContext::new(cx).await;
20089
20090 let diff_base = r#"
20091 use some::mod1;
20092 use some::mod2;
20093
20094 const A: u32 = 42;
20095 const B: u32 = 42;
20096 const C: u32 = 42;
20097
20098
20099 fn main() {
20100 println!("hello");
20101
20102 println!("world");
20103 }
20104 "#
20105 .unindent();
20106 executor.run_until_parked();
20107 cx.set_state(
20108 &r#"
20109 use some::mod1;
20110 use some::mod2;
20111
20112 ˇconst B: u32 = 42;
20113 const C: u32 = 42;
20114
20115
20116 fn main() {
20117 println!("hello");
20118
20119 println!("world");
20120 }
20121 "#
20122 .unindent(),
20123 );
20124
20125 cx.set_head_text(&diff_base);
20126 executor.run_until_parked();
20127
20128 cx.update_editor(|editor, window, cx| {
20129 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20130 });
20131 executor.run_until_parked();
20132
20133 cx.assert_state_with_diff(
20134 r#"
20135 use some::mod1;
20136 use some::mod2;
20137
20138 - const A: u32 = 42;
20139 ˇconst B: u32 = 42;
20140 const C: u32 = 42;
20141
20142
20143 fn main() {
20144 println!("hello");
20145
20146 println!("world");
20147 }
20148 "#
20149 .unindent(),
20150 );
20151
20152 cx.update_editor(|editor, window, cx| {
20153 editor.delete_line(&DeleteLine, window, cx);
20154 });
20155 executor.run_until_parked();
20156 cx.assert_state_with_diff(
20157 r#"
20158 use some::mod1;
20159 use some::mod2;
20160
20161 - const A: u32 = 42;
20162 - const B: u32 = 42;
20163 ˇconst C: u32 = 42;
20164
20165
20166 fn main() {
20167 println!("hello");
20168
20169 println!("world");
20170 }
20171 "#
20172 .unindent(),
20173 );
20174
20175 cx.update_editor(|editor, window, cx| {
20176 editor.delete_line(&DeleteLine, window, cx);
20177 });
20178 executor.run_until_parked();
20179 cx.assert_state_with_diff(
20180 r#"
20181 use some::mod1;
20182 use some::mod2;
20183
20184 - const A: u32 = 42;
20185 - const B: u32 = 42;
20186 - const C: u32 = 42;
20187 ˇ
20188
20189 fn main() {
20190 println!("hello");
20191
20192 println!("world");
20193 }
20194 "#
20195 .unindent(),
20196 );
20197
20198 cx.update_editor(|editor, window, cx| {
20199 editor.handle_input("replacement", window, cx);
20200 });
20201 executor.run_until_parked();
20202 cx.assert_state_with_diff(
20203 r#"
20204 use some::mod1;
20205 use some::mod2;
20206
20207 - const A: u32 = 42;
20208 - const B: u32 = 42;
20209 - const C: u32 = 42;
20210 -
20211 + replacementˇ
20212
20213 fn main() {
20214 println!("hello");
20215
20216 println!("world");
20217 }
20218 "#
20219 .unindent(),
20220 );
20221}
20222
20223#[gpui::test]
20224async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20225 init_test(cx, |_| {});
20226
20227 let mut cx = EditorTestContext::new(cx).await;
20228
20229 let base_text = r#"
20230 one
20231 two
20232 three
20233 four
20234 five
20235 "#
20236 .unindent();
20237 executor.run_until_parked();
20238 cx.set_state(
20239 &r#"
20240 one
20241 two
20242 fˇour
20243 five
20244 "#
20245 .unindent(),
20246 );
20247
20248 cx.set_head_text(&base_text);
20249 executor.run_until_parked();
20250
20251 cx.update_editor(|editor, window, cx| {
20252 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20253 });
20254 executor.run_until_parked();
20255
20256 cx.assert_state_with_diff(
20257 r#"
20258 one
20259 two
20260 - three
20261 fˇour
20262 five
20263 "#
20264 .unindent(),
20265 );
20266
20267 cx.update_editor(|editor, window, cx| {
20268 editor.backspace(&Backspace, window, cx);
20269 editor.backspace(&Backspace, window, cx);
20270 });
20271 executor.run_until_parked();
20272 cx.assert_state_with_diff(
20273 r#"
20274 one
20275 two
20276 - threeˇ
20277 - four
20278 + our
20279 five
20280 "#
20281 .unindent(),
20282 );
20283}
20284
20285#[gpui::test]
20286async fn test_edit_after_expanded_modification_hunk(
20287 executor: BackgroundExecutor,
20288 cx: &mut TestAppContext,
20289) {
20290 init_test(cx, |_| {});
20291
20292 let mut cx = EditorTestContext::new(cx).await;
20293
20294 let diff_base = r#"
20295 use some::mod1;
20296 use some::mod2;
20297
20298 const A: u32 = 42;
20299 const B: u32 = 42;
20300 const C: u32 = 42;
20301 const D: u32 = 42;
20302
20303
20304 fn main() {
20305 println!("hello");
20306
20307 println!("world");
20308 }"#
20309 .unindent();
20310
20311 cx.set_state(
20312 &r#"
20313 use some::mod1;
20314 use some::mod2;
20315
20316 const A: u32 = 42;
20317 const B: u32 = 42;
20318 const C: u32 = 43ˇ
20319 const D: u32 = 42;
20320
20321
20322 fn main() {
20323 println!("hello");
20324
20325 println!("world");
20326 }"#
20327 .unindent(),
20328 );
20329
20330 cx.set_head_text(&diff_base);
20331 executor.run_until_parked();
20332 cx.update_editor(|editor, window, cx| {
20333 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20334 });
20335 executor.run_until_parked();
20336
20337 cx.assert_state_with_diff(
20338 r#"
20339 use some::mod1;
20340 use some::mod2;
20341
20342 const A: u32 = 42;
20343 const B: u32 = 42;
20344 - const C: u32 = 42;
20345 + const C: u32 = 43ˇ
20346 const D: u32 = 42;
20347
20348
20349 fn main() {
20350 println!("hello");
20351
20352 println!("world");
20353 }"#
20354 .unindent(),
20355 );
20356
20357 cx.update_editor(|editor, window, cx| {
20358 editor.handle_input("\nnew_line\n", window, cx);
20359 });
20360 executor.run_until_parked();
20361
20362 cx.assert_state_with_diff(
20363 r#"
20364 use some::mod1;
20365 use some::mod2;
20366
20367 const A: u32 = 42;
20368 const B: u32 = 42;
20369 - const C: u32 = 42;
20370 + const C: u32 = 43
20371 + new_line
20372 + ˇ
20373 const D: u32 = 42;
20374
20375
20376 fn main() {
20377 println!("hello");
20378
20379 println!("world");
20380 }"#
20381 .unindent(),
20382 );
20383}
20384
20385#[gpui::test]
20386async fn test_stage_and_unstage_added_file_hunk(
20387 executor: BackgroundExecutor,
20388 cx: &mut TestAppContext,
20389) {
20390 init_test(cx, |_| {});
20391
20392 let mut cx = EditorTestContext::new(cx).await;
20393 cx.update_editor(|editor, _, cx| {
20394 editor.set_expand_all_diff_hunks(cx);
20395 });
20396
20397 let working_copy = r#"
20398 ˇfn main() {
20399 println!("hello, world!");
20400 }
20401 "#
20402 .unindent();
20403
20404 cx.set_state(&working_copy);
20405 executor.run_until_parked();
20406
20407 cx.assert_state_with_diff(
20408 r#"
20409 + ˇfn main() {
20410 + println!("hello, world!");
20411 + }
20412 "#
20413 .unindent(),
20414 );
20415 cx.assert_index_text(None);
20416
20417 cx.update_editor(|editor, window, cx| {
20418 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20419 });
20420 executor.run_until_parked();
20421 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20422 cx.assert_state_with_diff(
20423 r#"
20424 + ˇfn main() {
20425 + println!("hello, world!");
20426 + }
20427 "#
20428 .unindent(),
20429 );
20430
20431 cx.update_editor(|editor, window, cx| {
20432 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20433 });
20434 executor.run_until_parked();
20435 cx.assert_index_text(None);
20436}
20437
20438async fn setup_indent_guides_editor(
20439 text: &str,
20440 cx: &mut TestAppContext,
20441) -> (BufferId, EditorTestContext) {
20442 init_test(cx, |_| {});
20443
20444 let mut cx = EditorTestContext::new(cx).await;
20445
20446 let buffer_id = cx.update_editor(|editor, window, cx| {
20447 editor.set_text(text, window, cx);
20448 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20449
20450 buffer_ids[0]
20451 });
20452
20453 (buffer_id, cx)
20454}
20455
20456fn assert_indent_guides(
20457 range: Range<u32>,
20458 expected: Vec<IndentGuide>,
20459 active_indices: Option<Vec<usize>>,
20460 cx: &mut EditorTestContext,
20461) {
20462 let indent_guides = cx.update_editor(|editor, window, cx| {
20463 let snapshot = editor.snapshot(window, cx).display_snapshot;
20464 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20465 editor,
20466 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20467 true,
20468 &snapshot,
20469 cx,
20470 );
20471
20472 indent_guides.sort_by(|a, b| {
20473 a.depth.cmp(&b.depth).then(
20474 a.start_row
20475 .cmp(&b.start_row)
20476 .then(a.end_row.cmp(&b.end_row)),
20477 )
20478 });
20479 indent_guides
20480 });
20481
20482 if let Some(expected) = active_indices {
20483 let active_indices = cx.update_editor(|editor, window, cx| {
20484 let snapshot = editor.snapshot(window, cx).display_snapshot;
20485 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20486 });
20487
20488 assert_eq!(
20489 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20490 expected,
20491 "Active indent guide indices do not match"
20492 );
20493 }
20494
20495 assert_eq!(indent_guides, expected, "Indent guides do not match");
20496}
20497
20498fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20499 IndentGuide {
20500 buffer_id,
20501 start_row: MultiBufferRow(start_row),
20502 end_row: MultiBufferRow(end_row),
20503 depth,
20504 tab_size: 4,
20505 settings: IndentGuideSettings {
20506 enabled: true,
20507 line_width: 1,
20508 active_line_width: 1,
20509 coloring: IndentGuideColoring::default(),
20510 background_coloring: IndentGuideBackgroundColoring::default(),
20511 },
20512 }
20513}
20514
20515#[gpui::test]
20516async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20517 let (buffer_id, mut cx) = setup_indent_guides_editor(
20518 &"
20519 fn main() {
20520 let a = 1;
20521 }"
20522 .unindent(),
20523 cx,
20524 )
20525 .await;
20526
20527 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20528}
20529
20530#[gpui::test]
20531async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20532 let (buffer_id, mut cx) = setup_indent_guides_editor(
20533 &"
20534 fn main() {
20535 let a = 1;
20536 let b = 2;
20537 }"
20538 .unindent(),
20539 cx,
20540 )
20541 .await;
20542
20543 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20544}
20545
20546#[gpui::test]
20547async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20548 let (buffer_id, mut cx) = setup_indent_guides_editor(
20549 &"
20550 fn main() {
20551 let a = 1;
20552 if a == 3 {
20553 let b = 2;
20554 } else {
20555 let c = 3;
20556 }
20557 }"
20558 .unindent(),
20559 cx,
20560 )
20561 .await;
20562
20563 assert_indent_guides(
20564 0..8,
20565 vec![
20566 indent_guide(buffer_id, 1, 6, 0),
20567 indent_guide(buffer_id, 3, 3, 1),
20568 indent_guide(buffer_id, 5, 5, 1),
20569 ],
20570 None,
20571 &mut cx,
20572 );
20573}
20574
20575#[gpui::test]
20576async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20577 let (buffer_id, mut cx) = setup_indent_guides_editor(
20578 &"
20579 fn main() {
20580 let a = 1;
20581 let b = 2;
20582 let c = 3;
20583 }"
20584 .unindent(),
20585 cx,
20586 )
20587 .await;
20588
20589 assert_indent_guides(
20590 0..5,
20591 vec![
20592 indent_guide(buffer_id, 1, 3, 0),
20593 indent_guide(buffer_id, 2, 2, 1),
20594 ],
20595 None,
20596 &mut cx,
20597 );
20598}
20599
20600#[gpui::test]
20601async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20602 let (buffer_id, mut cx) = setup_indent_guides_editor(
20603 &"
20604 fn main() {
20605 let a = 1;
20606
20607 let c = 3;
20608 }"
20609 .unindent(),
20610 cx,
20611 )
20612 .await;
20613
20614 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20615}
20616
20617#[gpui::test]
20618async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20619 let (buffer_id, mut cx) = setup_indent_guides_editor(
20620 &"
20621 fn main() {
20622 let a = 1;
20623
20624 let c = 3;
20625
20626 if a == 3 {
20627 let b = 2;
20628 } else {
20629 let c = 3;
20630 }
20631 }"
20632 .unindent(),
20633 cx,
20634 )
20635 .await;
20636
20637 assert_indent_guides(
20638 0..11,
20639 vec![
20640 indent_guide(buffer_id, 1, 9, 0),
20641 indent_guide(buffer_id, 6, 6, 1),
20642 indent_guide(buffer_id, 8, 8, 1),
20643 ],
20644 None,
20645 &mut cx,
20646 );
20647}
20648
20649#[gpui::test]
20650async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20651 let (buffer_id, mut cx) = setup_indent_guides_editor(
20652 &"
20653 fn main() {
20654 let a = 1;
20655
20656 let c = 3;
20657
20658 if a == 3 {
20659 let b = 2;
20660 } else {
20661 let c = 3;
20662 }
20663 }"
20664 .unindent(),
20665 cx,
20666 )
20667 .await;
20668
20669 assert_indent_guides(
20670 1..11,
20671 vec![
20672 indent_guide(buffer_id, 1, 9, 0),
20673 indent_guide(buffer_id, 6, 6, 1),
20674 indent_guide(buffer_id, 8, 8, 1),
20675 ],
20676 None,
20677 &mut cx,
20678 );
20679}
20680
20681#[gpui::test]
20682async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20683 let (buffer_id, mut cx) = setup_indent_guides_editor(
20684 &"
20685 fn main() {
20686 let a = 1;
20687
20688 let c = 3;
20689
20690 if a == 3 {
20691 let b = 2;
20692 } else {
20693 let c = 3;
20694 }
20695 }"
20696 .unindent(),
20697 cx,
20698 )
20699 .await;
20700
20701 assert_indent_guides(
20702 1..10,
20703 vec![
20704 indent_guide(buffer_id, 1, 9, 0),
20705 indent_guide(buffer_id, 6, 6, 1),
20706 indent_guide(buffer_id, 8, 8, 1),
20707 ],
20708 None,
20709 &mut cx,
20710 );
20711}
20712
20713#[gpui::test]
20714async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20715 let (buffer_id, mut cx) = setup_indent_guides_editor(
20716 &"
20717 fn main() {
20718 if a {
20719 b(
20720 c,
20721 d,
20722 )
20723 } else {
20724 e(
20725 f
20726 )
20727 }
20728 }"
20729 .unindent(),
20730 cx,
20731 )
20732 .await;
20733
20734 assert_indent_guides(
20735 0..11,
20736 vec![
20737 indent_guide(buffer_id, 1, 10, 0),
20738 indent_guide(buffer_id, 2, 5, 1),
20739 indent_guide(buffer_id, 7, 9, 1),
20740 indent_guide(buffer_id, 3, 4, 2),
20741 indent_guide(buffer_id, 8, 8, 2),
20742 ],
20743 None,
20744 &mut cx,
20745 );
20746
20747 cx.update_editor(|editor, window, cx| {
20748 editor.fold_at(MultiBufferRow(2), window, cx);
20749 assert_eq!(
20750 editor.display_text(cx),
20751 "
20752 fn main() {
20753 if a {
20754 b(⋯
20755 )
20756 } else {
20757 e(
20758 f
20759 )
20760 }
20761 }"
20762 .unindent()
20763 );
20764 });
20765
20766 assert_indent_guides(
20767 0..11,
20768 vec![
20769 indent_guide(buffer_id, 1, 10, 0),
20770 indent_guide(buffer_id, 2, 5, 1),
20771 indent_guide(buffer_id, 7, 9, 1),
20772 indent_guide(buffer_id, 8, 8, 2),
20773 ],
20774 None,
20775 &mut cx,
20776 );
20777}
20778
20779#[gpui::test]
20780async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20781 let (buffer_id, mut cx) = setup_indent_guides_editor(
20782 &"
20783 block1
20784 block2
20785 block3
20786 block4
20787 block2
20788 block1
20789 block1"
20790 .unindent(),
20791 cx,
20792 )
20793 .await;
20794
20795 assert_indent_guides(
20796 1..10,
20797 vec![
20798 indent_guide(buffer_id, 1, 4, 0),
20799 indent_guide(buffer_id, 2, 3, 1),
20800 indent_guide(buffer_id, 3, 3, 2),
20801 ],
20802 None,
20803 &mut cx,
20804 );
20805}
20806
20807#[gpui::test]
20808async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20809 let (buffer_id, mut cx) = setup_indent_guides_editor(
20810 &"
20811 block1
20812 block2
20813 block3
20814
20815 block1
20816 block1"
20817 .unindent(),
20818 cx,
20819 )
20820 .await;
20821
20822 assert_indent_guides(
20823 0..6,
20824 vec![
20825 indent_guide(buffer_id, 1, 2, 0),
20826 indent_guide(buffer_id, 2, 2, 1),
20827 ],
20828 None,
20829 &mut cx,
20830 );
20831}
20832
20833#[gpui::test]
20834async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20835 let (buffer_id, mut cx) = setup_indent_guides_editor(
20836 &"
20837 function component() {
20838 \treturn (
20839 \t\t\t
20840 \t\t<div>
20841 \t\t\t<abc></abc>
20842 \t\t</div>
20843 \t)
20844 }"
20845 .unindent(),
20846 cx,
20847 )
20848 .await;
20849
20850 assert_indent_guides(
20851 0..8,
20852 vec![
20853 indent_guide(buffer_id, 1, 6, 0),
20854 indent_guide(buffer_id, 2, 5, 1),
20855 indent_guide(buffer_id, 4, 4, 2),
20856 ],
20857 None,
20858 &mut cx,
20859 );
20860}
20861
20862#[gpui::test]
20863async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20864 let (buffer_id, mut cx) = setup_indent_guides_editor(
20865 &"
20866 function component() {
20867 \treturn (
20868 \t
20869 \t\t<div>
20870 \t\t\t<abc></abc>
20871 \t\t</div>
20872 \t)
20873 }"
20874 .unindent(),
20875 cx,
20876 )
20877 .await;
20878
20879 assert_indent_guides(
20880 0..8,
20881 vec![
20882 indent_guide(buffer_id, 1, 6, 0),
20883 indent_guide(buffer_id, 2, 5, 1),
20884 indent_guide(buffer_id, 4, 4, 2),
20885 ],
20886 None,
20887 &mut cx,
20888 );
20889}
20890
20891#[gpui::test]
20892async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20893 let (buffer_id, mut cx) = setup_indent_guides_editor(
20894 &"
20895 block1
20896
20897
20898
20899 block2
20900 "
20901 .unindent(),
20902 cx,
20903 )
20904 .await;
20905
20906 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20907}
20908
20909#[gpui::test]
20910async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20911 let (buffer_id, mut cx) = setup_indent_guides_editor(
20912 &"
20913 def a:
20914 \tb = 3
20915 \tif True:
20916 \t\tc = 4
20917 \t\td = 5
20918 \tprint(b)
20919 "
20920 .unindent(),
20921 cx,
20922 )
20923 .await;
20924
20925 assert_indent_guides(
20926 0..6,
20927 vec![
20928 indent_guide(buffer_id, 1, 5, 0),
20929 indent_guide(buffer_id, 3, 4, 1),
20930 ],
20931 None,
20932 &mut cx,
20933 );
20934}
20935
20936#[gpui::test]
20937async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20938 let (buffer_id, mut cx) = setup_indent_guides_editor(
20939 &"
20940 fn main() {
20941 let a = 1;
20942 }"
20943 .unindent(),
20944 cx,
20945 )
20946 .await;
20947
20948 cx.update_editor(|editor, window, cx| {
20949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20950 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20951 });
20952 });
20953
20954 assert_indent_guides(
20955 0..3,
20956 vec![indent_guide(buffer_id, 1, 1, 0)],
20957 Some(vec![0]),
20958 &mut cx,
20959 );
20960}
20961
20962#[gpui::test]
20963async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20964 let (buffer_id, mut cx) = setup_indent_guides_editor(
20965 &"
20966 fn main() {
20967 if 1 == 2 {
20968 let a = 1;
20969 }
20970 }"
20971 .unindent(),
20972 cx,
20973 )
20974 .await;
20975
20976 cx.update_editor(|editor, window, cx| {
20977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20978 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20979 });
20980 });
20981
20982 assert_indent_guides(
20983 0..4,
20984 vec![
20985 indent_guide(buffer_id, 1, 3, 0),
20986 indent_guide(buffer_id, 2, 2, 1),
20987 ],
20988 Some(vec![1]),
20989 &mut cx,
20990 );
20991
20992 cx.update_editor(|editor, window, cx| {
20993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20994 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20995 });
20996 });
20997
20998 assert_indent_guides(
20999 0..4,
21000 vec![
21001 indent_guide(buffer_id, 1, 3, 0),
21002 indent_guide(buffer_id, 2, 2, 1),
21003 ],
21004 Some(vec![1]),
21005 &mut cx,
21006 );
21007
21008 cx.update_editor(|editor, window, cx| {
21009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21010 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21011 });
21012 });
21013
21014 assert_indent_guides(
21015 0..4,
21016 vec![
21017 indent_guide(buffer_id, 1, 3, 0),
21018 indent_guide(buffer_id, 2, 2, 1),
21019 ],
21020 Some(vec![0]),
21021 &mut cx,
21022 );
21023}
21024
21025#[gpui::test]
21026async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21027 let (buffer_id, mut cx) = setup_indent_guides_editor(
21028 &"
21029 fn main() {
21030 let a = 1;
21031
21032 let b = 2;
21033 }"
21034 .unindent(),
21035 cx,
21036 )
21037 .await;
21038
21039 cx.update_editor(|editor, window, cx| {
21040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21041 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21042 });
21043 });
21044
21045 assert_indent_guides(
21046 0..5,
21047 vec![indent_guide(buffer_id, 1, 3, 0)],
21048 Some(vec![0]),
21049 &mut cx,
21050 );
21051}
21052
21053#[gpui::test]
21054async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21055 let (buffer_id, mut cx) = setup_indent_guides_editor(
21056 &"
21057 def m:
21058 a = 1
21059 pass"
21060 .unindent(),
21061 cx,
21062 )
21063 .await;
21064
21065 cx.update_editor(|editor, window, cx| {
21066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21067 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21068 });
21069 });
21070
21071 assert_indent_guides(
21072 0..3,
21073 vec![indent_guide(buffer_id, 1, 2, 0)],
21074 Some(vec![0]),
21075 &mut cx,
21076 );
21077}
21078
21079#[gpui::test]
21080async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21081 init_test(cx, |_| {});
21082 let mut cx = EditorTestContext::new(cx).await;
21083 let text = indoc! {
21084 "
21085 impl A {
21086 fn b() {
21087 0;
21088 3;
21089 5;
21090 6;
21091 7;
21092 }
21093 }
21094 "
21095 };
21096 let base_text = indoc! {
21097 "
21098 impl A {
21099 fn b() {
21100 0;
21101 1;
21102 2;
21103 3;
21104 4;
21105 }
21106 fn c() {
21107 5;
21108 6;
21109 7;
21110 }
21111 }
21112 "
21113 };
21114
21115 cx.update_editor(|editor, window, cx| {
21116 editor.set_text(text, window, cx);
21117
21118 editor.buffer().update(cx, |multibuffer, cx| {
21119 let buffer = multibuffer.as_singleton().unwrap();
21120 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21121
21122 multibuffer.set_all_diff_hunks_expanded(cx);
21123 multibuffer.add_diff(diff, cx);
21124
21125 buffer.read(cx).remote_id()
21126 })
21127 });
21128 cx.run_until_parked();
21129
21130 cx.assert_state_with_diff(
21131 indoc! { "
21132 impl A {
21133 fn b() {
21134 0;
21135 - 1;
21136 - 2;
21137 3;
21138 - 4;
21139 - }
21140 - fn c() {
21141 5;
21142 6;
21143 7;
21144 }
21145 }
21146 ˇ"
21147 }
21148 .to_string(),
21149 );
21150
21151 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21152 editor
21153 .snapshot(window, cx)
21154 .buffer_snapshot()
21155 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21156 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21157 .collect::<Vec<_>>()
21158 });
21159 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21160 assert_eq!(
21161 actual_guides,
21162 vec![
21163 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21164 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21165 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21166 ]
21167 );
21168}
21169
21170#[gpui::test]
21171async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21172 init_test(cx, |_| {});
21173 let mut cx = EditorTestContext::new(cx).await;
21174
21175 let diff_base = r#"
21176 a
21177 b
21178 c
21179 "#
21180 .unindent();
21181
21182 cx.set_state(
21183 &r#"
21184 ˇA
21185 b
21186 C
21187 "#
21188 .unindent(),
21189 );
21190 cx.set_head_text(&diff_base);
21191 cx.update_editor(|editor, window, cx| {
21192 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21193 });
21194 executor.run_until_parked();
21195
21196 let both_hunks_expanded = r#"
21197 - a
21198 + ˇA
21199 b
21200 - c
21201 + C
21202 "#
21203 .unindent();
21204
21205 cx.assert_state_with_diff(both_hunks_expanded.clone());
21206
21207 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21208 let snapshot = editor.snapshot(window, cx);
21209 let hunks = editor
21210 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21211 .collect::<Vec<_>>();
21212 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21213 let buffer_id = hunks[0].buffer_id;
21214 hunks
21215 .into_iter()
21216 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21217 .collect::<Vec<_>>()
21218 });
21219 assert_eq!(hunk_ranges.len(), 2);
21220
21221 cx.update_editor(|editor, _, cx| {
21222 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21223 });
21224 executor.run_until_parked();
21225
21226 let second_hunk_expanded = r#"
21227 ˇA
21228 b
21229 - c
21230 + C
21231 "#
21232 .unindent();
21233
21234 cx.assert_state_with_diff(second_hunk_expanded);
21235
21236 cx.update_editor(|editor, _, cx| {
21237 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21238 });
21239 executor.run_until_parked();
21240
21241 cx.assert_state_with_diff(both_hunks_expanded.clone());
21242
21243 cx.update_editor(|editor, _, cx| {
21244 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21245 });
21246 executor.run_until_parked();
21247
21248 let first_hunk_expanded = r#"
21249 - a
21250 + ˇA
21251 b
21252 C
21253 "#
21254 .unindent();
21255
21256 cx.assert_state_with_diff(first_hunk_expanded);
21257
21258 cx.update_editor(|editor, _, cx| {
21259 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21260 });
21261 executor.run_until_parked();
21262
21263 cx.assert_state_with_diff(both_hunks_expanded);
21264
21265 cx.set_state(
21266 &r#"
21267 ˇA
21268 b
21269 "#
21270 .unindent(),
21271 );
21272 cx.run_until_parked();
21273
21274 // TODO this cursor position seems bad
21275 cx.assert_state_with_diff(
21276 r#"
21277 - ˇa
21278 + A
21279 b
21280 "#
21281 .unindent(),
21282 );
21283
21284 cx.update_editor(|editor, window, cx| {
21285 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21286 });
21287
21288 cx.assert_state_with_diff(
21289 r#"
21290 - ˇa
21291 + A
21292 b
21293 - c
21294 "#
21295 .unindent(),
21296 );
21297
21298 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21299 let snapshot = editor.snapshot(window, cx);
21300 let hunks = editor
21301 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21302 .collect::<Vec<_>>();
21303 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21304 let buffer_id = hunks[0].buffer_id;
21305 hunks
21306 .into_iter()
21307 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21308 .collect::<Vec<_>>()
21309 });
21310 assert_eq!(hunk_ranges.len(), 2);
21311
21312 cx.update_editor(|editor, _, cx| {
21313 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21314 });
21315 executor.run_until_parked();
21316
21317 cx.assert_state_with_diff(
21318 r#"
21319 - ˇa
21320 + A
21321 b
21322 "#
21323 .unindent(),
21324 );
21325}
21326
21327#[gpui::test]
21328async fn test_toggle_deletion_hunk_at_start_of_file(
21329 executor: BackgroundExecutor,
21330 cx: &mut TestAppContext,
21331) {
21332 init_test(cx, |_| {});
21333 let mut cx = EditorTestContext::new(cx).await;
21334
21335 let diff_base = r#"
21336 a
21337 b
21338 c
21339 "#
21340 .unindent();
21341
21342 cx.set_state(
21343 &r#"
21344 ˇb
21345 c
21346 "#
21347 .unindent(),
21348 );
21349 cx.set_head_text(&diff_base);
21350 cx.update_editor(|editor, window, cx| {
21351 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21352 });
21353 executor.run_until_parked();
21354
21355 let hunk_expanded = r#"
21356 - a
21357 ˇb
21358 c
21359 "#
21360 .unindent();
21361
21362 cx.assert_state_with_diff(hunk_expanded.clone());
21363
21364 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21365 let snapshot = editor.snapshot(window, cx);
21366 let hunks = editor
21367 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21368 .collect::<Vec<_>>();
21369 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21370 let buffer_id = hunks[0].buffer_id;
21371 hunks
21372 .into_iter()
21373 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21374 .collect::<Vec<_>>()
21375 });
21376 assert_eq!(hunk_ranges.len(), 1);
21377
21378 cx.update_editor(|editor, _, cx| {
21379 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21380 });
21381 executor.run_until_parked();
21382
21383 let hunk_collapsed = r#"
21384 ˇb
21385 c
21386 "#
21387 .unindent();
21388
21389 cx.assert_state_with_diff(hunk_collapsed);
21390
21391 cx.update_editor(|editor, _, cx| {
21392 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21393 });
21394 executor.run_until_parked();
21395
21396 cx.assert_state_with_diff(hunk_expanded);
21397}
21398
21399#[gpui::test]
21400async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21401 init_test(cx, |_| {});
21402
21403 let fs = FakeFs::new(cx.executor());
21404 fs.insert_tree(
21405 path!("/test"),
21406 json!({
21407 ".git": {},
21408 "file-1": "ONE\n",
21409 "file-2": "TWO\n",
21410 "file-3": "THREE\n",
21411 }),
21412 )
21413 .await;
21414
21415 fs.set_head_for_repo(
21416 path!("/test/.git").as_ref(),
21417 &[
21418 ("file-1", "one\n".into()),
21419 ("file-2", "two\n".into()),
21420 ("file-3", "three\n".into()),
21421 ],
21422 "deadbeef",
21423 );
21424
21425 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21426 let mut buffers = vec![];
21427 for i in 1..=3 {
21428 let buffer = project
21429 .update(cx, |project, cx| {
21430 let path = format!(path!("/test/file-{}"), i);
21431 project.open_local_buffer(path, cx)
21432 })
21433 .await
21434 .unwrap();
21435 buffers.push(buffer);
21436 }
21437
21438 let multibuffer = cx.new(|cx| {
21439 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21440 multibuffer.set_all_diff_hunks_expanded(cx);
21441 for buffer in &buffers {
21442 let snapshot = buffer.read(cx).snapshot();
21443 multibuffer.set_excerpts_for_path(
21444 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21445 buffer.clone(),
21446 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21447 2,
21448 cx,
21449 );
21450 }
21451 multibuffer
21452 });
21453
21454 let editor = cx.add_window(|window, cx| {
21455 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21456 });
21457 cx.run_until_parked();
21458
21459 let snapshot = editor
21460 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21461 .unwrap();
21462 let hunks = snapshot
21463 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21464 .map(|hunk| match hunk {
21465 DisplayDiffHunk::Unfolded {
21466 display_row_range, ..
21467 } => display_row_range,
21468 DisplayDiffHunk::Folded { .. } => unreachable!(),
21469 })
21470 .collect::<Vec<_>>();
21471 assert_eq!(
21472 hunks,
21473 [
21474 DisplayRow(2)..DisplayRow(4),
21475 DisplayRow(7)..DisplayRow(9),
21476 DisplayRow(12)..DisplayRow(14),
21477 ]
21478 );
21479}
21480
21481#[gpui::test]
21482async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21483 init_test(cx, |_| {});
21484
21485 let mut cx = EditorTestContext::new(cx).await;
21486 cx.set_head_text(indoc! { "
21487 one
21488 two
21489 three
21490 four
21491 five
21492 "
21493 });
21494 cx.set_index_text(indoc! { "
21495 one
21496 two
21497 three
21498 four
21499 five
21500 "
21501 });
21502 cx.set_state(indoc! {"
21503 one
21504 TWO
21505 ˇTHREE
21506 FOUR
21507 five
21508 "});
21509 cx.run_until_parked();
21510 cx.update_editor(|editor, window, cx| {
21511 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21512 });
21513 cx.run_until_parked();
21514 cx.assert_index_text(Some(indoc! {"
21515 one
21516 TWO
21517 THREE
21518 FOUR
21519 five
21520 "}));
21521 cx.set_state(indoc! { "
21522 one
21523 TWO
21524 ˇTHREE-HUNDRED
21525 FOUR
21526 five
21527 "});
21528 cx.run_until_parked();
21529 cx.update_editor(|editor, window, cx| {
21530 let snapshot = editor.snapshot(window, cx);
21531 let hunks = editor
21532 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21533 .collect::<Vec<_>>();
21534 assert_eq!(hunks.len(), 1);
21535 assert_eq!(
21536 hunks[0].status(),
21537 DiffHunkStatus {
21538 kind: DiffHunkStatusKind::Modified,
21539 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21540 }
21541 );
21542
21543 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21544 });
21545 cx.run_until_parked();
21546 cx.assert_index_text(Some(indoc! {"
21547 one
21548 TWO
21549 THREE-HUNDRED
21550 FOUR
21551 five
21552 "}));
21553}
21554
21555#[gpui::test]
21556fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21557 init_test(cx, |_| {});
21558
21559 let editor = cx.add_window(|window, cx| {
21560 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21561 build_editor(buffer, window, cx)
21562 });
21563
21564 let render_args = Arc::new(Mutex::new(None));
21565 let snapshot = editor
21566 .update(cx, |editor, window, cx| {
21567 let snapshot = editor.buffer().read(cx).snapshot(cx);
21568 let range =
21569 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21570
21571 struct RenderArgs {
21572 row: MultiBufferRow,
21573 folded: bool,
21574 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21575 }
21576
21577 let crease = Crease::inline(
21578 range,
21579 FoldPlaceholder::test(),
21580 {
21581 let toggle_callback = render_args.clone();
21582 move |row, folded, callback, _window, _cx| {
21583 *toggle_callback.lock() = Some(RenderArgs {
21584 row,
21585 folded,
21586 callback,
21587 });
21588 div()
21589 }
21590 },
21591 |_row, _folded, _window, _cx| div(),
21592 );
21593
21594 editor.insert_creases(Some(crease), cx);
21595 let snapshot = editor.snapshot(window, cx);
21596 let _div =
21597 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21598 snapshot
21599 })
21600 .unwrap();
21601
21602 let render_args = render_args.lock().take().unwrap();
21603 assert_eq!(render_args.row, MultiBufferRow(1));
21604 assert!(!render_args.folded);
21605 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21606
21607 cx.update_window(*editor, |_, window, cx| {
21608 (render_args.callback)(true, window, cx)
21609 })
21610 .unwrap();
21611 let snapshot = editor
21612 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21613 .unwrap();
21614 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21615
21616 cx.update_window(*editor, |_, window, cx| {
21617 (render_args.callback)(false, window, cx)
21618 })
21619 .unwrap();
21620 let snapshot = editor
21621 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21622 .unwrap();
21623 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21624}
21625
21626#[gpui::test]
21627async fn test_input_text(cx: &mut TestAppContext) {
21628 init_test(cx, |_| {});
21629 let mut cx = EditorTestContext::new(cx).await;
21630
21631 cx.set_state(
21632 &r#"ˇone
21633 two
21634
21635 three
21636 fourˇ
21637 five
21638
21639 siˇx"#
21640 .unindent(),
21641 );
21642
21643 cx.dispatch_action(HandleInput(String::new()));
21644 cx.assert_editor_state(
21645 &r#"ˇone
21646 two
21647
21648 three
21649 fourˇ
21650 five
21651
21652 siˇx"#
21653 .unindent(),
21654 );
21655
21656 cx.dispatch_action(HandleInput("AAAA".to_string()));
21657 cx.assert_editor_state(
21658 &r#"AAAAˇone
21659 two
21660
21661 three
21662 fourAAAAˇ
21663 five
21664
21665 siAAAAˇx"#
21666 .unindent(),
21667 );
21668}
21669
21670#[gpui::test]
21671async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21672 init_test(cx, |_| {});
21673
21674 let mut cx = EditorTestContext::new(cx).await;
21675 cx.set_state(
21676 r#"let foo = 1;
21677let foo = 2;
21678let foo = 3;
21679let fooˇ = 4;
21680let foo = 5;
21681let foo = 6;
21682let foo = 7;
21683let foo = 8;
21684let foo = 9;
21685let foo = 10;
21686let foo = 11;
21687let foo = 12;
21688let foo = 13;
21689let foo = 14;
21690let foo = 15;"#,
21691 );
21692
21693 cx.update_editor(|e, window, cx| {
21694 assert_eq!(
21695 e.next_scroll_position,
21696 NextScrollCursorCenterTopBottom::Center,
21697 "Default next scroll direction is center",
21698 );
21699
21700 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21701 assert_eq!(
21702 e.next_scroll_position,
21703 NextScrollCursorCenterTopBottom::Top,
21704 "After center, next scroll direction should be top",
21705 );
21706
21707 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21708 assert_eq!(
21709 e.next_scroll_position,
21710 NextScrollCursorCenterTopBottom::Bottom,
21711 "After top, next scroll direction should be bottom",
21712 );
21713
21714 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21715 assert_eq!(
21716 e.next_scroll_position,
21717 NextScrollCursorCenterTopBottom::Center,
21718 "After bottom, scrolling should start over",
21719 );
21720
21721 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21722 assert_eq!(
21723 e.next_scroll_position,
21724 NextScrollCursorCenterTopBottom::Top,
21725 "Scrolling continues if retriggered fast enough"
21726 );
21727 });
21728
21729 cx.executor()
21730 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21731 cx.executor().run_until_parked();
21732 cx.update_editor(|e, _, _| {
21733 assert_eq!(
21734 e.next_scroll_position,
21735 NextScrollCursorCenterTopBottom::Center,
21736 "If scrolling is not triggered fast enough, it should reset"
21737 );
21738 });
21739}
21740
21741#[gpui::test]
21742async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21743 init_test(cx, |_| {});
21744 let mut cx = EditorLspTestContext::new_rust(
21745 lsp::ServerCapabilities {
21746 definition_provider: Some(lsp::OneOf::Left(true)),
21747 references_provider: Some(lsp::OneOf::Left(true)),
21748 ..lsp::ServerCapabilities::default()
21749 },
21750 cx,
21751 )
21752 .await;
21753
21754 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21755 let go_to_definition = cx
21756 .lsp
21757 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21758 move |params, _| async move {
21759 if empty_go_to_definition {
21760 Ok(None)
21761 } else {
21762 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21763 uri: params.text_document_position_params.text_document.uri,
21764 range: lsp::Range::new(
21765 lsp::Position::new(4, 3),
21766 lsp::Position::new(4, 6),
21767 ),
21768 })))
21769 }
21770 },
21771 );
21772 let references = cx
21773 .lsp
21774 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21775 Ok(Some(vec![lsp::Location {
21776 uri: params.text_document_position.text_document.uri,
21777 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21778 }]))
21779 });
21780 (go_to_definition, references)
21781 };
21782
21783 cx.set_state(
21784 &r#"fn one() {
21785 let mut a = ˇtwo();
21786 }
21787
21788 fn two() {}"#
21789 .unindent(),
21790 );
21791 set_up_lsp_handlers(false, &mut cx);
21792 let navigated = cx
21793 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21794 .await
21795 .expect("Failed to navigate to definition");
21796 assert_eq!(
21797 navigated,
21798 Navigated::Yes,
21799 "Should have navigated to definition from the GetDefinition response"
21800 );
21801 cx.assert_editor_state(
21802 &r#"fn one() {
21803 let mut a = two();
21804 }
21805
21806 fn «twoˇ»() {}"#
21807 .unindent(),
21808 );
21809
21810 let editors = cx.update_workspace(|workspace, _, cx| {
21811 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21812 });
21813 cx.update_editor(|_, _, test_editor_cx| {
21814 assert_eq!(
21815 editors.len(),
21816 1,
21817 "Initially, only one, test, editor should be open in the workspace"
21818 );
21819 assert_eq!(
21820 test_editor_cx.entity(),
21821 editors.last().expect("Asserted len is 1").clone()
21822 );
21823 });
21824
21825 set_up_lsp_handlers(true, &mut cx);
21826 let navigated = cx
21827 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21828 .await
21829 .expect("Failed to navigate to lookup references");
21830 assert_eq!(
21831 navigated,
21832 Navigated::Yes,
21833 "Should have navigated to references as a fallback after empty GoToDefinition response"
21834 );
21835 // We should not change the selections in the existing file,
21836 // if opening another milti buffer with the references
21837 cx.assert_editor_state(
21838 &r#"fn one() {
21839 let mut a = two();
21840 }
21841
21842 fn «twoˇ»() {}"#
21843 .unindent(),
21844 );
21845 let editors = cx.update_workspace(|workspace, _, cx| {
21846 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21847 });
21848 cx.update_editor(|_, _, test_editor_cx| {
21849 assert_eq!(
21850 editors.len(),
21851 2,
21852 "After falling back to references search, we open a new editor with the results"
21853 );
21854 let references_fallback_text = editors
21855 .into_iter()
21856 .find(|new_editor| *new_editor != test_editor_cx.entity())
21857 .expect("Should have one non-test editor now")
21858 .read(test_editor_cx)
21859 .text(test_editor_cx);
21860 assert_eq!(
21861 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21862 "Should use the range from the references response and not the GoToDefinition one"
21863 );
21864 });
21865}
21866
21867#[gpui::test]
21868async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21869 init_test(cx, |_| {});
21870 cx.update(|cx| {
21871 let mut editor_settings = EditorSettings::get_global(cx).clone();
21872 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21873 EditorSettings::override_global(editor_settings, cx);
21874 });
21875 let mut cx = EditorLspTestContext::new_rust(
21876 lsp::ServerCapabilities {
21877 definition_provider: Some(lsp::OneOf::Left(true)),
21878 references_provider: Some(lsp::OneOf::Left(true)),
21879 ..lsp::ServerCapabilities::default()
21880 },
21881 cx,
21882 )
21883 .await;
21884 let original_state = r#"fn one() {
21885 let mut a = ˇtwo();
21886 }
21887
21888 fn two() {}"#
21889 .unindent();
21890 cx.set_state(&original_state);
21891
21892 let mut go_to_definition = cx
21893 .lsp
21894 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21895 move |_, _| async move { Ok(None) },
21896 );
21897 let _references = cx
21898 .lsp
21899 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21900 panic!("Should not call for references with no go to definition fallback")
21901 });
21902
21903 let navigated = cx
21904 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21905 .await
21906 .expect("Failed to navigate to lookup references");
21907 go_to_definition
21908 .next()
21909 .await
21910 .expect("Should have called the go_to_definition handler");
21911
21912 assert_eq!(
21913 navigated,
21914 Navigated::No,
21915 "Should have navigated to references as a fallback after empty GoToDefinition response"
21916 );
21917 cx.assert_editor_state(&original_state);
21918 let editors = cx.update_workspace(|workspace, _, cx| {
21919 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21920 });
21921 cx.update_editor(|_, _, _| {
21922 assert_eq!(
21923 editors.len(),
21924 1,
21925 "After unsuccessful fallback, no other editor should have been opened"
21926 );
21927 });
21928}
21929
21930#[gpui::test]
21931async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21932 init_test(cx, |_| {});
21933 let mut cx = EditorLspTestContext::new_rust(
21934 lsp::ServerCapabilities {
21935 references_provider: Some(lsp::OneOf::Left(true)),
21936 ..lsp::ServerCapabilities::default()
21937 },
21938 cx,
21939 )
21940 .await;
21941
21942 cx.set_state(
21943 &r#"
21944 fn one() {
21945 let mut a = two();
21946 }
21947
21948 fn ˇtwo() {}"#
21949 .unindent(),
21950 );
21951 cx.lsp
21952 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21953 Ok(Some(vec![
21954 lsp::Location {
21955 uri: params.text_document_position.text_document.uri.clone(),
21956 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21957 },
21958 lsp::Location {
21959 uri: params.text_document_position.text_document.uri,
21960 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21961 },
21962 ]))
21963 });
21964 let navigated = cx
21965 .update_editor(|editor, window, cx| {
21966 editor.find_all_references(&FindAllReferences, window, cx)
21967 })
21968 .unwrap()
21969 .await
21970 .expect("Failed to navigate to references");
21971 assert_eq!(
21972 navigated,
21973 Navigated::Yes,
21974 "Should have navigated to references from the FindAllReferences response"
21975 );
21976 cx.assert_editor_state(
21977 &r#"fn one() {
21978 let mut a = two();
21979 }
21980
21981 fn ˇtwo() {}"#
21982 .unindent(),
21983 );
21984
21985 let editors = cx.update_workspace(|workspace, _, cx| {
21986 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21987 });
21988 cx.update_editor(|_, _, _| {
21989 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21990 });
21991
21992 cx.set_state(
21993 &r#"fn one() {
21994 let mut a = ˇtwo();
21995 }
21996
21997 fn two() {}"#
21998 .unindent(),
21999 );
22000 let navigated = cx
22001 .update_editor(|editor, window, cx| {
22002 editor.find_all_references(&FindAllReferences, window, cx)
22003 })
22004 .unwrap()
22005 .await
22006 .expect("Failed to navigate to references");
22007 assert_eq!(
22008 navigated,
22009 Navigated::Yes,
22010 "Should have navigated to references from the FindAllReferences response"
22011 );
22012 cx.assert_editor_state(
22013 &r#"fn one() {
22014 let mut a = ˇtwo();
22015 }
22016
22017 fn two() {}"#
22018 .unindent(),
22019 );
22020 let editors = cx.update_workspace(|workspace, _, cx| {
22021 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22022 });
22023 cx.update_editor(|_, _, _| {
22024 assert_eq!(
22025 editors.len(),
22026 2,
22027 "should have re-used the previous multibuffer"
22028 );
22029 });
22030
22031 cx.set_state(
22032 &r#"fn one() {
22033 let mut a = ˇtwo();
22034 }
22035 fn three() {}
22036 fn two() {}"#
22037 .unindent(),
22038 );
22039 cx.lsp
22040 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22041 Ok(Some(vec![
22042 lsp::Location {
22043 uri: params.text_document_position.text_document.uri.clone(),
22044 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22045 },
22046 lsp::Location {
22047 uri: params.text_document_position.text_document.uri,
22048 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22049 },
22050 ]))
22051 });
22052 let navigated = cx
22053 .update_editor(|editor, window, cx| {
22054 editor.find_all_references(&FindAllReferences, window, cx)
22055 })
22056 .unwrap()
22057 .await
22058 .expect("Failed to navigate to references");
22059 assert_eq!(
22060 navigated,
22061 Navigated::Yes,
22062 "Should have navigated to references from the FindAllReferences response"
22063 );
22064 cx.assert_editor_state(
22065 &r#"fn one() {
22066 let mut a = ˇtwo();
22067 }
22068 fn three() {}
22069 fn two() {}"#
22070 .unindent(),
22071 );
22072 let editors = cx.update_workspace(|workspace, _, cx| {
22073 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22074 });
22075 cx.update_editor(|_, _, _| {
22076 assert_eq!(
22077 editors.len(),
22078 3,
22079 "should have used a new multibuffer as offsets changed"
22080 );
22081 });
22082}
22083#[gpui::test]
22084async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22085 init_test(cx, |_| {});
22086
22087 let language = Arc::new(Language::new(
22088 LanguageConfig::default(),
22089 Some(tree_sitter_rust::LANGUAGE.into()),
22090 ));
22091
22092 let text = r#"
22093 #[cfg(test)]
22094 mod tests() {
22095 #[test]
22096 fn runnable_1() {
22097 let a = 1;
22098 }
22099
22100 #[test]
22101 fn runnable_2() {
22102 let a = 1;
22103 let b = 2;
22104 }
22105 }
22106 "#
22107 .unindent();
22108
22109 let fs = FakeFs::new(cx.executor());
22110 fs.insert_file("/file.rs", Default::default()).await;
22111
22112 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22113 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22114 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22115 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22116 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22117
22118 let editor = cx.new_window_entity(|window, cx| {
22119 Editor::new(
22120 EditorMode::full(),
22121 multi_buffer,
22122 Some(project.clone()),
22123 window,
22124 cx,
22125 )
22126 });
22127
22128 editor.update_in(cx, |editor, window, cx| {
22129 let snapshot = editor.buffer().read(cx).snapshot(cx);
22130 editor.tasks.insert(
22131 (buffer.read(cx).remote_id(), 3),
22132 RunnableTasks {
22133 templates: vec![],
22134 offset: snapshot.anchor_before(43),
22135 column: 0,
22136 extra_variables: HashMap::default(),
22137 context_range: BufferOffset(43)..BufferOffset(85),
22138 },
22139 );
22140 editor.tasks.insert(
22141 (buffer.read(cx).remote_id(), 8),
22142 RunnableTasks {
22143 templates: vec![],
22144 offset: snapshot.anchor_before(86),
22145 column: 0,
22146 extra_variables: HashMap::default(),
22147 context_range: BufferOffset(86)..BufferOffset(191),
22148 },
22149 );
22150
22151 // Test finding task when cursor is inside function body
22152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22153 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22154 });
22155 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22156 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22157
22158 // Test finding task when cursor is on function name
22159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22160 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22161 });
22162 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22163 assert_eq!(row, 8, "Should find task when cursor is on function name");
22164 });
22165}
22166
22167#[gpui::test]
22168async fn test_folding_buffers(cx: &mut TestAppContext) {
22169 init_test(cx, |_| {});
22170
22171 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22172 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22173 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22174
22175 let fs = FakeFs::new(cx.executor());
22176 fs.insert_tree(
22177 path!("/a"),
22178 json!({
22179 "first.rs": sample_text_1,
22180 "second.rs": sample_text_2,
22181 "third.rs": sample_text_3,
22182 }),
22183 )
22184 .await;
22185 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22186 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22187 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22188 let worktree = project.update(cx, |project, cx| {
22189 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22190 assert_eq!(worktrees.len(), 1);
22191 worktrees.pop().unwrap()
22192 });
22193 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22194
22195 let buffer_1 = project
22196 .update(cx, |project, cx| {
22197 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22198 })
22199 .await
22200 .unwrap();
22201 let buffer_2 = project
22202 .update(cx, |project, cx| {
22203 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22204 })
22205 .await
22206 .unwrap();
22207 let buffer_3 = project
22208 .update(cx, |project, cx| {
22209 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22210 })
22211 .await
22212 .unwrap();
22213
22214 let multi_buffer = cx.new(|cx| {
22215 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22216 multi_buffer.push_excerpts(
22217 buffer_1.clone(),
22218 [
22219 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22220 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22221 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22222 ],
22223 cx,
22224 );
22225 multi_buffer.push_excerpts(
22226 buffer_2.clone(),
22227 [
22228 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22229 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22230 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22231 ],
22232 cx,
22233 );
22234 multi_buffer.push_excerpts(
22235 buffer_3.clone(),
22236 [
22237 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22238 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22239 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22240 ],
22241 cx,
22242 );
22243 multi_buffer
22244 });
22245 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22246 Editor::new(
22247 EditorMode::full(),
22248 multi_buffer.clone(),
22249 Some(project.clone()),
22250 window,
22251 cx,
22252 )
22253 });
22254
22255 assert_eq!(
22256 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22257 "\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",
22258 );
22259
22260 multi_buffer_editor.update(cx, |editor, cx| {
22261 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22262 });
22263 assert_eq!(
22264 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22265 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22266 "After folding the first buffer, its text should not be displayed"
22267 );
22268
22269 multi_buffer_editor.update(cx, |editor, cx| {
22270 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22271 });
22272 assert_eq!(
22273 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22274 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22275 "After folding the second buffer, its text should not be displayed"
22276 );
22277
22278 multi_buffer_editor.update(cx, |editor, cx| {
22279 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22280 });
22281 assert_eq!(
22282 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22283 "\n\n\n\n\n",
22284 "After folding the third buffer, its text should not be displayed"
22285 );
22286
22287 // Emulate selection inside the fold logic, that should work
22288 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22289 editor
22290 .snapshot(window, cx)
22291 .next_line_boundary(Point::new(0, 4));
22292 });
22293
22294 multi_buffer_editor.update(cx, |editor, cx| {
22295 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22296 });
22297 assert_eq!(
22298 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22299 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22300 "After unfolding the second buffer, its text should be displayed"
22301 );
22302
22303 // Typing inside of buffer 1 causes that buffer to be unfolded.
22304 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22305 assert_eq!(
22306 multi_buffer
22307 .read(cx)
22308 .snapshot(cx)
22309 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22310 .collect::<String>(),
22311 "bbbb"
22312 );
22313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22314 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22315 });
22316 editor.handle_input("B", window, cx);
22317 });
22318
22319 assert_eq!(
22320 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22321 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22322 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22323 );
22324
22325 multi_buffer_editor.update(cx, |editor, cx| {
22326 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22327 });
22328 assert_eq!(
22329 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22330 "\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",
22331 "After unfolding the all buffers, all original text should be displayed"
22332 );
22333}
22334
22335#[gpui::test]
22336async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22337 init_test(cx, |_| {});
22338
22339 let sample_text_1 = "1111\n2222\n3333".to_string();
22340 let sample_text_2 = "4444\n5555\n6666".to_string();
22341 let sample_text_3 = "7777\n8888\n9999".to_string();
22342
22343 let fs = FakeFs::new(cx.executor());
22344 fs.insert_tree(
22345 path!("/a"),
22346 json!({
22347 "first.rs": sample_text_1,
22348 "second.rs": sample_text_2,
22349 "third.rs": sample_text_3,
22350 }),
22351 )
22352 .await;
22353 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22355 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22356 let worktree = project.update(cx, |project, cx| {
22357 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22358 assert_eq!(worktrees.len(), 1);
22359 worktrees.pop().unwrap()
22360 });
22361 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22362
22363 let buffer_1 = project
22364 .update(cx, |project, cx| {
22365 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22366 })
22367 .await
22368 .unwrap();
22369 let buffer_2 = project
22370 .update(cx, |project, cx| {
22371 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22372 })
22373 .await
22374 .unwrap();
22375 let buffer_3 = project
22376 .update(cx, |project, cx| {
22377 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22378 })
22379 .await
22380 .unwrap();
22381
22382 let multi_buffer = cx.new(|cx| {
22383 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22384 multi_buffer.push_excerpts(
22385 buffer_1.clone(),
22386 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22387 cx,
22388 );
22389 multi_buffer.push_excerpts(
22390 buffer_2.clone(),
22391 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22392 cx,
22393 );
22394 multi_buffer.push_excerpts(
22395 buffer_3.clone(),
22396 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22397 cx,
22398 );
22399 multi_buffer
22400 });
22401
22402 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22403 Editor::new(
22404 EditorMode::full(),
22405 multi_buffer,
22406 Some(project.clone()),
22407 window,
22408 cx,
22409 )
22410 });
22411
22412 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22413 assert_eq!(
22414 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22415 full_text,
22416 );
22417
22418 multi_buffer_editor.update(cx, |editor, cx| {
22419 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22420 });
22421 assert_eq!(
22422 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22423 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22424 "After folding the first buffer, its text should not be displayed"
22425 );
22426
22427 multi_buffer_editor.update(cx, |editor, cx| {
22428 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22429 });
22430
22431 assert_eq!(
22432 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22433 "\n\n\n\n\n\n7777\n8888\n9999",
22434 "After folding the second buffer, its text should not be displayed"
22435 );
22436
22437 multi_buffer_editor.update(cx, |editor, cx| {
22438 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22439 });
22440 assert_eq!(
22441 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22442 "\n\n\n\n\n",
22443 "After folding the third buffer, its text should not be displayed"
22444 );
22445
22446 multi_buffer_editor.update(cx, |editor, cx| {
22447 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22448 });
22449 assert_eq!(
22450 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22451 "\n\n\n\n4444\n5555\n6666\n\n",
22452 "After unfolding the second buffer, its text should be displayed"
22453 );
22454
22455 multi_buffer_editor.update(cx, |editor, cx| {
22456 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22457 });
22458 assert_eq!(
22459 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22460 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22461 "After unfolding the first buffer, its text should be displayed"
22462 );
22463
22464 multi_buffer_editor.update(cx, |editor, cx| {
22465 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22466 });
22467 assert_eq!(
22468 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22469 full_text,
22470 "After unfolding all buffers, all original text should be displayed"
22471 );
22472}
22473
22474#[gpui::test]
22475async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22476 init_test(cx, |_| {});
22477
22478 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22479
22480 let fs = FakeFs::new(cx.executor());
22481 fs.insert_tree(
22482 path!("/a"),
22483 json!({
22484 "main.rs": sample_text,
22485 }),
22486 )
22487 .await;
22488 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22489 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22490 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22491 let worktree = project.update(cx, |project, cx| {
22492 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22493 assert_eq!(worktrees.len(), 1);
22494 worktrees.pop().unwrap()
22495 });
22496 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22497
22498 let buffer_1 = project
22499 .update(cx, |project, cx| {
22500 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22501 })
22502 .await
22503 .unwrap();
22504
22505 let multi_buffer = cx.new(|cx| {
22506 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22507 multi_buffer.push_excerpts(
22508 buffer_1.clone(),
22509 [ExcerptRange::new(
22510 Point::new(0, 0)
22511 ..Point::new(
22512 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22513 0,
22514 ),
22515 )],
22516 cx,
22517 );
22518 multi_buffer
22519 });
22520 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22521 Editor::new(
22522 EditorMode::full(),
22523 multi_buffer,
22524 Some(project.clone()),
22525 window,
22526 cx,
22527 )
22528 });
22529
22530 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22531 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22532 enum TestHighlight {}
22533 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22534 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22535 editor.highlight_text::<TestHighlight>(
22536 vec![highlight_range.clone()],
22537 HighlightStyle::color(Hsla::green()),
22538 cx,
22539 );
22540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22541 s.select_ranges(Some(highlight_range))
22542 });
22543 });
22544
22545 let full_text = format!("\n\n{sample_text}");
22546 assert_eq!(
22547 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22548 full_text,
22549 );
22550}
22551
22552#[gpui::test]
22553async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22554 init_test(cx, |_| {});
22555 cx.update(|cx| {
22556 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22557 "keymaps/default-linux.json",
22558 cx,
22559 )
22560 .unwrap();
22561 cx.bind_keys(default_key_bindings);
22562 });
22563
22564 let (editor, cx) = cx.add_window_view(|window, cx| {
22565 let multi_buffer = MultiBuffer::build_multi(
22566 [
22567 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22568 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22569 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22570 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22571 ],
22572 cx,
22573 );
22574 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22575
22576 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22577 // fold all but the second buffer, so that we test navigating between two
22578 // adjacent folded buffers, as well as folded buffers at the start and
22579 // end the multibuffer
22580 editor.fold_buffer(buffer_ids[0], cx);
22581 editor.fold_buffer(buffer_ids[2], cx);
22582 editor.fold_buffer(buffer_ids[3], cx);
22583
22584 editor
22585 });
22586 cx.simulate_resize(size(px(1000.), px(1000.)));
22587
22588 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22589 cx.assert_excerpts_with_selections(indoc! {"
22590 [EXCERPT]
22591 ˇ[FOLDED]
22592 [EXCERPT]
22593 a1
22594 b1
22595 [EXCERPT]
22596 [FOLDED]
22597 [EXCERPT]
22598 [FOLDED]
22599 "
22600 });
22601 cx.simulate_keystroke("down");
22602 cx.assert_excerpts_with_selections(indoc! {"
22603 [EXCERPT]
22604 [FOLDED]
22605 [EXCERPT]
22606 ˇa1
22607 b1
22608 [EXCERPT]
22609 [FOLDED]
22610 [EXCERPT]
22611 [FOLDED]
22612 "
22613 });
22614 cx.simulate_keystroke("down");
22615 cx.assert_excerpts_with_selections(indoc! {"
22616 [EXCERPT]
22617 [FOLDED]
22618 [EXCERPT]
22619 a1
22620 ˇb1
22621 [EXCERPT]
22622 [FOLDED]
22623 [EXCERPT]
22624 [FOLDED]
22625 "
22626 });
22627 cx.simulate_keystroke("down");
22628 cx.assert_excerpts_with_selections(indoc! {"
22629 [EXCERPT]
22630 [FOLDED]
22631 [EXCERPT]
22632 a1
22633 b1
22634 ˇ[EXCERPT]
22635 [FOLDED]
22636 [EXCERPT]
22637 [FOLDED]
22638 "
22639 });
22640 cx.simulate_keystroke("down");
22641 cx.assert_excerpts_with_selections(indoc! {"
22642 [EXCERPT]
22643 [FOLDED]
22644 [EXCERPT]
22645 a1
22646 b1
22647 [EXCERPT]
22648 ˇ[FOLDED]
22649 [EXCERPT]
22650 [FOLDED]
22651 "
22652 });
22653 for _ in 0..5 {
22654 cx.simulate_keystroke("down");
22655 cx.assert_excerpts_with_selections(indoc! {"
22656 [EXCERPT]
22657 [FOLDED]
22658 [EXCERPT]
22659 a1
22660 b1
22661 [EXCERPT]
22662 [FOLDED]
22663 [EXCERPT]
22664 ˇ[FOLDED]
22665 "
22666 });
22667 }
22668
22669 cx.simulate_keystroke("up");
22670 cx.assert_excerpts_with_selections(indoc! {"
22671 [EXCERPT]
22672 [FOLDED]
22673 [EXCERPT]
22674 a1
22675 b1
22676 [EXCERPT]
22677 ˇ[FOLDED]
22678 [EXCERPT]
22679 [FOLDED]
22680 "
22681 });
22682 cx.simulate_keystroke("up");
22683 cx.assert_excerpts_with_selections(indoc! {"
22684 [EXCERPT]
22685 [FOLDED]
22686 [EXCERPT]
22687 a1
22688 b1
22689 ˇ[EXCERPT]
22690 [FOLDED]
22691 [EXCERPT]
22692 [FOLDED]
22693 "
22694 });
22695 cx.simulate_keystroke("up");
22696 cx.assert_excerpts_with_selections(indoc! {"
22697 [EXCERPT]
22698 [FOLDED]
22699 [EXCERPT]
22700 a1
22701 ˇb1
22702 [EXCERPT]
22703 [FOLDED]
22704 [EXCERPT]
22705 [FOLDED]
22706 "
22707 });
22708 cx.simulate_keystroke("up");
22709 cx.assert_excerpts_with_selections(indoc! {"
22710 [EXCERPT]
22711 [FOLDED]
22712 [EXCERPT]
22713 ˇa1
22714 b1
22715 [EXCERPT]
22716 [FOLDED]
22717 [EXCERPT]
22718 [FOLDED]
22719 "
22720 });
22721 for _ in 0..5 {
22722 cx.simulate_keystroke("up");
22723 cx.assert_excerpts_with_selections(indoc! {"
22724 [EXCERPT]
22725 ˇ[FOLDED]
22726 [EXCERPT]
22727 a1
22728 b1
22729 [EXCERPT]
22730 [FOLDED]
22731 [EXCERPT]
22732 [FOLDED]
22733 "
22734 });
22735 }
22736}
22737
22738#[gpui::test]
22739async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22740 init_test(cx, |_| {});
22741
22742 // Simple insertion
22743 assert_highlighted_edits(
22744 "Hello, world!",
22745 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22746 true,
22747 cx,
22748 |highlighted_edits, cx| {
22749 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22750 assert_eq!(highlighted_edits.highlights.len(), 1);
22751 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22752 assert_eq!(
22753 highlighted_edits.highlights[0].1.background_color,
22754 Some(cx.theme().status().created_background)
22755 );
22756 },
22757 )
22758 .await;
22759
22760 // Replacement
22761 assert_highlighted_edits(
22762 "This is a test.",
22763 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22764 false,
22765 cx,
22766 |highlighted_edits, cx| {
22767 assert_eq!(highlighted_edits.text, "That is a test.");
22768 assert_eq!(highlighted_edits.highlights.len(), 1);
22769 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22770 assert_eq!(
22771 highlighted_edits.highlights[0].1.background_color,
22772 Some(cx.theme().status().created_background)
22773 );
22774 },
22775 )
22776 .await;
22777
22778 // Multiple edits
22779 assert_highlighted_edits(
22780 "Hello, world!",
22781 vec![
22782 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22783 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22784 ],
22785 false,
22786 cx,
22787 |highlighted_edits, cx| {
22788 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22789 assert_eq!(highlighted_edits.highlights.len(), 2);
22790 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22791 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22792 assert_eq!(
22793 highlighted_edits.highlights[0].1.background_color,
22794 Some(cx.theme().status().created_background)
22795 );
22796 assert_eq!(
22797 highlighted_edits.highlights[1].1.background_color,
22798 Some(cx.theme().status().created_background)
22799 );
22800 },
22801 )
22802 .await;
22803
22804 // Multiple lines with edits
22805 assert_highlighted_edits(
22806 "First line\nSecond line\nThird line\nFourth line",
22807 vec![
22808 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22809 (
22810 Point::new(2, 0)..Point::new(2, 10),
22811 "New third line".to_string(),
22812 ),
22813 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22814 ],
22815 false,
22816 cx,
22817 |highlighted_edits, cx| {
22818 assert_eq!(
22819 highlighted_edits.text,
22820 "Second modified\nNew third line\nFourth updated line"
22821 );
22822 assert_eq!(highlighted_edits.highlights.len(), 3);
22823 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22824 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22825 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22826 for highlight in &highlighted_edits.highlights {
22827 assert_eq!(
22828 highlight.1.background_color,
22829 Some(cx.theme().status().created_background)
22830 );
22831 }
22832 },
22833 )
22834 .await;
22835}
22836
22837#[gpui::test]
22838async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22839 init_test(cx, |_| {});
22840
22841 // Deletion
22842 assert_highlighted_edits(
22843 "Hello, world!",
22844 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22845 true,
22846 cx,
22847 |highlighted_edits, cx| {
22848 assert_eq!(highlighted_edits.text, "Hello, world!");
22849 assert_eq!(highlighted_edits.highlights.len(), 1);
22850 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22851 assert_eq!(
22852 highlighted_edits.highlights[0].1.background_color,
22853 Some(cx.theme().status().deleted_background)
22854 );
22855 },
22856 )
22857 .await;
22858
22859 // Insertion
22860 assert_highlighted_edits(
22861 "Hello, world!",
22862 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22863 true,
22864 cx,
22865 |highlighted_edits, cx| {
22866 assert_eq!(highlighted_edits.highlights.len(), 1);
22867 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22868 assert_eq!(
22869 highlighted_edits.highlights[0].1.background_color,
22870 Some(cx.theme().status().created_background)
22871 );
22872 },
22873 )
22874 .await;
22875}
22876
22877async fn assert_highlighted_edits(
22878 text: &str,
22879 edits: Vec<(Range<Point>, String)>,
22880 include_deletions: bool,
22881 cx: &mut TestAppContext,
22882 assertion_fn: impl Fn(HighlightedText, &App),
22883) {
22884 let window = cx.add_window(|window, cx| {
22885 let buffer = MultiBuffer::build_simple(text, cx);
22886 Editor::new(EditorMode::full(), buffer, None, window, cx)
22887 });
22888 let cx = &mut VisualTestContext::from_window(*window, cx);
22889
22890 let (buffer, snapshot) = window
22891 .update(cx, |editor, _window, cx| {
22892 (
22893 editor.buffer().clone(),
22894 editor.buffer().read(cx).snapshot(cx),
22895 )
22896 })
22897 .unwrap();
22898
22899 let edits = edits
22900 .into_iter()
22901 .map(|(range, edit)| {
22902 (
22903 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22904 edit,
22905 )
22906 })
22907 .collect::<Vec<_>>();
22908
22909 let text_anchor_edits = edits
22910 .clone()
22911 .into_iter()
22912 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22913 .collect::<Vec<_>>();
22914
22915 let edit_preview = window
22916 .update(cx, |_, _window, cx| {
22917 buffer
22918 .read(cx)
22919 .as_singleton()
22920 .unwrap()
22921 .read(cx)
22922 .preview_edits(text_anchor_edits.into(), cx)
22923 })
22924 .unwrap()
22925 .await;
22926
22927 cx.update(|_window, cx| {
22928 let highlighted_edits = edit_prediction_edit_text(
22929 snapshot.as_singleton().unwrap().2,
22930 &edits,
22931 &edit_preview,
22932 include_deletions,
22933 cx,
22934 );
22935 assertion_fn(highlighted_edits, cx)
22936 });
22937}
22938
22939#[track_caller]
22940fn assert_breakpoint(
22941 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22942 path: &Arc<Path>,
22943 expected: Vec<(u32, Breakpoint)>,
22944) {
22945 if expected.is_empty() {
22946 assert!(!breakpoints.contains_key(path), "{}", path.display());
22947 } else {
22948 let mut breakpoint = breakpoints
22949 .get(path)
22950 .unwrap()
22951 .iter()
22952 .map(|breakpoint| {
22953 (
22954 breakpoint.row,
22955 Breakpoint {
22956 message: breakpoint.message.clone(),
22957 state: breakpoint.state,
22958 condition: breakpoint.condition.clone(),
22959 hit_condition: breakpoint.hit_condition.clone(),
22960 },
22961 )
22962 })
22963 .collect::<Vec<_>>();
22964
22965 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22966
22967 assert_eq!(expected, breakpoint);
22968 }
22969}
22970
22971fn add_log_breakpoint_at_cursor(
22972 editor: &mut Editor,
22973 log_message: &str,
22974 window: &mut Window,
22975 cx: &mut Context<Editor>,
22976) {
22977 let (anchor, bp) = editor
22978 .breakpoints_at_cursors(window, cx)
22979 .first()
22980 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22981 .unwrap_or_else(|| {
22982 let snapshot = editor.snapshot(window, cx);
22983 let cursor_position: Point =
22984 editor.selections.newest(&snapshot.display_snapshot).head();
22985
22986 let breakpoint_position = snapshot
22987 .buffer_snapshot()
22988 .anchor_before(Point::new(cursor_position.row, 0));
22989
22990 (breakpoint_position, Breakpoint::new_log(log_message))
22991 });
22992
22993 editor.edit_breakpoint_at_anchor(
22994 anchor,
22995 bp,
22996 BreakpointEditAction::EditLogMessage(log_message.into()),
22997 cx,
22998 );
22999}
23000
23001#[gpui::test]
23002async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23003 init_test(cx, |_| {});
23004
23005 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23006 let fs = FakeFs::new(cx.executor());
23007 fs.insert_tree(
23008 path!("/a"),
23009 json!({
23010 "main.rs": sample_text,
23011 }),
23012 )
23013 .await;
23014 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23015 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23016 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23017
23018 let fs = FakeFs::new(cx.executor());
23019 fs.insert_tree(
23020 path!("/a"),
23021 json!({
23022 "main.rs": sample_text,
23023 }),
23024 )
23025 .await;
23026 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23027 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23028 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23029 let worktree_id = workspace
23030 .update(cx, |workspace, _window, cx| {
23031 workspace.project().update(cx, |project, cx| {
23032 project.worktrees(cx).next().unwrap().read(cx).id()
23033 })
23034 })
23035 .unwrap();
23036
23037 let buffer = project
23038 .update(cx, |project, cx| {
23039 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23040 })
23041 .await
23042 .unwrap();
23043
23044 let (editor, cx) = cx.add_window_view(|window, cx| {
23045 Editor::new(
23046 EditorMode::full(),
23047 MultiBuffer::build_from_buffer(buffer, cx),
23048 Some(project.clone()),
23049 window,
23050 cx,
23051 )
23052 });
23053
23054 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23055 let abs_path = project.read_with(cx, |project, cx| {
23056 project
23057 .absolute_path(&project_path, cx)
23058 .map(Arc::from)
23059 .unwrap()
23060 });
23061
23062 // assert we can add breakpoint on the first line
23063 editor.update_in(cx, |editor, window, cx| {
23064 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23065 editor.move_to_end(&MoveToEnd, window, cx);
23066 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23067 });
23068
23069 let breakpoints = editor.update(cx, |editor, cx| {
23070 editor
23071 .breakpoint_store()
23072 .as_ref()
23073 .unwrap()
23074 .read(cx)
23075 .all_source_breakpoints(cx)
23076 });
23077
23078 assert_eq!(1, breakpoints.len());
23079 assert_breakpoint(
23080 &breakpoints,
23081 &abs_path,
23082 vec![
23083 (0, Breakpoint::new_standard()),
23084 (3, Breakpoint::new_standard()),
23085 ],
23086 );
23087
23088 editor.update_in(cx, |editor, window, cx| {
23089 editor.move_to_beginning(&MoveToBeginning, window, cx);
23090 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23091 });
23092
23093 let breakpoints = editor.update(cx, |editor, cx| {
23094 editor
23095 .breakpoint_store()
23096 .as_ref()
23097 .unwrap()
23098 .read(cx)
23099 .all_source_breakpoints(cx)
23100 });
23101
23102 assert_eq!(1, breakpoints.len());
23103 assert_breakpoint(
23104 &breakpoints,
23105 &abs_path,
23106 vec![(3, Breakpoint::new_standard())],
23107 );
23108
23109 editor.update_in(cx, |editor, window, cx| {
23110 editor.move_to_end(&MoveToEnd, window, cx);
23111 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23112 });
23113
23114 let breakpoints = editor.update(cx, |editor, cx| {
23115 editor
23116 .breakpoint_store()
23117 .as_ref()
23118 .unwrap()
23119 .read(cx)
23120 .all_source_breakpoints(cx)
23121 });
23122
23123 assert_eq!(0, breakpoints.len());
23124 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23125}
23126
23127#[gpui::test]
23128async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23129 init_test(cx, |_| {});
23130
23131 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23132
23133 let fs = FakeFs::new(cx.executor());
23134 fs.insert_tree(
23135 path!("/a"),
23136 json!({
23137 "main.rs": sample_text,
23138 }),
23139 )
23140 .await;
23141 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23142 let (workspace, cx) =
23143 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23144
23145 let worktree_id = workspace.update(cx, |workspace, cx| {
23146 workspace.project().update(cx, |project, cx| {
23147 project.worktrees(cx).next().unwrap().read(cx).id()
23148 })
23149 });
23150
23151 let buffer = project
23152 .update(cx, |project, cx| {
23153 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23154 })
23155 .await
23156 .unwrap();
23157
23158 let (editor, cx) = cx.add_window_view(|window, cx| {
23159 Editor::new(
23160 EditorMode::full(),
23161 MultiBuffer::build_from_buffer(buffer, cx),
23162 Some(project.clone()),
23163 window,
23164 cx,
23165 )
23166 });
23167
23168 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23169 let abs_path = project.read_with(cx, |project, cx| {
23170 project
23171 .absolute_path(&project_path, cx)
23172 .map(Arc::from)
23173 .unwrap()
23174 });
23175
23176 editor.update_in(cx, |editor, window, cx| {
23177 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23178 });
23179
23180 let breakpoints = editor.update(cx, |editor, cx| {
23181 editor
23182 .breakpoint_store()
23183 .as_ref()
23184 .unwrap()
23185 .read(cx)
23186 .all_source_breakpoints(cx)
23187 });
23188
23189 assert_breakpoint(
23190 &breakpoints,
23191 &abs_path,
23192 vec![(0, Breakpoint::new_log("hello world"))],
23193 );
23194
23195 // Removing a log message from a log breakpoint should remove it
23196 editor.update_in(cx, |editor, window, cx| {
23197 add_log_breakpoint_at_cursor(editor, "", window, cx);
23198 });
23199
23200 let breakpoints = editor.update(cx, |editor, cx| {
23201 editor
23202 .breakpoint_store()
23203 .as_ref()
23204 .unwrap()
23205 .read(cx)
23206 .all_source_breakpoints(cx)
23207 });
23208
23209 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23210
23211 editor.update_in(cx, |editor, window, cx| {
23212 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23213 editor.move_to_end(&MoveToEnd, window, cx);
23214 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23215 // Not adding a log message to a standard breakpoint shouldn't remove it
23216 add_log_breakpoint_at_cursor(editor, "", window, cx);
23217 });
23218
23219 let breakpoints = editor.update(cx, |editor, cx| {
23220 editor
23221 .breakpoint_store()
23222 .as_ref()
23223 .unwrap()
23224 .read(cx)
23225 .all_source_breakpoints(cx)
23226 });
23227
23228 assert_breakpoint(
23229 &breakpoints,
23230 &abs_path,
23231 vec![
23232 (0, Breakpoint::new_standard()),
23233 (3, Breakpoint::new_standard()),
23234 ],
23235 );
23236
23237 editor.update_in(cx, |editor, window, cx| {
23238 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23239 });
23240
23241 let breakpoints = editor.update(cx, |editor, cx| {
23242 editor
23243 .breakpoint_store()
23244 .as_ref()
23245 .unwrap()
23246 .read(cx)
23247 .all_source_breakpoints(cx)
23248 });
23249
23250 assert_breakpoint(
23251 &breakpoints,
23252 &abs_path,
23253 vec![
23254 (0, Breakpoint::new_standard()),
23255 (3, Breakpoint::new_log("hello world")),
23256 ],
23257 );
23258
23259 editor.update_in(cx, |editor, window, cx| {
23260 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23261 });
23262
23263 let breakpoints = editor.update(cx, |editor, cx| {
23264 editor
23265 .breakpoint_store()
23266 .as_ref()
23267 .unwrap()
23268 .read(cx)
23269 .all_source_breakpoints(cx)
23270 });
23271
23272 assert_breakpoint(
23273 &breakpoints,
23274 &abs_path,
23275 vec![
23276 (0, Breakpoint::new_standard()),
23277 (3, Breakpoint::new_log("hello Earth!!")),
23278 ],
23279 );
23280}
23281
23282/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23283/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23284/// or when breakpoints were placed out of order. This tests for a regression too
23285#[gpui::test]
23286async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23287 init_test(cx, |_| {});
23288
23289 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23290 let fs = FakeFs::new(cx.executor());
23291 fs.insert_tree(
23292 path!("/a"),
23293 json!({
23294 "main.rs": sample_text,
23295 }),
23296 )
23297 .await;
23298 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23299 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23300 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23301
23302 let fs = FakeFs::new(cx.executor());
23303 fs.insert_tree(
23304 path!("/a"),
23305 json!({
23306 "main.rs": sample_text,
23307 }),
23308 )
23309 .await;
23310 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23311 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23312 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23313 let worktree_id = workspace
23314 .update(cx, |workspace, _window, cx| {
23315 workspace.project().update(cx, |project, cx| {
23316 project.worktrees(cx).next().unwrap().read(cx).id()
23317 })
23318 })
23319 .unwrap();
23320
23321 let buffer = project
23322 .update(cx, |project, cx| {
23323 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23324 })
23325 .await
23326 .unwrap();
23327
23328 let (editor, cx) = cx.add_window_view(|window, cx| {
23329 Editor::new(
23330 EditorMode::full(),
23331 MultiBuffer::build_from_buffer(buffer, cx),
23332 Some(project.clone()),
23333 window,
23334 cx,
23335 )
23336 });
23337
23338 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23339 let abs_path = project.read_with(cx, |project, cx| {
23340 project
23341 .absolute_path(&project_path, cx)
23342 .map(Arc::from)
23343 .unwrap()
23344 });
23345
23346 // assert we can add breakpoint on the first line
23347 editor.update_in(cx, |editor, window, cx| {
23348 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23349 editor.move_to_end(&MoveToEnd, window, cx);
23350 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23351 editor.move_up(&MoveUp, window, cx);
23352 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23353 });
23354
23355 let breakpoints = editor.update(cx, |editor, cx| {
23356 editor
23357 .breakpoint_store()
23358 .as_ref()
23359 .unwrap()
23360 .read(cx)
23361 .all_source_breakpoints(cx)
23362 });
23363
23364 assert_eq!(1, breakpoints.len());
23365 assert_breakpoint(
23366 &breakpoints,
23367 &abs_path,
23368 vec![
23369 (0, Breakpoint::new_standard()),
23370 (2, Breakpoint::new_standard()),
23371 (3, Breakpoint::new_standard()),
23372 ],
23373 );
23374
23375 editor.update_in(cx, |editor, window, cx| {
23376 editor.move_to_beginning(&MoveToBeginning, window, cx);
23377 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23378 editor.move_to_end(&MoveToEnd, window, cx);
23379 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23380 // Disabling a breakpoint that doesn't exist should do nothing
23381 editor.move_up(&MoveUp, window, cx);
23382 editor.move_up(&MoveUp, window, cx);
23383 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23384 });
23385
23386 let breakpoints = editor.update(cx, |editor, cx| {
23387 editor
23388 .breakpoint_store()
23389 .as_ref()
23390 .unwrap()
23391 .read(cx)
23392 .all_source_breakpoints(cx)
23393 });
23394
23395 let disable_breakpoint = {
23396 let mut bp = Breakpoint::new_standard();
23397 bp.state = BreakpointState::Disabled;
23398 bp
23399 };
23400
23401 assert_eq!(1, breakpoints.len());
23402 assert_breakpoint(
23403 &breakpoints,
23404 &abs_path,
23405 vec![
23406 (0, disable_breakpoint.clone()),
23407 (2, Breakpoint::new_standard()),
23408 (3, disable_breakpoint.clone()),
23409 ],
23410 );
23411
23412 editor.update_in(cx, |editor, window, cx| {
23413 editor.move_to_beginning(&MoveToBeginning, window, cx);
23414 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23415 editor.move_to_end(&MoveToEnd, window, cx);
23416 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23417 editor.move_up(&MoveUp, window, cx);
23418 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23419 });
23420
23421 let breakpoints = editor.update(cx, |editor, cx| {
23422 editor
23423 .breakpoint_store()
23424 .as_ref()
23425 .unwrap()
23426 .read(cx)
23427 .all_source_breakpoints(cx)
23428 });
23429
23430 assert_eq!(1, breakpoints.len());
23431 assert_breakpoint(
23432 &breakpoints,
23433 &abs_path,
23434 vec![
23435 (0, Breakpoint::new_standard()),
23436 (2, disable_breakpoint),
23437 (3, Breakpoint::new_standard()),
23438 ],
23439 );
23440}
23441
23442#[gpui::test]
23443async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23444 init_test(cx, |_| {});
23445 let capabilities = lsp::ServerCapabilities {
23446 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23447 prepare_provider: Some(true),
23448 work_done_progress_options: Default::default(),
23449 })),
23450 ..Default::default()
23451 };
23452 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23453
23454 cx.set_state(indoc! {"
23455 struct Fˇoo {}
23456 "});
23457
23458 cx.update_editor(|editor, _, cx| {
23459 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23460 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23461 editor.highlight_background::<DocumentHighlightRead>(
23462 &[highlight_range],
23463 |theme| theme.colors().editor_document_highlight_read_background,
23464 cx,
23465 );
23466 });
23467
23468 let mut prepare_rename_handler = cx
23469 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23470 move |_, _, _| async move {
23471 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23472 start: lsp::Position {
23473 line: 0,
23474 character: 7,
23475 },
23476 end: lsp::Position {
23477 line: 0,
23478 character: 10,
23479 },
23480 })))
23481 },
23482 );
23483 let prepare_rename_task = cx
23484 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23485 .expect("Prepare rename was not started");
23486 prepare_rename_handler.next().await.unwrap();
23487 prepare_rename_task.await.expect("Prepare rename failed");
23488
23489 let mut rename_handler =
23490 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23491 let edit = lsp::TextEdit {
23492 range: lsp::Range {
23493 start: lsp::Position {
23494 line: 0,
23495 character: 7,
23496 },
23497 end: lsp::Position {
23498 line: 0,
23499 character: 10,
23500 },
23501 },
23502 new_text: "FooRenamed".to_string(),
23503 };
23504 Ok(Some(lsp::WorkspaceEdit::new(
23505 // Specify the same edit twice
23506 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23507 )))
23508 });
23509 let rename_task = cx
23510 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23511 .expect("Confirm rename was not started");
23512 rename_handler.next().await.unwrap();
23513 rename_task.await.expect("Confirm rename failed");
23514 cx.run_until_parked();
23515
23516 // Despite two edits, only one is actually applied as those are identical
23517 cx.assert_editor_state(indoc! {"
23518 struct FooRenamedˇ {}
23519 "});
23520}
23521
23522#[gpui::test]
23523async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23524 init_test(cx, |_| {});
23525 // These capabilities indicate that the server does not support prepare rename.
23526 let capabilities = lsp::ServerCapabilities {
23527 rename_provider: Some(lsp::OneOf::Left(true)),
23528 ..Default::default()
23529 };
23530 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23531
23532 cx.set_state(indoc! {"
23533 struct Fˇoo {}
23534 "});
23535
23536 cx.update_editor(|editor, _window, cx| {
23537 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23538 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23539 editor.highlight_background::<DocumentHighlightRead>(
23540 &[highlight_range],
23541 |theme| theme.colors().editor_document_highlight_read_background,
23542 cx,
23543 );
23544 });
23545
23546 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23547 .expect("Prepare rename was not started")
23548 .await
23549 .expect("Prepare rename failed");
23550
23551 let mut rename_handler =
23552 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23553 let edit = lsp::TextEdit {
23554 range: lsp::Range {
23555 start: lsp::Position {
23556 line: 0,
23557 character: 7,
23558 },
23559 end: lsp::Position {
23560 line: 0,
23561 character: 10,
23562 },
23563 },
23564 new_text: "FooRenamed".to_string(),
23565 };
23566 Ok(Some(lsp::WorkspaceEdit::new(
23567 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23568 )))
23569 });
23570 let rename_task = cx
23571 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23572 .expect("Confirm rename was not started");
23573 rename_handler.next().await.unwrap();
23574 rename_task.await.expect("Confirm rename failed");
23575 cx.run_until_parked();
23576
23577 // Correct range is renamed, as `surrounding_word` is used to find it.
23578 cx.assert_editor_state(indoc! {"
23579 struct FooRenamedˇ {}
23580 "});
23581}
23582
23583#[gpui::test]
23584async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23585 init_test(cx, |_| {});
23586 let mut cx = EditorTestContext::new(cx).await;
23587
23588 let language = Arc::new(
23589 Language::new(
23590 LanguageConfig::default(),
23591 Some(tree_sitter_html::LANGUAGE.into()),
23592 )
23593 .with_brackets_query(
23594 r#"
23595 ("<" @open "/>" @close)
23596 ("</" @open ">" @close)
23597 ("<" @open ">" @close)
23598 ("\"" @open "\"" @close)
23599 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23600 "#,
23601 )
23602 .unwrap(),
23603 );
23604 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23605
23606 cx.set_state(indoc! {"
23607 <span>ˇ</span>
23608 "});
23609 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23610 cx.assert_editor_state(indoc! {"
23611 <span>
23612 ˇ
23613 </span>
23614 "});
23615
23616 cx.set_state(indoc! {"
23617 <span><span></span>ˇ</span>
23618 "});
23619 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23620 cx.assert_editor_state(indoc! {"
23621 <span><span></span>
23622 ˇ</span>
23623 "});
23624
23625 cx.set_state(indoc! {"
23626 <span>ˇ
23627 </span>
23628 "});
23629 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23630 cx.assert_editor_state(indoc! {"
23631 <span>
23632 ˇ
23633 </span>
23634 "});
23635}
23636
23637#[gpui::test(iterations = 10)]
23638async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23639 init_test(cx, |_| {});
23640
23641 let fs = FakeFs::new(cx.executor());
23642 fs.insert_tree(
23643 path!("/dir"),
23644 json!({
23645 "a.ts": "a",
23646 }),
23647 )
23648 .await;
23649
23650 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23651 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23652 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23653
23654 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23655 language_registry.add(Arc::new(Language::new(
23656 LanguageConfig {
23657 name: "TypeScript".into(),
23658 matcher: LanguageMatcher {
23659 path_suffixes: vec!["ts".to_string()],
23660 ..Default::default()
23661 },
23662 ..Default::default()
23663 },
23664 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23665 )));
23666 let mut fake_language_servers = language_registry.register_fake_lsp(
23667 "TypeScript",
23668 FakeLspAdapter {
23669 capabilities: lsp::ServerCapabilities {
23670 code_lens_provider: Some(lsp::CodeLensOptions {
23671 resolve_provider: Some(true),
23672 }),
23673 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23674 commands: vec!["_the/command".to_string()],
23675 ..lsp::ExecuteCommandOptions::default()
23676 }),
23677 ..lsp::ServerCapabilities::default()
23678 },
23679 ..FakeLspAdapter::default()
23680 },
23681 );
23682
23683 let editor = workspace
23684 .update(cx, |workspace, window, cx| {
23685 workspace.open_abs_path(
23686 PathBuf::from(path!("/dir/a.ts")),
23687 OpenOptions::default(),
23688 window,
23689 cx,
23690 )
23691 })
23692 .unwrap()
23693 .await
23694 .unwrap()
23695 .downcast::<Editor>()
23696 .unwrap();
23697 cx.executor().run_until_parked();
23698
23699 let fake_server = fake_language_servers.next().await.unwrap();
23700
23701 let buffer = editor.update(cx, |editor, cx| {
23702 editor
23703 .buffer()
23704 .read(cx)
23705 .as_singleton()
23706 .expect("have opened a single file by path")
23707 });
23708
23709 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23710 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23711 drop(buffer_snapshot);
23712 let actions = cx
23713 .update_window(*workspace, |_, window, cx| {
23714 project.code_actions(&buffer, anchor..anchor, window, cx)
23715 })
23716 .unwrap();
23717
23718 fake_server
23719 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23720 Ok(Some(vec![
23721 lsp::CodeLens {
23722 range: lsp::Range::default(),
23723 command: Some(lsp::Command {
23724 title: "Code lens command".to_owned(),
23725 command: "_the/command".to_owned(),
23726 arguments: None,
23727 }),
23728 data: None,
23729 },
23730 lsp::CodeLens {
23731 range: lsp::Range::default(),
23732 command: Some(lsp::Command {
23733 title: "Command not in capabilities".to_owned(),
23734 command: "not in capabilities".to_owned(),
23735 arguments: None,
23736 }),
23737 data: None,
23738 },
23739 lsp::CodeLens {
23740 range: lsp::Range {
23741 start: lsp::Position {
23742 line: 1,
23743 character: 1,
23744 },
23745 end: lsp::Position {
23746 line: 1,
23747 character: 1,
23748 },
23749 },
23750 command: Some(lsp::Command {
23751 title: "Command not in range".to_owned(),
23752 command: "_the/command".to_owned(),
23753 arguments: None,
23754 }),
23755 data: None,
23756 },
23757 ]))
23758 })
23759 .next()
23760 .await;
23761
23762 let actions = actions.await.unwrap();
23763 assert_eq!(
23764 actions.len(),
23765 1,
23766 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23767 );
23768 let action = actions[0].clone();
23769 let apply = project.update(cx, |project, cx| {
23770 project.apply_code_action(buffer.clone(), action, true, cx)
23771 });
23772
23773 // Resolving the code action does not populate its edits. In absence of
23774 // edits, we must execute the given command.
23775 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23776 |mut lens, _| async move {
23777 let lens_command = lens.command.as_mut().expect("should have a command");
23778 assert_eq!(lens_command.title, "Code lens command");
23779 lens_command.arguments = Some(vec![json!("the-argument")]);
23780 Ok(lens)
23781 },
23782 );
23783
23784 // While executing the command, the language server sends the editor
23785 // a `workspaceEdit` request.
23786 fake_server
23787 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23788 let fake = fake_server.clone();
23789 move |params, _| {
23790 assert_eq!(params.command, "_the/command");
23791 let fake = fake.clone();
23792 async move {
23793 fake.server
23794 .request::<lsp::request::ApplyWorkspaceEdit>(
23795 lsp::ApplyWorkspaceEditParams {
23796 label: None,
23797 edit: lsp::WorkspaceEdit {
23798 changes: Some(
23799 [(
23800 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23801 vec![lsp::TextEdit {
23802 range: lsp::Range::new(
23803 lsp::Position::new(0, 0),
23804 lsp::Position::new(0, 0),
23805 ),
23806 new_text: "X".into(),
23807 }],
23808 )]
23809 .into_iter()
23810 .collect(),
23811 ),
23812 ..lsp::WorkspaceEdit::default()
23813 },
23814 },
23815 )
23816 .await
23817 .into_response()
23818 .unwrap();
23819 Ok(Some(json!(null)))
23820 }
23821 }
23822 })
23823 .next()
23824 .await;
23825
23826 // Applying the code lens command returns a project transaction containing the edits
23827 // sent by the language server in its `workspaceEdit` request.
23828 let transaction = apply.await.unwrap();
23829 assert!(transaction.0.contains_key(&buffer));
23830 buffer.update(cx, |buffer, cx| {
23831 assert_eq!(buffer.text(), "Xa");
23832 buffer.undo(cx);
23833 assert_eq!(buffer.text(), "a");
23834 });
23835
23836 let actions_after_edits = cx
23837 .update_window(*workspace, |_, window, cx| {
23838 project.code_actions(&buffer, anchor..anchor, window, cx)
23839 })
23840 .unwrap()
23841 .await
23842 .unwrap();
23843 assert_eq!(
23844 actions, actions_after_edits,
23845 "For the same selection, same code lens actions should be returned"
23846 );
23847
23848 let _responses =
23849 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23850 panic!("No more code lens requests are expected");
23851 });
23852 editor.update_in(cx, |editor, window, cx| {
23853 editor.select_all(&SelectAll, window, cx);
23854 });
23855 cx.executor().run_until_parked();
23856 let new_actions = cx
23857 .update_window(*workspace, |_, window, cx| {
23858 project.code_actions(&buffer, anchor..anchor, window, cx)
23859 })
23860 .unwrap()
23861 .await
23862 .unwrap();
23863 assert_eq!(
23864 actions, new_actions,
23865 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23866 );
23867}
23868
23869#[gpui::test]
23870async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23871 init_test(cx, |_| {});
23872
23873 let fs = FakeFs::new(cx.executor());
23874 let main_text = r#"fn main() {
23875println!("1");
23876println!("2");
23877println!("3");
23878println!("4");
23879println!("5");
23880}"#;
23881 let lib_text = "mod foo {}";
23882 fs.insert_tree(
23883 path!("/a"),
23884 json!({
23885 "lib.rs": lib_text,
23886 "main.rs": main_text,
23887 }),
23888 )
23889 .await;
23890
23891 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23892 let (workspace, cx) =
23893 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23894 let worktree_id = workspace.update(cx, |workspace, cx| {
23895 workspace.project().update(cx, |project, cx| {
23896 project.worktrees(cx).next().unwrap().read(cx).id()
23897 })
23898 });
23899
23900 let expected_ranges = vec![
23901 Point::new(0, 0)..Point::new(0, 0),
23902 Point::new(1, 0)..Point::new(1, 1),
23903 Point::new(2, 0)..Point::new(2, 2),
23904 Point::new(3, 0)..Point::new(3, 3),
23905 ];
23906
23907 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23908 let editor_1 = workspace
23909 .update_in(cx, |workspace, window, cx| {
23910 workspace.open_path(
23911 (worktree_id, rel_path("main.rs")),
23912 Some(pane_1.downgrade()),
23913 true,
23914 window,
23915 cx,
23916 )
23917 })
23918 .unwrap()
23919 .await
23920 .downcast::<Editor>()
23921 .unwrap();
23922 pane_1.update(cx, |pane, cx| {
23923 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23924 open_editor.update(cx, |editor, cx| {
23925 assert_eq!(
23926 editor.display_text(cx),
23927 main_text,
23928 "Original main.rs text on initial open",
23929 );
23930 assert_eq!(
23931 editor
23932 .selections
23933 .all::<Point>(&editor.display_snapshot(cx))
23934 .into_iter()
23935 .map(|s| s.range())
23936 .collect::<Vec<_>>(),
23937 vec![Point::zero()..Point::zero()],
23938 "Default selections on initial open",
23939 );
23940 })
23941 });
23942 editor_1.update_in(cx, |editor, window, cx| {
23943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23944 s.select_ranges(expected_ranges.clone());
23945 });
23946 });
23947
23948 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23949 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23950 });
23951 let editor_2 = workspace
23952 .update_in(cx, |workspace, window, cx| {
23953 workspace.open_path(
23954 (worktree_id, rel_path("main.rs")),
23955 Some(pane_2.downgrade()),
23956 true,
23957 window,
23958 cx,
23959 )
23960 })
23961 .unwrap()
23962 .await
23963 .downcast::<Editor>()
23964 .unwrap();
23965 pane_2.update(cx, |pane, cx| {
23966 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23967 open_editor.update(cx, |editor, cx| {
23968 assert_eq!(
23969 editor.display_text(cx),
23970 main_text,
23971 "Original main.rs text on initial open in another panel",
23972 );
23973 assert_eq!(
23974 editor
23975 .selections
23976 .all::<Point>(&editor.display_snapshot(cx))
23977 .into_iter()
23978 .map(|s| s.range())
23979 .collect::<Vec<_>>(),
23980 vec![Point::zero()..Point::zero()],
23981 "Default selections on initial open in another panel",
23982 );
23983 })
23984 });
23985
23986 editor_2.update_in(cx, |editor, window, cx| {
23987 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23988 });
23989
23990 let _other_editor_1 = workspace
23991 .update_in(cx, |workspace, window, cx| {
23992 workspace.open_path(
23993 (worktree_id, rel_path("lib.rs")),
23994 Some(pane_1.downgrade()),
23995 true,
23996 window,
23997 cx,
23998 )
23999 })
24000 .unwrap()
24001 .await
24002 .downcast::<Editor>()
24003 .unwrap();
24004 pane_1
24005 .update_in(cx, |pane, window, cx| {
24006 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24007 })
24008 .await
24009 .unwrap();
24010 drop(editor_1);
24011 pane_1.update(cx, |pane, cx| {
24012 pane.active_item()
24013 .unwrap()
24014 .downcast::<Editor>()
24015 .unwrap()
24016 .update(cx, |editor, cx| {
24017 assert_eq!(
24018 editor.display_text(cx),
24019 lib_text,
24020 "Other file should be open and active",
24021 );
24022 });
24023 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24024 });
24025
24026 let _other_editor_2 = workspace
24027 .update_in(cx, |workspace, window, cx| {
24028 workspace.open_path(
24029 (worktree_id, rel_path("lib.rs")),
24030 Some(pane_2.downgrade()),
24031 true,
24032 window,
24033 cx,
24034 )
24035 })
24036 .unwrap()
24037 .await
24038 .downcast::<Editor>()
24039 .unwrap();
24040 pane_2
24041 .update_in(cx, |pane, window, cx| {
24042 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24043 })
24044 .await
24045 .unwrap();
24046 drop(editor_2);
24047 pane_2.update(cx, |pane, cx| {
24048 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24049 open_editor.update(cx, |editor, cx| {
24050 assert_eq!(
24051 editor.display_text(cx),
24052 lib_text,
24053 "Other file should be open and active in another panel too",
24054 );
24055 });
24056 assert_eq!(
24057 pane.items().count(),
24058 1,
24059 "No other editors should be open in another pane",
24060 );
24061 });
24062
24063 let _editor_1_reopened = workspace
24064 .update_in(cx, |workspace, window, cx| {
24065 workspace.open_path(
24066 (worktree_id, rel_path("main.rs")),
24067 Some(pane_1.downgrade()),
24068 true,
24069 window,
24070 cx,
24071 )
24072 })
24073 .unwrap()
24074 .await
24075 .downcast::<Editor>()
24076 .unwrap();
24077 let _editor_2_reopened = workspace
24078 .update_in(cx, |workspace, window, cx| {
24079 workspace.open_path(
24080 (worktree_id, rel_path("main.rs")),
24081 Some(pane_2.downgrade()),
24082 true,
24083 window,
24084 cx,
24085 )
24086 })
24087 .unwrap()
24088 .await
24089 .downcast::<Editor>()
24090 .unwrap();
24091 pane_1.update(cx, |pane, cx| {
24092 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24093 open_editor.update(cx, |editor, cx| {
24094 assert_eq!(
24095 editor.display_text(cx),
24096 main_text,
24097 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24098 );
24099 assert_eq!(
24100 editor
24101 .selections
24102 .all::<Point>(&editor.display_snapshot(cx))
24103 .into_iter()
24104 .map(|s| s.range())
24105 .collect::<Vec<_>>(),
24106 expected_ranges,
24107 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24108 );
24109 })
24110 });
24111 pane_2.update(cx, |pane, cx| {
24112 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24113 open_editor.update(cx, |editor, cx| {
24114 assert_eq!(
24115 editor.display_text(cx),
24116 r#"fn main() {
24117⋯rintln!("1");
24118⋯intln!("2");
24119⋯ntln!("3");
24120println!("4");
24121println!("5");
24122}"#,
24123 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24124 );
24125 assert_eq!(
24126 editor
24127 .selections
24128 .all::<Point>(&editor.display_snapshot(cx))
24129 .into_iter()
24130 .map(|s| s.range())
24131 .collect::<Vec<_>>(),
24132 vec![Point::zero()..Point::zero()],
24133 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24134 );
24135 })
24136 });
24137}
24138
24139#[gpui::test]
24140async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24141 init_test(cx, |_| {});
24142
24143 let fs = FakeFs::new(cx.executor());
24144 let main_text = r#"fn main() {
24145println!("1");
24146println!("2");
24147println!("3");
24148println!("4");
24149println!("5");
24150}"#;
24151 let lib_text = "mod foo {}";
24152 fs.insert_tree(
24153 path!("/a"),
24154 json!({
24155 "lib.rs": lib_text,
24156 "main.rs": main_text,
24157 }),
24158 )
24159 .await;
24160
24161 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24162 let (workspace, cx) =
24163 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24164 let worktree_id = workspace.update(cx, |workspace, cx| {
24165 workspace.project().update(cx, |project, cx| {
24166 project.worktrees(cx).next().unwrap().read(cx).id()
24167 })
24168 });
24169
24170 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24171 let editor = workspace
24172 .update_in(cx, |workspace, window, cx| {
24173 workspace.open_path(
24174 (worktree_id, rel_path("main.rs")),
24175 Some(pane.downgrade()),
24176 true,
24177 window,
24178 cx,
24179 )
24180 })
24181 .unwrap()
24182 .await
24183 .downcast::<Editor>()
24184 .unwrap();
24185 pane.update(cx, |pane, cx| {
24186 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24187 open_editor.update(cx, |editor, cx| {
24188 assert_eq!(
24189 editor.display_text(cx),
24190 main_text,
24191 "Original main.rs text on initial open",
24192 );
24193 })
24194 });
24195 editor.update_in(cx, |editor, window, cx| {
24196 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24197 });
24198
24199 cx.update_global(|store: &mut SettingsStore, cx| {
24200 store.update_user_settings(cx, |s| {
24201 s.workspace.restore_on_file_reopen = Some(false);
24202 });
24203 });
24204 editor.update_in(cx, |editor, window, cx| {
24205 editor.fold_ranges(
24206 vec![
24207 Point::new(1, 0)..Point::new(1, 1),
24208 Point::new(2, 0)..Point::new(2, 2),
24209 Point::new(3, 0)..Point::new(3, 3),
24210 ],
24211 false,
24212 window,
24213 cx,
24214 );
24215 });
24216 pane.update_in(cx, |pane, window, cx| {
24217 pane.close_all_items(&CloseAllItems::default(), window, cx)
24218 })
24219 .await
24220 .unwrap();
24221 pane.update(cx, |pane, _| {
24222 assert!(pane.active_item().is_none());
24223 });
24224 cx.update_global(|store: &mut SettingsStore, cx| {
24225 store.update_user_settings(cx, |s| {
24226 s.workspace.restore_on_file_reopen = Some(true);
24227 });
24228 });
24229
24230 let _editor_reopened = workspace
24231 .update_in(cx, |workspace, window, cx| {
24232 workspace.open_path(
24233 (worktree_id, rel_path("main.rs")),
24234 Some(pane.downgrade()),
24235 true,
24236 window,
24237 cx,
24238 )
24239 })
24240 .unwrap()
24241 .await
24242 .downcast::<Editor>()
24243 .unwrap();
24244 pane.update(cx, |pane, cx| {
24245 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24246 open_editor.update(cx, |editor, cx| {
24247 assert_eq!(
24248 editor.display_text(cx),
24249 main_text,
24250 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24251 );
24252 })
24253 });
24254}
24255
24256#[gpui::test]
24257async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24258 struct EmptyModalView {
24259 focus_handle: gpui::FocusHandle,
24260 }
24261 impl EventEmitter<DismissEvent> for EmptyModalView {}
24262 impl Render for EmptyModalView {
24263 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24264 div()
24265 }
24266 }
24267 impl Focusable for EmptyModalView {
24268 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24269 self.focus_handle.clone()
24270 }
24271 }
24272 impl workspace::ModalView for EmptyModalView {}
24273 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24274 EmptyModalView {
24275 focus_handle: cx.focus_handle(),
24276 }
24277 }
24278
24279 init_test(cx, |_| {});
24280
24281 let fs = FakeFs::new(cx.executor());
24282 let project = Project::test(fs, [], cx).await;
24283 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24284 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24285 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24286 let editor = cx.new_window_entity(|window, cx| {
24287 Editor::new(
24288 EditorMode::full(),
24289 buffer,
24290 Some(project.clone()),
24291 window,
24292 cx,
24293 )
24294 });
24295 workspace
24296 .update(cx, |workspace, window, cx| {
24297 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24298 })
24299 .unwrap();
24300 editor.update_in(cx, |editor, window, cx| {
24301 editor.open_context_menu(&OpenContextMenu, window, cx);
24302 assert!(editor.mouse_context_menu.is_some());
24303 });
24304 workspace
24305 .update(cx, |workspace, window, cx| {
24306 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24307 })
24308 .unwrap();
24309 cx.read(|cx| {
24310 assert!(editor.read(cx).mouse_context_menu.is_none());
24311 });
24312}
24313
24314fn set_linked_edit_ranges(
24315 opening: (Point, Point),
24316 closing: (Point, Point),
24317 editor: &mut Editor,
24318 cx: &mut Context<Editor>,
24319) {
24320 let Some((buffer, _)) = editor
24321 .buffer
24322 .read(cx)
24323 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24324 else {
24325 panic!("Failed to get buffer for selection position");
24326 };
24327 let buffer = buffer.read(cx);
24328 let buffer_id = buffer.remote_id();
24329 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24330 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24331 let mut linked_ranges = HashMap::default();
24332 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24333 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24334}
24335
24336#[gpui::test]
24337async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24338 init_test(cx, |_| {});
24339
24340 let fs = FakeFs::new(cx.executor());
24341 fs.insert_file(path!("/file.html"), Default::default())
24342 .await;
24343
24344 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24345
24346 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24347 let html_language = Arc::new(Language::new(
24348 LanguageConfig {
24349 name: "HTML".into(),
24350 matcher: LanguageMatcher {
24351 path_suffixes: vec!["html".to_string()],
24352 ..LanguageMatcher::default()
24353 },
24354 brackets: BracketPairConfig {
24355 pairs: vec![BracketPair {
24356 start: "<".into(),
24357 end: ">".into(),
24358 close: true,
24359 ..Default::default()
24360 }],
24361 ..Default::default()
24362 },
24363 ..Default::default()
24364 },
24365 Some(tree_sitter_html::LANGUAGE.into()),
24366 ));
24367 language_registry.add(html_language);
24368 let mut fake_servers = language_registry.register_fake_lsp(
24369 "HTML",
24370 FakeLspAdapter {
24371 capabilities: lsp::ServerCapabilities {
24372 completion_provider: Some(lsp::CompletionOptions {
24373 resolve_provider: Some(true),
24374 ..Default::default()
24375 }),
24376 ..Default::default()
24377 },
24378 ..Default::default()
24379 },
24380 );
24381
24382 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24383 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24384
24385 let worktree_id = workspace
24386 .update(cx, |workspace, _window, cx| {
24387 workspace.project().update(cx, |project, cx| {
24388 project.worktrees(cx).next().unwrap().read(cx).id()
24389 })
24390 })
24391 .unwrap();
24392 project
24393 .update(cx, |project, cx| {
24394 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24395 })
24396 .await
24397 .unwrap();
24398 let editor = workspace
24399 .update(cx, |workspace, window, cx| {
24400 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24401 })
24402 .unwrap()
24403 .await
24404 .unwrap()
24405 .downcast::<Editor>()
24406 .unwrap();
24407
24408 let fake_server = fake_servers.next().await.unwrap();
24409 editor.update_in(cx, |editor, window, cx| {
24410 editor.set_text("<ad></ad>", window, cx);
24411 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24412 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24413 });
24414 set_linked_edit_ranges(
24415 (Point::new(0, 1), Point::new(0, 3)),
24416 (Point::new(0, 6), Point::new(0, 8)),
24417 editor,
24418 cx,
24419 );
24420 });
24421 let mut completion_handle =
24422 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24423 Ok(Some(lsp::CompletionResponse::Array(vec![
24424 lsp::CompletionItem {
24425 label: "head".to_string(),
24426 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24427 lsp::InsertReplaceEdit {
24428 new_text: "head".to_string(),
24429 insert: lsp::Range::new(
24430 lsp::Position::new(0, 1),
24431 lsp::Position::new(0, 3),
24432 ),
24433 replace: lsp::Range::new(
24434 lsp::Position::new(0, 1),
24435 lsp::Position::new(0, 3),
24436 ),
24437 },
24438 )),
24439 ..Default::default()
24440 },
24441 ])))
24442 });
24443 editor.update_in(cx, |editor, window, cx| {
24444 editor.show_completions(&ShowCompletions, window, cx);
24445 });
24446 cx.run_until_parked();
24447 completion_handle.next().await.unwrap();
24448 editor.update(cx, |editor, _| {
24449 assert!(
24450 editor.context_menu_visible(),
24451 "Completion menu should be visible"
24452 );
24453 });
24454 editor.update_in(cx, |editor, window, cx| {
24455 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24456 });
24457 cx.executor().run_until_parked();
24458 editor.update(cx, |editor, cx| {
24459 assert_eq!(editor.text(cx), "<head></head>");
24460 });
24461}
24462
24463#[gpui::test]
24464async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24465 init_test(cx, |_| {});
24466
24467 let mut cx = EditorTestContext::new(cx).await;
24468 let language = Arc::new(Language::new(
24469 LanguageConfig {
24470 name: "TSX".into(),
24471 matcher: LanguageMatcher {
24472 path_suffixes: vec!["tsx".to_string()],
24473 ..LanguageMatcher::default()
24474 },
24475 brackets: BracketPairConfig {
24476 pairs: vec![BracketPair {
24477 start: "<".into(),
24478 end: ">".into(),
24479 close: true,
24480 ..Default::default()
24481 }],
24482 ..Default::default()
24483 },
24484 linked_edit_characters: HashSet::from_iter(['.']),
24485 ..Default::default()
24486 },
24487 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24488 ));
24489 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24490
24491 // Test typing > does not extend linked pair
24492 cx.set_state("<divˇ<div></div>");
24493 cx.update_editor(|editor, _, cx| {
24494 set_linked_edit_ranges(
24495 (Point::new(0, 1), Point::new(0, 4)),
24496 (Point::new(0, 11), Point::new(0, 14)),
24497 editor,
24498 cx,
24499 );
24500 });
24501 cx.update_editor(|editor, window, cx| {
24502 editor.handle_input(">", window, cx);
24503 });
24504 cx.assert_editor_state("<div>ˇ<div></div>");
24505
24506 // Test typing . do extend linked pair
24507 cx.set_state("<Animatedˇ></Animated>");
24508 cx.update_editor(|editor, _, cx| {
24509 set_linked_edit_ranges(
24510 (Point::new(0, 1), Point::new(0, 9)),
24511 (Point::new(0, 12), Point::new(0, 20)),
24512 editor,
24513 cx,
24514 );
24515 });
24516 cx.update_editor(|editor, window, cx| {
24517 editor.handle_input(".", window, cx);
24518 });
24519 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24520 cx.update_editor(|editor, _, cx| {
24521 set_linked_edit_ranges(
24522 (Point::new(0, 1), Point::new(0, 10)),
24523 (Point::new(0, 13), Point::new(0, 21)),
24524 editor,
24525 cx,
24526 );
24527 });
24528 cx.update_editor(|editor, window, cx| {
24529 editor.handle_input("V", window, cx);
24530 });
24531 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24532}
24533
24534#[gpui::test]
24535async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24536 init_test(cx, |_| {});
24537
24538 let fs = FakeFs::new(cx.executor());
24539 fs.insert_tree(
24540 path!("/root"),
24541 json!({
24542 "a": {
24543 "main.rs": "fn main() {}",
24544 },
24545 "foo": {
24546 "bar": {
24547 "external_file.rs": "pub mod external {}",
24548 }
24549 }
24550 }),
24551 )
24552 .await;
24553
24554 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24555 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24556 language_registry.add(rust_lang());
24557 let _fake_servers = language_registry.register_fake_lsp(
24558 "Rust",
24559 FakeLspAdapter {
24560 ..FakeLspAdapter::default()
24561 },
24562 );
24563 let (workspace, cx) =
24564 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24565 let worktree_id = workspace.update(cx, |workspace, cx| {
24566 workspace.project().update(cx, |project, cx| {
24567 project.worktrees(cx).next().unwrap().read(cx).id()
24568 })
24569 });
24570
24571 let assert_language_servers_count =
24572 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24573 project.update(cx, |project, cx| {
24574 let current = project
24575 .lsp_store()
24576 .read(cx)
24577 .as_local()
24578 .unwrap()
24579 .language_servers
24580 .len();
24581 assert_eq!(expected, current, "{context}");
24582 });
24583 };
24584
24585 assert_language_servers_count(
24586 0,
24587 "No servers should be running before any file is open",
24588 cx,
24589 );
24590 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24591 let main_editor = workspace
24592 .update_in(cx, |workspace, window, cx| {
24593 workspace.open_path(
24594 (worktree_id, rel_path("main.rs")),
24595 Some(pane.downgrade()),
24596 true,
24597 window,
24598 cx,
24599 )
24600 })
24601 .unwrap()
24602 .await
24603 .downcast::<Editor>()
24604 .unwrap();
24605 pane.update(cx, |pane, cx| {
24606 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24607 open_editor.update(cx, |editor, cx| {
24608 assert_eq!(
24609 editor.display_text(cx),
24610 "fn main() {}",
24611 "Original main.rs text on initial open",
24612 );
24613 });
24614 assert_eq!(open_editor, main_editor);
24615 });
24616 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24617
24618 let external_editor = workspace
24619 .update_in(cx, |workspace, window, cx| {
24620 workspace.open_abs_path(
24621 PathBuf::from("/root/foo/bar/external_file.rs"),
24622 OpenOptions::default(),
24623 window,
24624 cx,
24625 )
24626 })
24627 .await
24628 .expect("opening external file")
24629 .downcast::<Editor>()
24630 .expect("downcasted external file's open element to editor");
24631 pane.update(cx, |pane, cx| {
24632 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24633 open_editor.update(cx, |editor, cx| {
24634 assert_eq!(
24635 editor.display_text(cx),
24636 "pub mod external {}",
24637 "External file is open now",
24638 );
24639 });
24640 assert_eq!(open_editor, external_editor);
24641 });
24642 assert_language_servers_count(
24643 1,
24644 "Second, external, *.rs file should join the existing server",
24645 cx,
24646 );
24647
24648 pane.update_in(cx, |pane, window, cx| {
24649 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24650 })
24651 .await
24652 .unwrap();
24653 pane.update_in(cx, |pane, window, cx| {
24654 pane.navigate_backward(&Default::default(), window, cx);
24655 });
24656 cx.run_until_parked();
24657 pane.update(cx, |pane, cx| {
24658 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24659 open_editor.update(cx, |editor, cx| {
24660 assert_eq!(
24661 editor.display_text(cx),
24662 "pub mod external {}",
24663 "External file is open now",
24664 );
24665 });
24666 });
24667 assert_language_servers_count(
24668 1,
24669 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24670 cx,
24671 );
24672
24673 cx.update(|_, cx| {
24674 workspace::reload(cx);
24675 });
24676 assert_language_servers_count(
24677 1,
24678 "After reloading the worktree with local and external files opened, only one project should be started",
24679 cx,
24680 );
24681}
24682
24683#[gpui::test]
24684async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24685 init_test(cx, |_| {});
24686
24687 let mut cx = EditorTestContext::new(cx).await;
24688 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24689 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24690
24691 // test cursor move to start of each line on tab
24692 // for `if`, `elif`, `else`, `while`, `with` and `for`
24693 cx.set_state(indoc! {"
24694 def main():
24695 ˇ for item in items:
24696 ˇ while item.active:
24697 ˇ if item.value > 10:
24698 ˇ continue
24699 ˇ elif item.value < 0:
24700 ˇ break
24701 ˇ else:
24702 ˇ with item.context() as ctx:
24703 ˇ yield count
24704 ˇ else:
24705 ˇ log('while else')
24706 ˇ else:
24707 ˇ log('for else')
24708 "});
24709 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24710 cx.assert_editor_state(indoc! {"
24711 def main():
24712 ˇfor item in items:
24713 ˇwhile item.active:
24714 ˇif item.value > 10:
24715 ˇcontinue
24716 ˇelif item.value < 0:
24717 ˇbreak
24718 ˇelse:
24719 ˇwith item.context() as ctx:
24720 ˇyield count
24721 ˇelse:
24722 ˇlog('while else')
24723 ˇelse:
24724 ˇlog('for else')
24725 "});
24726 // test relative indent is preserved when tab
24727 // for `if`, `elif`, `else`, `while`, `with` and `for`
24728 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24729 cx.assert_editor_state(indoc! {"
24730 def main():
24731 ˇfor item in items:
24732 ˇwhile item.active:
24733 ˇif item.value > 10:
24734 ˇcontinue
24735 ˇelif item.value < 0:
24736 ˇbreak
24737 ˇelse:
24738 ˇwith item.context() as ctx:
24739 ˇyield count
24740 ˇelse:
24741 ˇlog('while else')
24742 ˇelse:
24743 ˇlog('for else')
24744 "});
24745
24746 // test cursor move to start of each line on tab
24747 // for `try`, `except`, `else`, `finally`, `match` and `def`
24748 cx.set_state(indoc! {"
24749 def main():
24750 ˇ try:
24751 ˇ fetch()
24752 ˇ except ValueError:
24753 ˇ handle_error()
24754 ˇ else:
24755 ˇ match value:
24756 ˇ case _:
24757 ˇ finally:
24758 ˇ def status():
24759 ˇ return 0
24760 "});
24761 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24762 cx.assert_editor_state(indoc! {"
24763 def main():
24764 ˇtry:
24765 ˇfetch()
24766 ˇexcept ValueError:
24767 ˇhandle_error()
24768 ˇelse:
24769 ˇmatch value:
24770 ˇcase _:
24771 ˇfinally:
24772 ˇdef status():
24773 ˇreturn 0
24774 "});
24775 // test relative indent is preserved when tab
24776 // for `try`, `except`, `else`, `finally`, `match` and `def`
24777 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24778 cx.assert_editor_state(indoc! {"
24779 def main():
24780 ˇtry:
24781 ˇfetch()
24782 ˇexcept ValueError:
24783 ˇhandle_error()
24784 ˇelse:
24785 ˇmatch value:
24786 ˇcase _:
24787 ˇfinally:
24788 ˇdef status():
24789 ˇreturn 0
24790 "});
24791}
24792
24793#[gpui::test]
24794async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24795 init_test(cx, |_| {});
24796
24797 let mut cx = EditorTestContext::new(cx).await;
24798 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24799 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24800
24801 // test `else` auto outdents when typed inside `if` block
24802 cx.set_state(indoc! {"
24803 def main():
24804 if i == 2:
24805 return
24806 ˇ
24807 "});
24808 cx.update_editor(|editor, window, cx| {
24809 editor.handle_input("else:", window, cx);
24810 });
24811 cx.assert_editor_state(indoc! {"
24812 def main():
24813 if i == 2:
24814 return
24815 else:ˇ
24816 "});
24817
24818 // test `except` auto outdents when typed inside `try` block
24819 cx.set_state(indoc! {"
24820 def main():
24821 try:
24822 i = 2
24823 ˇ
24824 "});
24825 cx.update_editor(|editor, window, cx| {
24826 editor.handle_input("except:", window, cx);
24827 });
24828 cx.assert_editor_state(indoc! {"
24829 def main():
24830 try:
24831 i = 2
24832 except:ˇ
24833 "});
24834
24835 // test `else` auto outdents when typed inside `except` block
24836 cx.set_state(indoc! {"
24837 def main():
24838 try:
24839 i = 2
24840 except:
24841 j = 2
24842 ˇ
24843 "});
24844 cx.update_editor(|editor, window, cx| {
24845 editor.handle_input("else:", window, cx);
24846 });
24847 cx.assert_editor_state(indoc! {"
24848 def main():
24849 try:
24850 i = 2
24851 except:
24852 j = 2
24853 else:ˇ
24854 "});
24855
24856 // test `finally` auto outdents when typed inside `else` block
24857 cx.set_state(indoc! {"
24858 def main():
24859 try:
24860 i = 2
24861 except:
24862 j = 2
24863 else:
24864 k = 2
24865 ˇ
24866 "});
24867 cx.update_editor(|editor, window, cx| {
24868 editor.handle_input("finally:", window, cx);
24869 });
24870 cx.assert_editor_state(indoc! {"
24871 def main():
24872 try:
24873 i = 2
24874 except:
24875 j = 2
24876 else:
24877 k = 2
24878 finally:ˇ
24879 "});
24880
24881 // test `else` does not outdents when typed inside `except` block right after for block
24882 cx.set_state(indoc! {"
24883 def main():
24884 try:
24885 i = 2
24886 except:
24887 for i in range(n):
24888 pass
24889 ˇ
24890 "});
24891 cx.update_editor(|editor, window, cx| {
24892 editor.handle_input("else:", window, cx);
24893 });
24894 cx.assert_editor_state(indoc! {"
24895 def main():
24896 try:
24897 i = 2
24898 except:
24899 for i in range(n):
24900 pass
24901 else:ˇ
24902 "});
24903
24904 // test `finally` auto outdents when typed inside `else` block right after for block
24905 cx.set_state(indoc! {"
24906 def main():
24907 try:
24908 i = 2
24909 except:
24910 j = 2
24911 else:
24912 for i in range(n):
24913 pass
24914 ˇ
24915 "});
24916 cx.update_editor(|editor, window, cx| {
24917 editor.handle_input("finally:", window, cx);
24918 });
24919 cx.assert_editor_state(indoc! {"
24920 def main():
24921 try:
24922 i = 2
24923 except:
24924 j = 2
24925 else:
24926 for i in range(n):
24927 pass
24928 finally:ˇ
24929 "});
24930
24931 // test `except` outdents to inner "try" block
24932 cx.set_state(indoc! {"
24933 def main():
24934 try:
24935 i = 2
24936 if i == 2:
24937 try:
24938 i = 3
24939 ˇ
24940 "});
24941 cx.update_editor(|editor, window, cx| {
24942 editor.handle_input("except:", window, cx);
24943 });
24944 cx.assert_editor_state(indoc! {"
24945 def main():
24946 try:
24947 i = 2
24948 if i == 2:
24949 try:
24950 i = 3
24951 except:ˇ
24952 "});
24953
24954 // test `except` outdents to outer "try" block
24955 cx.set_state(indoc! {"
24956 def main():
24957 try:
24958 i = 2
24959 if i == 2:
24960 try:
24961 i = 3
24962 ˇ
24963 "});
24964 cx.update_editor(|editor, window, cx| {
24965 editor.handle_input("except:", window, cx);
24966 });
24967 cx.assert_editor_state(indoc! {"
24968 def main():
24969 try:
24970 i = 2
24971 if i == 2:
24972 try:
24973 i = 3
24974 except:ˇ
24975 "});
24976
24977 // test `else` stays at correct indent when typed after `for` block
24978 cx.set_state(indoc! {"
24979 def main():
24980 for i in range(10):
24981 if i == 3:
24982 break
24983 ˇ
24984 "});
24985 cx.update_editor(|editor, window, cx| {
24986 editor.handle_input("else:", window, cx);
24987 });
24988 cx.assert_editor_state(indoc! {"
24989 def main():
24990 for i in range(10):
24991 if i == 3:
24992 break
24993 else:ˇ
24994 "});
24995
24996 // test does not outdent on typing after line with square brackets
24997 cx.set_state(indoc! {"
24998 def f() -> list[str]:
24999 ˇ
25000 "});
25001 cx.update_editor(|editor, window, cx| {
25002 editor.handle_input("a", window, cx);
25003 });
25004 cx.assert_editor_state(indoc! {"
25005 def f() -> list[str]:
25006 aˇ
25007 "});
25008
25009 // test does not outdent on typing : after case keyword
25010 cx.set_state(indoc! {"
25011 match 1:
25012 caseˇ
25013 "});
25014 cx.update_editor(|editor, window, cx| {
25015 editor.handle_input(":", window, cx);
25016 });
25017 cx.assert_editor_state(indoc! {"
25018 match 1:
25019 case:ˇ
25020 "});
25021}
25022
25023#[gpui::test]
25024async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25025 init_test(cx, |_| {});
25026 update_test_language_settings(cx, |settings| {
25027 settings.defaults.extend_comment_on_newline = Some(false);
25028 });
25029 let mut cx = EditorTestContext::new(cx).await;
25030 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25031 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25032
25033 // test correct indent after newline on comment
25034 cx.set_state(indoc! {"
25035 # COMMENT:ˇ
25036 "});
25037 cx.update_editor(|editor, window, cx| {
25038 editor.newline(&Newline, window, cx);
25039 });
25040 cx.assert_editor_state(indoc! {"
25041 # COMMENT:
25042 ˇ
25043 "});
25044
25045 // test correct indent after newline in brackets
25046 cx.set_state(indoc! {"
25047 {ˇ}
25048 "});
25049 cx.update_editor(|editor, window, cx| {
25050 editor.newline(&Newline, window, cx);
25051 });
25052 cx.run_until_parked();
25053 cx.assert_editor_state(indoc! {"
25054 {
25055 ˇ
25056 }
25057 "});
25058
25059 cx.set_state(indoc! {"
25060 (ˇ)
25061 "});
25062 cx.update_editor(|editor, window, cx| {
25063 editor.newline(&Newline, window, cx);
25064 });
25065 cx.run_until_parked();
25066 cx.assert_editor_state(indoc! {"
25067 (
25068 ˇ
25069 )
25070 "});
25071
25072 // do not indent after empty lists or dictionaries
25073 cx.set_state(indoc! {"
25074 a = []ˇ
25075 "});
25076 cx.update_editor(|editor, window, cx| {
25077 editor.newline(&Newline, window, cx);
25078 });
25079 cx.run_until_parked();
25080 cx.assert_editor_state(indoc! {"
25081 a = []
25082 ˇ
25083 "});
25084}
25085
25086#[gpui::test]
25087async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25088 init_test(cx, |_| {});
25089
25090 let mut cx = EditorTestContext::new(cx).await;
25091 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25092 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25093
25094 // test cursor move to start of each line on tab
25095 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25096 cx.set_state(indoc! {"
25097 function main() {
25098 ˇ for item in $items; do
25099 ˇ while [ -n \"$item\" ]; do
25100 ˇ if [ \"$value\" -gt 10 ]; then
25101 ˇ continue
25102 ˇ elif [ \"$value\" -lt 0 ]; then
25103 ˇ break
25104 ˇ else
25105 ˇ echo \"$item\"
25106 ˇ fi
25107 ˇ done
25108 ˇ done
25109 ˇ}
25110 "});
25111 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25112 cx.assert_editor_state(indoc! {"
25113 function main() {
25114 ˇfor item in $items; do
25115 ˇwhile [ -n \"$item\" ]; do
25116 ˇif [ \"$value\" -gt 10 ]; then
25117 ˇcontinue
25118 ˇelif [ \"$value\" -lt 0 ]; then
25119 ˇbreak
25120 ˇelse
25121 ˇecho \"$item\"
25122 ˇfi
25123 ˇdone
25124 ˇdone
25125 ˇ}
25126 "});
25127 // test relative indent is preserved when tab
25128 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25129 cx.assert_editor_state(indoc! {"
25130 function main() {
25131 ˇfor item in $items; do
25132 ˇwhile [ -n \"$item\" ]; do
25133 ˇif [ \"$value\" -gt 10 ]; then
25134 ˇcontinue
25135 ˇelif [ \"$value\" -lt 0 ]; then
25136 ˇbreak
25137 ˇelse
25138 ˇecho \"$item\"
25139 ˇfi
25140 ˇdone
25141 ˇdone
25142 ˇ}
25143 "});
25144
25145 // test cursor move to start of each line on tab
25146 // for `case` statement with patterns
25147 cx.set_state(indoc! {"
25148 function handle() {
25149 ˇ case \"$1\" in
25150 ˇ start)
25151 ˇ echo \"a\"
25152 ˇ ;;
25153 ˇ stop)
25154 ˇ echo \"b\"
25155 ˇ ;;
25156 ˇ *)
25157 ˇ echo \"c\"
25158 ˇ ;;
25159 ˇ esac
25160 ˇ}
25161 "});
25162 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25163 cx.assert_editor_state(indoc! {"
25164 function handle() {
25165 ˇcase \"$1\" in
25166 ˇstart)
25167 ˇecho \"a\"
25168 ˇ;;
25169 ˇstop)
25170 ˇecho \"b\"
25171 ˇ;;
25172 ˇ*)
25173 ˇecho \"c\"
25174 ˇ;;
25175 ˇesac
25176 ˇ}
25177 "});
25178}
25179
25180#[gpui::test]
25181async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25182 init_test(cx, |_| {});
25183
25184 let mut cx = EditorTestContext::new(cx).await;
25185 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25186 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25187
25188 // test indents on comment insert
25189 cx.set_state(indoc! {"
25190 function main() {
25191 ˇ for item in $items; do
25192 ˇ while [ -n \"$item\" ]; do
25193 ˇ if [ \"$value\" -gt 10 ]; then
25194 ˇ continue
25195 ˇ elif [ \"$value\" -lt 0 ]; then
25196 ˇ break
25197 ˇ else
25198 ˇ echo \"$item\"
25199 ˇ fi
25200 ˇ done
25201 ˇ done
25202 ˇ}
25203 "});
25204 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25205 cx.assert_editor_state(indoc! {"
25206 function main() {
25207 #ˇ for item in $items; do
25208 #ˇ while [ -n \"$item\" ]; do
25209 #ˇ if [ \"$value\" -gt 10 ]; then
25210 #ˇ continue
25211 #ˇ elif [ \"$value\" -lt 0 ]; then
25212 #ˇ break
25213 #ˇ else
25214 #ˇ echo \"$item\"
25215 #ˇ fi
25216 #ˇ done
25217 #ˇ done
25218 #ˇ}
25219 "});
25220}
25221
25222#[gpui::test]
25223async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25224 init_test(cx, |_| {});
25225
25226 let mut cx = EditorTestContext::new(cx).await;
25227 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25228 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25229
25230 // test `else` auto outdents when typed inside `if` block
25231 cx.set_state(indoc! {"
25232 if [ \"$1\" = \"test\" ]; then
25233 echo \"foo bar\"
25234 ˇ
25235 "});
25236 cx.update_editor(|editor, window, cx| {
25237 editor.handle_input("else", window, cx);
25238 });
25239 cx.assert_editor_state(indoc! {"
25240 if [ \"$1\" = \"test\" ]; then
25241 echo \"foo bar\"
25242 elseˇ
25243 "});
25244
25245 // test `elif` auto outdents when typed inside `if` block
25246 cx.set_state(indoc! {"
25247 if [ \"$1\" = \"test\" ]; then
25248 echo \"foo bar\"
25249 ˇ
25250 "});
25251 cx.update_editor(|editor, window, cx| {
25252 editor.handle_input("elif", window, cx);
25253 });
25254 cx.assert_editor_state(indoc! {"
25255 if [ \"$1\" = \"test\" ]; then
25256 echo \"foo bar\"
25257 elifˇ
25258 "});
25259
25260 // test `fi` auto outdents when typed inside `else` block
25261 cx.set_state(indoc! {"
25262 if [ \"$1\" = \"test\" ]; then
25263 echo \"foo bar\"
25264 else
25265 echo \"bar baz\"
25266 ˇ
25267 "});
25268 cx.update_editor(|editor, window, cx| {
25269 editor.handle_input("fi", window, cx);
25270 });
25271 cx.assert_editor_state(indoc! {"
25272 if [ \"$1\" = \"test\" ]; then
25273 echo \"foo bar\"
25274 else
25275 echo \"bar baz\"
25276 fiˇ
25277 "});
25278
25279 // test `done` auto outdents when typed inside `while` block
25280 cx.set_state(indoc! {"
25281 while read line; do
25282 echo \"$line\"
25283 ˇ
25284 "});
25285 cx.update_editor(|editor, window, cx| {
25286 editor.handle_input("done", window, cx);
25287 });
25288 cx.assert_editor_state(indoc! {"
25289 while read line; do
25290 echo \"$line\"
25291 doneˇ
25292 "});
25293
25294 // test `done` auto outdents when typed inside `for` block
25295 cx.set_state(indoc! {"
25296 for file in *.txt; do
25297 cat \"$file\"
25298 ˇ
25299 "});
25300 cx.update_editor(|editor, window, cx| {
25301 editor.handle_input("done", window, cx);
25302 });
25303 cx.assert_editor_state(indoc! {"
25304 for file in *.txt; do
25305 cat \"$file\"
25306 doneˇ
25307 "});
25308
25309 // test `esac` auto outdents when typed inside `case` block
25310 cx.set_state(indoc! {"
25311 case \"$1\" in
25312 start)
25313 echo \"foo bar\"
25314 ;;
25315 stop)
25316 echo \"bar baz\"
25317 ;;
25318 ˇ
25319 "});
25320 cx.update_editor(|editor, window, cx| {
25321 editor.handle_input("esac", window, cx);
25322 });
25323 cx.assert_editor_state(indoc! {"
25324 case \"$1\" in
25325 start)
25326 echo \"foo bar\"
25327 ;;
25328 stop)
25329 echo \"bar baz\"
25330 ;;
25331 esacˇ
25332 "});
25333
25334 // test `*)` auto outdents when typed inside `case` block
25335 cx.set_state(indoc! {"
25336 case \"$1\" in
25337 start)
25338 echo \"foo bar\"
25339 ;;
25340 ˇ
25341 "});
25342 cx.update_editor(|editor, window, cx| {
25343 editor.handle_input("*)", window, cx);
25344 });
25345 cx.assert_editor_state(indoc! {"
25346 case \"$1\" in
25347 start)
25348 echo \"foo bar\"
25349 ;;
25350 *)ˇ
25351 "});
25352
25353 // test `fi` outdents to correct level with nested if blocks
25354 cx.set_state(indoc! {"
25355 if [ \"$1\" = \"test\" ]; then
25356 echo \"outer if\"
25357 if [ \"$2\" = \"debug\" ]; then
25358 echo \"inner if\"
25359 ˇ
25360 "});
25361 cx.update_editor(|editor, window, cx| {
25362 editor.handle_input("fi", window, cx);
25363 });
25364 cx.assert_editor_state(indoc! {"
25365 if [ \"$1\" = \"test\" ]; then
25366 echo \"outer if\"
25367 if [ \"$2\" = \"debug\" ]; then
25368 echo \"inner if\"
25369 fiˇ
25370 "});
25371}
25372
25373#[gpui::test]
25374async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25375 init_test(cx, |_| {});
25376 update_test_language_settings(cx, |settings| {
25377 settings.defaults.extend_comment_on_newline = Some(false);
25378 });
25379 let mut cx = EditorTestContext::new(cx).await;
25380 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25381 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25382
25383 // test correct indent after newline on comment
25384 cx.set_state(indoc! {"
25385 # COMMENT:ˇ
25386 "});
25387 cx.update_editor(|editor, window, cx| {
25388 editor.newline(&Newline, window, cx);
25389 });
25390 cx.assert_editor_state(indoc! {"
25391 # COMMENT:
25392 ˇ
25393 "});
25394
25395 // test correct indent after newline after `then`
25396 cx.set_state(indoc! {"
25397
25398 if [ \"$1\" = \"test\" ]; thenˇ
25399 "});
25400 cx.update_editor(|editor, window, cx| {
25401 editor.newline(&Newline, window, cx);
25402 });
25403 cx.run_until_parked();
25404 cx.assert_editor_state(indoc! {"
25405
25406 if [ \"$1\" = \"test\" ]; then
25407 ˇ
25408 "});
25409
25410 // test correct indent after newline after `else`
25411 cx.set_state(indoc! {"
25412 if [ \"$1\" = \"test\" ]; then
25413 elseˇ
25414 "});
25415 cx.update_editor(|editor, window, cx| {
25416 editor.newline(&Newline, window, cx);
25417 });
25418 cx.run_until_parked();
25419 cx.assert_editor_state(indoc! {"
25420 if [ \"$1\" = \"test\" ]; then
25421 else
25422 ˇ
25423 "});
25424
25425 // test correct indent after newline after `elif`
25426 cx.set_state(indoc! {"
25427 if [ \"$1\" = \"test\" ]; then
25428 elifˇ
25429 "});
25430 cx.update_editor(|editor, window, cx| {
25431 editor.newline(&Newline, window, cx);
25432 });
25433 cx.run_until_parked();
25434 cx.assert_editor_state(indoc! {"
25435 if [ \"$1\" = \"test\" ]; then
25436 elif
25437 ˇ
25438 "});
25439
25440 // test correct indent after newline after `do`
25441 cx.set_state(indoc! {"
25442 for file in *.txt; doˇ
25443 "});
25444 cx.update_editor(|editor, window, cx| {
25445 editor.newline(&Newline, window, cx);
25446 });
25447 cx.run_until_parked();
25448 cx.assert_editor_state(indoc! {"
25449 for file in *.txt; do
25450 ˇ
25451 "});
25452
25453 // test correct indent after newline after case pattern
25454 cx.set_state(indoc! {"
25455 case \"$1\" in
25456 start)ˇ
25457 "});
25458 cx.update_editor(|editor, window, cx| {
25459 editor.newline(&Newline, window, cx);
25460 });
25461 cx.run_until_parked();
25462 cx.assert_editor_state(indoc! {"
25463 case \"$1\" in
25464 start)
25465 ˇ
25466 "});
25467
25468 // test correct indent after newline after case pattern
25469 cx.set_state(indoc! {"
25470 case \"$1\" in
25471 start)
25472 ;;
25473 *)ˇ
25474 "});
25475 cx.update_editor(|editor, window, cx| {
25476 editor.newline(&Newline, window, cx);
25477 });
25478 cx.run_until_parked();
25479 cx.assert_editor_state(indoc! {"
25480 case \"$1\" in
25481 start)
25482 ;;
25483 *)
25484 ˇ
25485 "});
25486
25487 // test correct indent after newline after function opening brace
25488 cx.set_state(indoc! {"
25489 function test() {ˇ}
25490 "});
25491 cx.update_editor(|editor, window, cx| {
25492 editor.newline(&Newline, window, cx);
25493 });
25494 cx.run_until_parked();
25495 cx.assert_editor_state(indoc! {"
25496 function test() {
25497 ˇ
25498 }
25499 "});
25500
25501 // test no extra indent after semicolon on same line
25502 cx.set_state(indoc! {"
25503 echo \"test\";ˇ
25504 "});
25505 cx.update_editor(|editor, window, cx| {
25506 editor.newline(&Newline, window, cx);
25507 });
25508 cx.run_until_parked();
25509 cx.assert_editor_state(indoc! {"
25510 echo \"test\";
25511 ˇ
25512 "});
25513}
25514
25515fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25516 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25517 point..point
25518}
25519
25520#[track_caller]
25521fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25522 let (text, ranges) = marked_text_ranges(marked_text, true);
25523 assert_eq!(editor.text(cx), text);
25524 assert_eq!(
25525 editor.selections.ranges(&editor.display_snapshot(cx)),
25526 ranges,
25527 "Assert selections are {}",
25528 marked_text
25529 );
25530}
25531
25532pub fn handle_signature_help_request(
25533 cx: &mut EditorLspTestContext,
25534 mocked_response: lsp::SignatureHelp,
25535) -> impl Future<Output = ()> + use<> {
25536 let mut request =
25537 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25538 let mocked_response = mocked_response.clone();
25539 async move { Ok(Some(mocked_response)) }
25540 });
25541
25542 async move {
25543 request.next().await;
25544 }
25545}
25546
25547#[track_caller]
25548pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25549 cx.update_editor(|editor, _, _| {
25550 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25551 let entries = menu.entries.borrow();
25552 let entries = entries
25553 .iter()
25554 .map(|entry| entry.string.as_str())
25555 .collect::<Vec<_>>();
25556 assert_eq!(entries, expected);
25557 } else {
25558 panic!("Expected completions menu");
25559 }
25560 });
25561}
25562
25563/// Handle completion request passing a marked string specifying where the completion
25564/// should be triggered from using '|' character, what range should be replaced, and what completions
25565/// should be returned using '<' and '>' to delimit the range.
25566///
25567/// Also see `handle_completion_request_with_insert_and_replace`.
25568#[track_caller]
25569pub fn handle_completion_request(
25570 marked_string: &str,
25571 completions: Vec<&'static str>,
25572 is_incomplete: bool,
25573 counter: Arc<AtomicUsize>,
25574 cx: &mut EditorLspTestContext,
25575) -> impl Future<Output = ()> {
25576 let complete_from_marker: TextRangeMarker = '|'.into();
25577 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25578 let (_, mut marked_ranges) = marked_text_ranges_by(
25579 marked_string,
25580 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25581 );
25582
25583 let complete_from_position =
25584 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25585 let replace_range =
25586 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25587
25588 let mut request =
25589 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25590 let completions = completions.clone();
25591 counter.fetch_add(1, atomic::Ordering::Release);
25592 async move {
25593 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25594 assert_eq!(
25595 params.text_document_position.position,
25596 complete_from_position
25597 );
25598 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25599 is_incomplete,
25600 item_defaults: None,
25601 items: completions
25602 .iter()
25603 .map(|completion_text| lsp::CompletionItem {
25604 label: completion_text.to_string(),
25605 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25606 range: replace_range,
25607 new_text: completion_text.to_string(),
25608 })),
25609 ..Default::default()
25610 })
25611 .collect(),
25612 })))
25613 }
25614 });
25615
25616 async move {
25617 request.next().await;
25618 }
25619}
25620
25621/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25622/// given instead, which also contains an `insert` range.
25623///
25624/// This function uses markers to define ranges:
25625/// - `|` marks the cursor position
25626/// - `<>` marks the replace range
25627/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25628pub fn handle_completion_request_with_insert_and_replace(
25629 cx: &mut EditorLspTestContext,
25630 marked_string: &str,
25631 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25632 counter: Arc<AtomicUsize>,
25633) -> impl Future<Output = ()> {
25634 let complete_from_marker: TextRangeMarker = '|'.into();
25635 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25636 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25637
25638 let (_, mut marked_ranges) = marked_text_ranges_by(
25639 marked_string,
25640 vec![
25641 complete_from_marker.clone(),
25642 replace_range_marker.clone(),
25643 insert_range_marker.clone(),
25644 ],
25645 );
25646
25647 let complete_from_position =
25648 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25649 let replace_range =
25650 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25651
25652 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25653 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25654 _ => lsp::Range {
25655 start: replace_range.start,
25656 end: complete_from_position,
25657 },
25658 };
25659
25660 let mut request =
25661 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25662 let completions = completions.clone();
25663 counter.fetch_add(1, atomic::Ordering::Release);
25664 async move {
25665 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25666 assert_eq!(
25667 params.text_document_position.position, complete_from_position,
25668 "marker `|` position doesn't match",
25669 );
25670 Ok(Some(lsp::CompletionResponse::Array(
25671 completions
25672 .iter()
25673 .map(|(label, new_text)| lsp::CompletionItem {
25674 label: label.to_string(),
25675 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25676 lsp::InsertReplaceEdit {
25677 insert: insert_range,
25678 replace: replace_range,
25679 new_text: new_text.to_string(),
25680 },
25681 )),
25682 ..Default::default()
25683 })
25684 .collect(),
25685 )))
25686 }
25687 });
25688
25689 async move {
25690 request.next().await;
25691 }
25692}
25693
25694fn handle_resolve_completion_request(
25695 cx: &mut EditorLspTestContext,
25696 edits: Option<Vec<(&'static str, &'static str)>>,
25697) -> impl Future<Output = ()> {
25698 let edits = edits.map(|edits| {
25699 edits
25700 .iter()
25701 .map(|(marked_string, new_text)| {
25702 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25703 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25704 lsp::TextEdit::new(replace_range, new_text.to_string())
25705 })
25706 .collect::<Vec<_>>()
25707 });
25708
25709 let mut request =
25710 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25711 let edits = edits.clone();
25712 async move {
25713 Ok(lsp::CompletionItem {
25714 additional_text_edits: edits,
25715 ..Default::default()
25716 })
25717 }
25718 });
25719
25720 async move {
25721 request.next().await;
25722 }
25723}
25724
25725pub(crate) fn update_test_language_settings(
25726 cx: &mut TestAppContext,
25727 f: impl Fn(&mut AllLanguageSettingsContent),
25728) {
25729 cx.update(|cx| {
25730 SettingsStore::update_global(cx, |store, cx| {
25731 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25732 });
25733 });
25734}
25735
25736pub(crate) fn update_test_project_settings(
25737 cx: &mut TestAppContext,
25738 f: impl Fn(&mut ProjectSettingsContent),
25739) {
25740 cx.update(|cx| {
25741 SettingsStore::update_global(cx, |store, cx| {
25742 store.update_user_settings(cx, |settings| f(&mut settings.project));
25743 });
25744 });
25745}
25746
25747pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25748 cx.update(|cx| {
25749 assets::Assets.load_test_fonts(cx);
25750 let store = SettingsStore::test(cx);
25751 cx.set_global(store);
25752 theme::init(theme::LoadThemes::JustBase, cx);
25753 release_channel::init(SemanticVersion::default(), cx);
25754 client::init_settings(cx);
25755 language::init(cx);
25756 Project::init_settings(cx);
25757 workspace::init_settings(cx);
25758 crate::init(cx);
25759 });
25760 zlog::init_test();
25761 update_test_language_settings(cx, f);
25762}
25763
25764#[track_caller]
25765fn assert_hunk_revert(
25766 not_reverted_text_with_selections: &str,
25767 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25768 expected_reverted_text_with_selections: &str,
25769 base_text: &str,
25770 cx: &mut EditorLspTestContext,
25771) {
25772 cx.set_state(not_reverted_text_with_selections);
25773 cx.set_head_text(base_text);
25774 cx.executor().run_until_parked();
25775
25776 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25777 let snapshot = editor.snapshot(window, cx);
25778 let reverted_hunk_statuses = snapshot
25779 .buffer_snapshot()
25780 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25781 .map(|hunk| hunk.status().kind)
25782 .collect::<Vec<_>>();
25783
25784 editor.git_restore(&Default::default(), window, cx);
25785 reverted_hunk_statuses
25786 });
25787 cx.executor().run_until_parked();
25788 cx.assert_editor_state(expected_reverted_text_with_selections);
25789 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25790}
25791
25792#[gpui::test(iterations = 10)]
25793async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25794 init_test(cx, |_| {});
25795
25796 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25797 let counter = diagnostic_requests.clone();
25798
25799 let fs = FakeFs::new(cx.executor());
25800 fs.insert_tree(
25801 path!("/a"),
25802 json!({
25803 "first.rs": "fn main() { let a = 5; }",
25804 "second.rs": "// Test file",
25805 }),
25806 )
25807 .await;
25808
25809 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25810 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25811 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25812
25813 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25814 language_registry.add(rust_lang());
25815 let mut fake_servers = language_registry.register_fake_lsp(
25816 "Rust",
25817 FakeLspAdapter {
25818 capabilities: lsp::ServerCapabilities {
25819 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25820 lsp::DiagnosticOptions {
25821 identifier: None,
25822 inter_file_dependencies: true,
25823 workspace_diagnostics: true,
25824 work_done_progress_options: Default::default(),
25825 },
25826 )),
25827 ..Default::default()
25828 },
25829 ..Default::default()
25830 },
25831 );
25832
25833 let editor = workspace
25834 .update(cx, |workspace, window, cx| {
25835 workspace.open_abs_path(
25836 PathBuf::from(path!("/a/first.rs")),
25837 OpenOptions::default(),
25838 window,
25839 cx,
25840 )
25841 })
25842 .unwrap()
25843 .await
25844 .unwrap()
25845 .downcast::<Editor>()
25846 .unwrap();
25847 let fake_server = fake_servers.next().await.unwrap();
25848 let server_id = fake_server.server.server_id();
25849 let mut first_request = fake_server
25850 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25851 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25852 let result_id = Some(new_result_id.to_string());
25853 assert_eq!(
25854 params.text_document.uri,
25855 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25856 );
25857 async move {
25858 Ok(lsp::DocumentDiagnosticReportResult::Report(
25859 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25860 related_documents: None,
25861 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25862 items: Vec::new(),
25863 result_id,
25864 },
25865 }),
25866 ))
25867 }
25868 });
25869
25870 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25871 project.update(cx, |project, cx| {
25872 let buffer_id = editor
25873 .read(cx)
25874 .buffer()
25875 .read(cx)
25876 .as_singleton()
25877 .expect("created a singleton buffer")
25878 .read(cx)
25879 .remote_id();
25880 let buffer_result_id = project
25881 .lsp_store()
25882 .read(cx)
25883 .result_id(server_id, buffer_id, cx);
25884 assert_eq!(expected, buffer_result_id);
25885 });
25886 };
25887
25888 ensure_result_id(None, cx);
25889 cx.executor().advance_clock(Duration::from_millis(60));
25890 cx.executor().run_until_parked();
25891 assert_eq!(
25892 diagnostic_requests.load(atomic::Ordering::Acquire),
25893 1,
25894 "Opening file should trigger diagnostic request"
25895 );
25896 first_request
25897 .next()
25898 .await
25899 .expect("should have sent the first diagnostics pull request");
25900 ensure_result_id(Some("1".to_string()), cx);
25901
25902 // Editing should trigger diagnostics
25903 editor.update_in(cx, |editor, window, cx| {
25904 editor.handle_input("2", window, cx)
25905 });
25906 cx.executor().advance_clock(Duration::from_millis(60));
25907 cx.executor().run_until_parked();
25908 assert_eq!(
25909 diagnostic_requests.load(atomic::Ordering::Acquire),
25910 2,
25911 "Editing should trigger diagnostic request"
25912 );
25913 ensure_result_id(Some("2".to_string()), cx);
25914
25915 // Moving cursor should not trigger diagnostic request
25916 editor.update_in(cx, |editor, window, cx| {
25917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25918 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25919 });
25920 });
25921 cx.executor().advance_clock(Duration::from_millis(60));
25922 cx.executor().run_until_parked();
25923 assert_eq!(
25924 diagnostic_requests.load(atomic::Ordering::Acquire),
25925 2,
25926 "Cursor movement should not trigger diagnostic request"
25927 );
25928 ensure_result_id(Some("2".to_string()), cx);
25929 // Multiple rapid edits should be debounced
25930 for _ in 0..5 {
25931 editor.update_in(cx, |editor, window, cx| {
25932 editor.handle_input("x", window, cx)
25933 });
25934 }
25935 cx.executor().advance_clock(Duration::from_millis(60));
25936 cx.executor().run_until_parked();
25937
25938 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25939 assert!(
25940 final_requests <= 4,
25941 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25942 );
25943 ensure_result_id(Some(final_requests.to_string()), cx);
25944}
25945
25946#[gpui::test]
25947async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25948 // Regression test for issue #11671
25949 // Previously, adding a cursor after moving multiple cursors would reset
25950 // the cursor count instead of adding to the existing cursors.
25951 init_test(cx, |_| {});
25952 let mut cx = EditorTestContext::new(cx).await;
25953
25954 // Create a simple buffer with cursor at start
25955 cx.set_state(indoc! {"
25956 ˇaaaa
25957 bbbb
25958 cccc
25959 dddd
25960 eeee
25961 ffff
25962 gggg
25963 hhhh"});
25964
25965 // Add 2 cursors below (so we have 3 total)
25966 cx.update_editor(|editor, window, cx| {
25967 editor.add_selection_below(&Default::default(), window, cx);
25968 editor.add_selection_below(&Default::default(), window, cx);
25969 });
25970
25971 // Verify we have 3 cursors
25972 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25973 assert_eq!(
25974 initial_count, 3,
25975 "Should have 3 cursors after adding 2 below"
25976 );
25977
25978 // Move down one line
25979 cx.update_editor(|editor, window, cx| {
25980 editor.move_down(&MoveDown, window, cx);
25981 });
25982
25983 // Add another cursor below
25984 cx.update_editor(|editor, window, cx| {
25985 editor.add_selection_below(&Default::default(), window, cx);
25986 });
25987
25988 // Should now have 4 cursors (3 original + 1 new)
25989 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25990 assert_eq!(
25991 final_count, 4,
25992 "Should have 4 cursors after moving and adding another"
25993 );
25994}
25995
25996#[gpui::test]
25997async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25998 init_test(cx, |_| {});
25999
26000 let mut cx = EditorTestContext::new(cx).await;
26001
26002 cx.set_state(indoc!(
26003 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26004 Second line here"#
26005 ));
26006
26007 cx.update_editor(|editor, window, cx| {
26008 // Enable soft wrapping with a narrow width to force soft wrapping and
26009 // confirm that more than 2 rows are being displayed.
26010 editor.set_wrap_width(Some(100.0.into()), cx);
26011 assert!(editor.display_text(cx).lines().count() > 2);
26012
26013 editor.add_selection_below(
26014 &AddSelectionBelow {
26015 skip_soft_wrap: true,
26016 },
26017 window,
26018 cx,
26019 );
26020
26021 assert_eq!(
26022 editor.selections.display_ranges(cx),
26023 &[
26024 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26025 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26026 ]
26027 );
26028
26029 editor.add_selection_above(
26030 &AddSelectionAbove {
26031 skip_soft_wrap: true,
26032 },
26033 window,
26034 cx,
26035 );
26036
26037 assert_eq!(
26038 editor.selections.display_ranges(cx),
26039 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26040 );
26041
26042 editor.add_selection_below(
26043 &AddSelectionBelow {
26044 skip_soft_wrap: false,
26045 },
26046 window,
26047 cx,
26048 );
26049
26050 assert_eq!(
26051 editor.selections.display_ranges(cx),
26052 &[
26053 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26054 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26055 ]
26056 );
26057
26058 editor.add_selection_above(
26059 &AddSelectionAbove {
26060 skip_soft_wrap: false,
26061 },
26062 window,
26063 cx,
26064 );
26065
26066 assert_eq!(
26067 editor.selections.display_ranges(cx),
26068 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26069 );
26070 });
26071}
26072
26073#[gpui::test(iterations = 10)]
26074async fn test_document_colors(cx: &mut TestAppContext) {
26075 let expected_color = Rgba {
26076 r: 0.33,
26077 g: 0.33,
26078 b: 0.33,
26079 a: 0.33,
26080 };
26081
26082 init_test(cx, |_| {});
26083
26084 let fs = FakeFs::new(cx.executor());
26085 fs.insert_tree(
26086 path!("/a"),
26087 json!({
26088 "first.rs": "fn main() { let a = 5; }",
26089 }),
26090 )
26091 .await;
26092
26093 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26094 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26095 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26096
26097 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26098 language_registry.add(rust_lang());
26099 let mut fake_servers = language_registry.register_fake_lsp(
26100 "Rust",
26101 FakeLspAdapter {
26102 capabilities: lsp::ServerCapabilities {
26103 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26104 ..lsp::ServerCapabilities::default()
26105 },
26106 name: "rust-analyzer",
26107 ..FakeLspAdapter::default()
26108 },
26109 );
26110 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26111 "Rust",
26112 FakeLspAdapter {
26113 capabilities: lsp::ServerCapabilities {
26114 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26115 ..lsp::ServerCapabilities::default()
26116 },
26117 name: "not-rust-analyzer",
26118 ..FakeLspAdapter::default()
26119 },
26120 );
26121
26122 let editor = workspace
26123 .update(cx, |workspace, window, cx| {
26124 workspace.open_abs_path(
26125 PathBuf::from(path!("/a/first.rs")),
26126 OpenOptions::default(),
26127 window,
26128 cx,
26129 )
26130 })
26131 .unwrap()
26132 .await
26133 .unwrap()
26134 .downcast::<Editor>()
26135 .unwrap();
26136 let fake_language_server = fake_servers.next().await.unwrap();
26137 let fake_language_server_without_capabilities =
26138 fake_servers_without_capabilities.next().await.unwrap();
26139 let requests_made = Arc::new(AtomicUsize::new(0));
26140 let closure_requests_made = Arc::clone(&requests_made);
26141 let mut color_request_handle = fake_language_server
26142 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26143 let requests_made = Arc::clone(&closure_requests_made);
26144 async move {
26145 assert_eq!(
26146 params.text_document.uri,
26147 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26148 );
26149 requests_made.fetch_add(1, atomic::Ordering::Release);
26150 Ok(vec![
26151 lsp::ColorInformation {
26152 range: lsp::Range {
26153 start: lsp::Position {
26154 line: 0,
26155 character: 0,
26156 },
26157 end: lsp::Position {
26158 line: 0,
26159 character: 1,
26160 },
26161 },
26162 color: lsp::Color {
26163 red: 0.33,
26164 green: 0.33,
26165 blue: 0.33,
26166 alpha: 0.33,
26167 },
26168 },
26169 lsp::ColorInformation {
26170 range: lsp::Range {
26171 start: lsp::Position {
26172 line: 0,
26173 character: 0,
26174 },
26175 end: lsp::Position {
26176 line: 0,
26177 character: 1,
26178 },
26179 },
26180 color: lsp::Color {
26181 red: 0.33,
26182 green: 0.33,
26183 blue: 0.33,
26184 alpha: 0.33,
26185 },
26186 },
26187 ])
26188 }
26189 });
26190
26191 let _handle = fake_language_server_without_capabilities
26192 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26193 panic!("Should not be called");
26194 });
26195 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26196 color_request_handle.next().await.unwrap();
26197 cx.run_until_parked();
26198 assert_eq!(
26199 1,
26200 requests_made.load(atomic::Ordering::Acquire),
26201 "Should query for colors once per editor open"
26202 );
26203 editor.update_in(cx, |editor, _, cx| {
26204 assert_eq!(
26205 vec![expected_color],
26206 extract_color_inlays(editor, cx),
26207 "Should have an initial inlay"
26208 );
26209 });
26210
26211 // opening another file in a split should not influence the LSP query counter
26212 workspace
26213 .update(cx, |workspace, window, cx| {
26214 assert_eq!(
26215 workspace.panes().len(),
26216 1,
26217 "Should have one pane with one editor"
26218 );
26219 workspace.move_item_to_pane_in_direction(
26220 &MoveItemToPaneInDirection {
26221 direction: SplitDirection::Right,
26222 focus: false,
26223 clone: true,
26224 },
26225 window,
26226 cx,
26227 );
26228 })
26229 .unwrap();
26230 cx.run_until_parked();
26231 workspace
26232 .update(cx, |workspace, _, cx| {
26233 let panes = workspace.panes();
26234 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26235 for pane in panes {
26236 let editor = pane
26237 .read(cx)
26238 .active_item()
26239 .and_then(|item| item.downcast::<Editor>())
26240 .expect("Should have opened an editor in each split");
26241 let editor_file = editor
26242 .read(cx)
26243 .buffer()
26244 .read(cx)
26245 .as_singleton()
26246 .expect("test deals with singleton buffers")
26247 .read(cx)
26248 .file()
26249 .expect("test buffese should have a file")
26250 .path();
26251 assert_eq!(
26252 editor_file.as_ref(),
26253 rel_path("first.rs"),
26254 "Both editors should be opened for the same file"
26255 )
26256 }
26257 })
26258 .unwrap();
26259
26260 cx.executor().advance_clock(Duration::from_millis(500));
26261 let save = editor.update_in(cx, |editor, window, cx| {
26262 editor.move_to_end(&MoveToEnd, window, cx);
26263 editor.handle_input("dirty", window, cx);
26264 editor.save(
26265 SaveOptions {
26266 format: true,
26267 autosave: true,
26268 },
26269 project.clone(),
26270 window,
26271 cx,
26272 )
26273 });
26274 save.await.unwrap();
26275
26276 color_request_handle.next().await.unwrap();
26277 cx.run_until_parked();
26278 assert_eq!(
26279 2,
26280 requests_made.load(atomic::Ordering::Acquire),
26281 "Should query for colors once per save (deduplicated) and once per formatting after save"
26282 );
26283
26284 drop(editor);
26285 let close = workspace
26286 .update(cx, |workspace, window, cx| {
26287 workspace.active_pane().update(cx, |pane, cx| {
26288 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26289 })
26290 })
26291 .unwrap();
26292 close.await.unwrap();
26293 let close = workspace
26294 .update(cx, |workspace, window, cx| {
26295 workspace.active_pane().update(cx, |pane, cx| {
26296 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26297 })
26298 })
26299 .unwrap();
26300 close.await.unwrap();
26301 assert_eq!(
26302 2,
26303 requests_made.load(atomic::Ordering::Acquire),
26304 "After saving and closing all editors, no extra requests should be made"
26305 );
26306 workspace
26307 .update(cx, |workspace, _, cx| {
26308 assert!(
26309 workspace.active_item(cx).is_none(),
26310 "Should close all editors"
26311 )
26312 })
26313 .unwrap();
26314
26315 workspace
26316 .update(cx, |workspace, window, cx| {
26317 workspace.active_pane().update(cx, |pane, cx| {
26318 pane.navigate_backward(&workspace::GoBack, window, cx);
26319 })
26320 })
26321 .unwrap();
26322 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26323 cx.run_until_parked();
26324 let editor = workspace
26325 .update(cx, |workspace, _, cx| {
26326 workspace
26327 .active_item(cx)
26328 .expect("Should have reopened the editor again after navigating back")
26329 .downcast::<Editor>()
26330 .expect("Should be an editor")
26331 })
26332 .unwrap();
26333
26334 assert_eq!(
26335 2,
26336 requests_made.load(atomic::Ordering::Acquire),
26337 "Cache should be reused on buffer close and reopen"
26338 );
26339 editor.update(cx, |editor, cx| {
26340 assert_eq!(
26341 vec![expected_color],
26342 extract_color_inlays(editor, cx),
26343 "Should have an initial inlay"
26344 );
26345 });
26346
26347 drop(color_request_handle);
26348 let closure_requests_made = Arc::clone(&requests_made);
26349 let mut empty_color_request_handle = fake_language_server
26350 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26351 let requests_made = Arc::clone(&closure_requests_made);
26352 async move {
26353 assert_eq!(
26354 params.text_document.uri,
26355 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26356 );
26357 requests_made.fetch_add(1, atomic::Ordering::Release);
26358 Ok(Vec::new())
26359 }
26360 });
26361 let save = editor.update_in(cx, |editor, window, cx| {
26362 editor.move_to_end(&MoveToEnd, window, cx);
26363 editor.handle_input("dirty_again", window, cx);
26364 editor.save(
26365 SaveOptions {
26366 format: false,
26367 autosave: true,
26368 },
26369 project.clone(),
26370 window,
26371 cx,
26372 )
26373 });
26374 save.await.unwrap();
26375
26376 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26377 empty_color_request_handle.next().await.unwrap();
26378 cx.run_until_parked();
26379 assert_eq!(
26380 3,
26381 requests_made.load(atomic::Ordering::Acquire),
26382 "Should query for colors once per save only, as formatting was not requested"
26383 );
26384 editor.update(cx, |editor, cx| {
26385 assert_eq!(
26386 Vec::<Rgba>::new(),
26387 extract_color_inlays(editor, cx),
26388 "Should clear all colors when the server returns an empty response"
26389 );
26390 });
26391}
26392
26393#[gpui::test]
26394async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26395 init_test(cx, |_| {});
26396 let (editor, cx) = cx.add_window_view(Editor::single_line);
26397 editor.update_in(cx, |editor, window, cx| {
26398 editor.set_text("oops\n\nwow\n", window, cx)
26399 });
26400 cx.run_until_parked();
26401 editor.update(cx, |editor, cx| {
26402 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26403 });
26404 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26405 cx.run_until_parked();
26406 editor.update(cx, |editor, cx| {
26407 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26408 });
26409}
26410
26411#[gpui::test]
26412async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26413 init_test(cx, |_| {});
26414
26415 cx.update(|cx| {
26416 register_project_item::<Editor>(cx);
26417 });
26418
26419 let fs = FakeFs::new(cx.executor());
26420 fs.insert_tree("/root1", json!({})).await;
26421 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26422 .await;
26423
26424 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26425 let (workspace, cx) =
26426 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26427
26428 let worktree_id = project.update(cx, |project, cx| {
26429 project.worktrees(cx).next().unwrap().read(cx).id()
26430 });
26431
26432 let handle = workspace
26433 .update_in(cx, |workspace, window, cx| {
26434 let project_path = (worktree_id, rel_path("one.pdf"));
26435 workspace.open_path(project_path, None, true, window, cx)
26436 })
26437 .await
26438 .unwrap();
26439
26440 assert_eq!(
26441 handle.to_any().entity_type(),
26442 TypeId::of::<InvalidItemView>()
26443 );
26444}
26445
26446#[gpui::test]
26447async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26448 init_test(cx, |_| {});
26449
26450 let language = Arc::new(Language::new(
26451 LanguageConfig::default(),
26452 Some(tree_sitter_rust::LANGUAGE.into()),
26453 ));
26454
26455 // Test hierarchical sibling navigation
26456 let text = r#"
26457 fn outer() {
26458 if condition {
26459 let a = 1;
26460 }
26461 let b = 2;
26462 }
26463
26464 fn another() {
26465 let c = 3;
26466 }
26467 "#;
26468
26469 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26470 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26471 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26472
26473 // Wait for parsing to complete
26474 editor
26475 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26476 .await;
26477
26478 editor.update_in(cx, |editor, window, cx| {
26479 // Start by selecting "let a = 1;" inside the if block
26480 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26481 s.select_display_ranges([
26482 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26483 ]);
26484 });
26485
26486 let initial_selection = editor.selections.display_ranges(cx);
26487 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26488
26489 // Test select next sibling - should move up levels to find the next sibling
26490 // Since "let a = 1;" has no siblings in the if block, it should move up
26491 // to find "let b = 2;" which is a sibling of the if block
26492 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26493 let next_selection = editor.selections.display_ranges(cx);
26494
26495 // Should have a selection and it should be different from the initial
26496 assert_eq!(
26497 next_selection.len(),
26498 1,
26499 "Should have one selection after next"
26500 );
26501 assert_ne!(
26502 next_selection[0], initial_selection[0],
26503 "Next sibling selection should be different"
26504 );
26505
26506 // Test hierarchical navigation by going to the end of the current function
26507 // and trying to navigate to the next function
26508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26509 s.select_display_ranges([
26510 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26511 ]);
26512 });
26513
26514 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26515 let function_next_selection = editor.selections.display_ranges(cx);
26516
26517 // Should move to the next function
26518 assert_eq!(
26519 function_next_selection.len(),
26520 1,
26521 "Should have one selection after function next"
26522 );
26523
26524 // Test select previous sibling navigation
26525 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26526 let prev_selection = editor.selections.display_ranges(cx);
26527
26528 // Should have a selection and it should be different
26529 assert_eq!(
26530 prev_selection.len(),
26531 1,
26532 "Should have one selection after prev"
26533 );
26534 assert_ne!(
26535 prev_selection[0], function_next_selection[0],
26536 "Previous sibling selection should be different from next"
26537 );
26538 });
26539}
26540
26541#[gpui::test]
26542async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26543 init_test(cx, |_| {});
26544
26545 let mut cx = EditorTestContext::new(cx).await;
26546 cx.set_state(
26547 "let ˇvariable = 42;
26548let another = variable + 1;
26549let result = variable * 2;",
26550 );
26551
26552 // Set up document highlights manually (simulating LSP response)
26553 cx.update_editor(|editor, _window, cx| {
26554 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26555
26556 // Create highlights for "variable" occurrences
26557 let highlight_ranges = [
26558 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26559 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26560 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26561 ];
26562
26563 let anchor_ranges: Vec<_> = highlight_ranges
26564 .iter()
26565 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26566 .collect();
26567
26568 editor.highlight_background::<DocumentHighlightRead>(
26569 &anchor_ranges,
26570 |theme| theme.colors().editor_document_highlight_read_background,
26571 cx,
26572 );
26573 });
26574
26575 // Go to next highlight - should move to second "variable"
26576 cx.update_editor(|editor, window, cx| {
26577 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26578 });
26579 cx.assert_editor_state(
26580 "let variable = 42;
26581let another = ˇvariable + 1;
26582let result = variable * 2;",
26583 );
26584
26585 // Go to next highlight - should move to third "variable"
26586 cx.update_editor(|editor, window, cx| {
26587 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26588 });
26589 cx.assert_editor_state(
26590 "let variable = 42;
26591let another = variable + 1;
26592let result = ˇvariable * 2;",
26593 );
26594
26595 // Go to next highlight - should stay at third "variable" (no wrap-around)
26596 cx.update_editor(|editor, window, cx| {
26597 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26598 });
26599 cx.assert_editor_state(
26600 "let variable = 42;
26601let another = variable + 1;
26602let result = ˇvariable * 2;",
26603 );
26604
26605 // Now test going backwards from third position
26606 cx.update_editor(|editor, window, cx| {
26607 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26608 });
26609 cx.assert_editor_state(
26610 "let variable = 42;
26611let another = ˇvariable + 1;
26612let result = variable * 2;",
26613 );
26614
26615 // Go to previous highlight - should move to first "variable"
26616 cx.update_editor(|editor, window, cx| {
26617 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26618 });
26619 cx.assert_editor_state(
26620 "let ˇvariable = 42;
26621let another = variable + 1;
26622let result = variable * 2;",
26623 );
26624
26625 // Go to previous highlight - should stay on first "variable"
26626 cx.update_editor(|editor, window, cx| {
26627 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26628 });
26629 cx.assert_editor_state(
26630 "let ˇvariable = 42;
26631let another = variable + 1;
26632let result = variable * 2;",
26633 );
26634}
26635
26636#[gpui::test]
26637async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26638 cx: &mut gpui::TestAppContext,
26639) {
26640 init_test(cx, |_| {});
26641
26642 let url = "https://zed.dev";
26643
26644 let markdown_language = Arc::new(Language::new(
26645 LanguageConfig {
26646 name: "Markdown".into(),
26647 ..LanguageConfig::default()
26648 },
26649 None,
26650 ));
26651
26652 let mut cx = EditorTestContext::new(cx).await;
26653 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26654 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26655
26656 cx.update_editor(|editor, window, cx| {
26657 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26658 editor.paste(&Paste, window, cx);
26659 });
26660
26661 cx.assert_editor_state(&format!(
26662 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26663 ));
26664}
26665
26666#[gpui::test]
26667async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26668 cx: &mut gpui::TestAppContext,
26669) {
26670 init_test(cx, |_| {});
26671
26672 let url = "https://zed.dev";
26673
26674 let markdown_language = Arc::new(Language::new(
26675 LanguageConfig {
26676 name: "Markdown".into(),
26677 ..LanguageConfig::default()
26678 },
26679 None,
26680 ));
26681
26682 let mut cx = EditorTestContext::new(cx).await;
26683 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26684 cx.set_state(&format!(
26685 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26686 ));
26687
26688 cx.update_editor(|editor, window, cx| {
26689 editor.copy(&Copy, window, cx);
26690 });
26691
26692 cx.set_state(&format!(
26693 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26694 ));
26695
26696 cx.update_editor(|editor, window, cx| {
26697 editor.paste(&Paste, window, cx);
26698 });
26699
26700 cx.assert_editor_state(&format!(
26701 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26702 ));
26703}
26704
26705#[gpui::test]
26706async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26707 cx: &mut gpui::TestAppContext,
26708) {
26709 init_test(cx, |_| {});
26710
26711 let url = "https://zed.dev";
26712
26713 let markdown_language = Arc::new(Language::new(
26714 LanguageConfig {
26715 name: "Markdown".into(),
26716 ..LanguageConfig::default()
26717 },
26718 None,
26719 ));
26720
26721 let mut cx = EditorTestContext::new(cx).await;
26722 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26723 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26724
26725 cx.update_editor(|editor, window, cx| {
26726 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26727 editor.paste(&Paste, window, cx);
26728 });
26729
26730 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26731}
26732
26733#[gpui::test]
26734async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26735 cx: &mut gpui::TestAppContext,
26736) {
26737 init_test(cx, |_| {});
26738
26739 let text = "Awesome";
26740
26741 let markdown_language = Arc::new(Language::new(
26742 LanguageConfig {
26743 name: "Markdown".into(),
26744 ..LanguageConfig::default()
26745 },
26746 None,
26747 ));
26748
26749 let mut cx = EditorTestContext::new(cx).await;
26750 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26751 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26752
26753 cx.update_editor(|editor, window, cx| {
26754 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26755 editor.paste(&Paste, window, cx);
26756 });
26757
26758 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26759}
26760
26761#[gpui::test]
26762async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26763 cx: &mut gpui::TestAppContext,
26764) {
26765 init_test(cx, |_| {});
26766
26767 let url = "https://zed.dev";
26768
26769 let markdown_language = Arc::new(Language::new(
26770 LanguageConfig {
26771 name: "Rust".into(),
26772 ..LanguageConfig::default()
26773 },
26774 None,
26775 ));
26776
26777 let mut cx = EditorTestContext::new(cx).await;
26778 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26779 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26780
26781 cx.update_editor(|editor, window, cx| {
26782 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26783 editor.paste(&Paste, window, cx);
26784 });
26785
26786 cx.assert_editor_state(&format!(
26787 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26788 ));
26789}
26790
26791#[gpui::test]
26792async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26793 cx: &mut TestAppContext,
26794) {
26795 init_test(cx, |_| {});
26796
26797 let url = "https://zed.dev";
26798
26799 let markdown_language = Arc::new(Language::new(
26800 LanguageConfig {
26801 name: "Markdown".into(),
26802 ..LanguageConfig::default()
26803 },
26804 None,
26805 ));
26806
26807 let (editor, cx) = cx.add_window_view(|window, cx| {
26808 let multi_buffer = MultiBuffer::build_multi(
26809 [
26810 ("this will embed -> link", vec![Point::row_range(0..1)]),
26811 ("this will replace -> link", vec![Point::row_range(0..1)]),
26812 ],
26813 cx,
26814 );
26815 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26817 s.select_ranges(vec![
26818 Point::new(0, 19)..Point::new(0, 23),
26819 Point::new(1, 21)..Point::new(1, 25),
26820 ])
26821 });
26822 let first_buffer_id = multi_buffer
26823 .read(cx)
26824 .excerpt_buffer_ids()
26825 .into_iter()
26826 .next()
26827 .unwrap();
26828 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26829 first_buffer.update(cx, |buffer, cx| {
26830 buffer.set_language(Some(markdown_language.clone()), cx);
26831 });
26832
26833 editor
26834 });
26835 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26836
26837 cx.update_editor(|editor, window, cx| {
26838 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26839 editor.paste(&Paste, window, cx);
26840 });
26841
26842 cx.assert_editor_state(&format!(
26843 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26844 ));
26845}
26846
26847#[gpui::test]
26848async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26849 init_test(cx, |_| {});
26850
26851 let fs = FakeFs::new(cx.executor());
26852 fs.insert_tree(
26853 path!("/project"),
26854 json!({
26855 "first.rs": "# First Document\nSome content here.",
26856 "second.rs": "Plain text content for second file.",
26857 }),
26858 )
26859 .await;
26860
26861 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26862 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26863 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26864
26865 let language = rust_lang();
26866 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26867 language_registry.add(language.clone());
26868 let mut fake_servers = language_registry.register_fake_lsp(
26869 "Rust",
26870 FakeLspAdapter {
26871 ..FakeLspAdapter::default()
26872 },
26873 );
26874
26875 let buffer1 = project
26876 .update(cx, |project, cx| {
26877 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26878 })
26879 .await
26880 .unwrap();
26881 let buffer2 = project
26882 .update(cx, |project, cx| {
26883 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26884 })
26885 .await
26886 .unwrap();
26887
26888 let multi_buffer = cx.new(|cx| {
26889 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26890 multi_buffer.set_excerpts_for_path(
26891 PathKey::for_buffer(&buffer1, cx),
26892 buffer1.clone(),
26893 [Point::zero()..buffer1.read(cx).max_point()],
26894 3,
26895 cx,
26896 );
26897 multi_buffer.set_excerpts_for_path(
26898 PathKey::for_buffer(&buffer2, cx),
26899 buffer2.clone(),
26900 [Point::zero()..buffer1.read(cx).max_point()],
26901 3,
26902 cx,
26903 );
26904 multi_buffer
26905 });
26906
26907 let (editor, cx) = cx.add_window_view(|window, cx| {
26908 Editor::new(
26909 EditorMode::full(),
26910 multi_buffer,
26911 Some(project.clone()),
26912 window,
26913 cx,
26914 )
26915 });
26916
26917 let fake_language_server = fake_servers.next().await.unwrap();
26918
26919 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26920
26921 let save = editor.update_in(cx, |editor, window, cx| {
26922 assert!(editor.is_dirty(cx));
26923
26924 editor.save(
26925 SaveOptions {
26926 format: true,
26927 autosave: true,
26928 },
26929 project,
26930 window,
26931 cx,
26932 )
26933 });
26934 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26935 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26936 let mut done_edit_rx = Some(done_edit_rx);
26937 let mut start_edit_tx = Some(start_edit_tx);
26938
26939 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26940 start_edit_tx.take().unwrap().send(()).unwrap();
26941 let done_edit_rx = done_edit_rx.take().unwrap();
26942 async move {
26943 done_edit_rx.await.unwrap();
26944 Ok(None)
26945 }
26946 });
26947
26948 start_edit_rx.await.unwrap();
26949 buffer2
26950 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26951 .unwrap();
26952
26953 done_edit_tx.send(()).unwrap();
26954
26955 save.await.unwrap();
26956 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26957}
26958
26959#[track_caller]
26960fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26961 editor
26962 .all_inlays(cx)
26963 .into_iter()
26964 .filter_map(|inlay| inlay.get_color())
26965 .map(Rgba::from)
26966 .collect()
26967}
26968
26969#[gpui::test]
26970fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26971 init_test(cx, |_| {});
26972
26973 let editor = cx.add_window(|window, cx| {
26974 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26975 build_editor(buffer, window, cx)
26976 });
26977
26978 editor
26979 .update(cx, |editor, window, cx| {
26980 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26981 s.select_display_ranges([
26982 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26983 ])
26984 });
26985
26986 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26987
26988 assert_eq!(
26989 editor.display_text(cx),
26990 "line1\nline2\nline2",
26991 "Duplicating last line upward should create duplicate above, not on same line"
26992 );
26993
26994 assert_eq!(
26995 editor.selections.display_ranges(cx),
26996 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26997 "Selection should move to the duplicated line"
26998 );
26999 })
27000 .unwrap();
27001}
27002
27003#[gpui::test]
27004async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27005 init_test(cx, |_| {});
27006
27007 let mut cx = EditorTestContext::new(cx).await;
27008
27009 cx.set_state("line1\nline2ˇ");
27010
27011 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27012
27013 let clipboard_text = cx
27014 .read_from_clipboard()
27015 .and_then(|item| item.text().as_deref().map(str::to_string));
27016
27017 assert_eq!(
27018 clipboard_text,
27019 Some("line2\n".to_string()),
27020 "Copying a line without trailing newline should include a newline"
27021 );
27022
27023 cx.set_state("line1\nˇ");
27024
27025 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27026
27027 cx.assert_editor_state("line1\nline2\nˇ");
27028}
27029
27030#[gpui::test]
27031async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27032 init_test(cx, |_| {});
27033
27034 let mut cx = EditorTestContext::new(cx).await;
27035
27036 cx.set_state("line1\nline2ˇ");
27037 cx.update_editor(|e, window, cx| {
27038 e.set_mode(EditorMode::SingleLine);
27039 assert!(e.key_context(window, cx).contains("end_of_input"));
27040 });
27041 cx.set_state("ˇline1\nline2");
27042 cx.update_editor(|e, window, cx| {
27043 assert!(!e.key_context(window, cx).contains("end_of_input"));
27044 });
27045 cx.set_state("line1ˇ\nline2");
27046 cx.update_editor(|e, window, cx| {
27047 assert!(!e.key_context(window, cx).contains("end_of_input"));
27048 });
27049}
27050
27051#[gpui::test]
27052async fn test_next_prev_reference(cx: &mut TestAppContext) {
27053 const CYCLE_POSITIONS: &[&'static str] = &[
27054 indoc! {"
27055 fn foo() {
27056 let ˇabc = 123;
27057 let x = abc + 1;
27058 let y = abc + 2;
27059 let z = abc + 2;
27060 }
27061 "},
27062 indoc! {"
27063 fn foo() {
27064 let abc = 123;
27065 let x = ˇabc + 1;
27066 let y = abc + 2;
27067 let z = abc + 2;
27068 }
27069 "},
27070 indoc! {"
27071 fn foo() {
27072 let abc = 123;
27073 let x = abc + 1;
27074 let y = ˇabc + 2;
27075 let z = abc + 2;
27076 }
27077 "},
27078 indoc! {"
27079 fn foo() {
27080 let abc = 123;
27081 let x = abc + 1;
27082 let y = abc + 2;
27083 let z = ˇabc + 2;
27084 }
27085 "},
27086 ];
27087
27088 init_test(cx, |_| {});
27089
27090 let mut cx = EditorLspTestContext::new_rust(
27091 lsp::ServerCapabilities {
27092 references_provider: Some(lsp::OneOf::Left(true)),
27093 ..Default::default()
27094 },
27095 cx,
27096 )
27097 .await;
27098
27099 // importantly, the cursor is in the middle
27100 cx.set_state(indoc! {"
27101 fn foo() {
27102 let aˇbc = 123;
27103 let x = abc + 1;
27104 let y = abc + 2;
27105 let z = abc + 2;
27106 }
27107 "});
27108
27109 let reference_ranges = [
27110 lsp::Position::new(1, 8),
27111 lsp::Position::new(2, 12),
27112 lsp::Position::new(3, 12),
27113 lsp::Position::new(4, 12),
27114 ]
27115 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27116
27117 cx.lsp
27118 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27119 Ok(Some(
27120 reference_ranges
27121 .map(|range| lsp::Location {
27122 uri: params.text_document_position.text_document.uri.clone(),
27123 range,
27124 })
27125 .to_vec(),
27126 ))
27127 });
27128
27129 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27130 cx.update_editor(|editor, window, cx| {
27131 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27132 })
27133 .unwrap()
27134 .await
27135 .unwrap()
27136 };
27137
27138 _move(Direction::Next, 1, &mut cx).await;
27139 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27140
27141 _move(Direction::Next, 1, &mut cx).await;
27142 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27143
27144 _move(Direction::Next, 1, &mut cx).await;
27145 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27146
27147 // loops back to the start
27148 _move(Direction::Next, 1, &mut cx).await;
27149 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27150
27151 // loops back to the end
27152 _move(Direction::Prev, 1, &mut cx).await;
27153 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27154
27155 _move(Direction::Prev, 1, &mut cx).await;
27156 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27157
27158 _move(Direction::Prev, 1, &mut cx).await;
27159 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27160
27161 _move(Direction::Prev, 1, &mut cx).await;
27162 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27163
27164 _move(Direction::Next, 3, &mut cx).await;
27165 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27166
27167 _move(Direction::Prev, 2, &mut cx).await;
27168 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27169}