1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use languages::rust_lang;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::build_editor_with_project;
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_item_view::InvalidItemView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(
224 editor.selections.ranges(&editor.display_snapshot(cx)),
225 vec![4..4]
226 );
227
228 editor.start_transaction_at(now, window, cx);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([4..5])
231 });
232 editor.insert("e", window, cx);
233 editor.end_transaction_at(now, cx);
234 assert_eq!(editor.text(cx), "12cde6");
235 assert_eq!(
236 editor.selections.ranges(&editor.display_snapshot(cx)),
237 vec![5..5]
238 );
239
240 now += group_interval + Duration::from_millis(1);
241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
242 s.select_ranges([2..2])
243 });
244
245 // Simulate an edit in another editor
246 buffer.update(cx, |buffer, cx| {
247 buffer.start_transaction_at(now, cx);
248 buffer.edit([(0..1, "a")], None, cx);
249 buffer.edit([(1..1, "b")], None, cx);
250 buffer.end_transaction_at(now, cx);
251 });
252
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(
255 editor.selections.ranges(&editor.display_snapshot(cx)),
256 vec![3..3]
257 );
258
259 // Last transaction happened past the group interval in a different editor.
260 // Undo it individually and don't restore selections.
261 editor.undo(&Undo, window, cx);
262 assert_eq!(editor.text(cx), "12cde6");
263 assert_eq!(
264 editor.selections.ranges(&editor.display_snapshot(cx)),
265 vec![2..2]
266 );
267
268 // First two transactions happened within the group interval in this editor.
269 // Undo them together and restore selections.
270 editor.undo(&Undo, window, cx);
271 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
272 assert_eq!(editor.text(cx), "123456");
273 assert_eq!(
274 editor.selections.ranges(&editor.display_snapshot(cx)),
275 vec![0..0]
276 );
277
278 // Redo the first two transactions together.
279 editor.redo(&Redo, window, cx);
280 assert_eq!(editor.text(cx), "12cde6");
281 assert_eq!(
282 editor.selections.ranges(&editor.display_snapshot(cx)),
283 vec![5..5]
284 );
285
286 // Redo the last transaction on its own.
287 editor.redo(&Redo, window, cx);
288 assert_eq!(editor.text(cx), "ab2cde6");
289 assert_eq!(
290 editor.selections.ranges(&editor.display_snapshot(cx)),
291 vec![6..6]
292 );
293
294 // Test empty transactions.
295 editor.start_transaction_at(now, window, cx);
296 editor.end_transaction_at(now, cx);
297 editor.undo(&Undo, window, cx);
298 assert_eq!(editor.text(cx), "12cde6");
299 });
300}
301
302#[gpui::test]
303fn test_ime_composition(cx: &mut TestAppContext) {
304 init_test(cx, |_| {});
305
306 let buffer = cx.new(|cx| {
307 let mut buffer = language::Buffer::local("abcde", cx);
308 // Ensure automatic grouping doesn't occur.
309 buffer.set_group_interval(Duration::ZERO);
310 buffer
311 });
312
313 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
314 cx.add_window(|window, cx| {
315 let mut editor = build_editor(buffer.clone(), window, cx);
316
317 // Start a new IME composition.
318 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
319 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
320 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
321 assert_eq!(editor.text(cx), "äbcde");
322 assert_eq!(
323 editor.marked_text_ranges(cx),
324 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
325 );
326
327 // Finalize IME composition.
328 editor.replace_text_in_range(None, "ā", window, cx);
329 assert_eq!(editor.text(cx), "ābcde");
330 assert_eq!(editor.marked_text_ranges(cx), None);
331
332 // IME composition edits are grouped and are undone/redone at once.
333 editor.undo(&Default::default(), window, cx);
334 assert_eq!(editor.text(cx), "abcde");
335 assert_eq!(editor.marked_text_ranges(cx), None);
336 editor.redo(&Default::default(), window, cx);
337 assert_eq!(editor.text(cx), "ābcde");
338 assert_eq!(editor.marked_text_ranges(cx), None);
339
340 // Start a new IME composition.
341 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
342 assert_eq!(
343 editor.marked_text_ranges(cx),
344 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
345 );
346
347 // Undoing during an IME composition cancels it.
348 editor.undo(&Default::default(), window, cx);
349 assert_eq!(editor.text(cx), "ābcde");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
353 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
354 assert_eq!(editor.text(cx), "ābcdè");
355 assert_eq!(
356 editor.marked_text_ranges(cx),
357 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
358 );
359
360 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
361 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
362 assert_eq!(editor.text(cx), "ābcdę");
363 assert_eq!(editor.marked_text_ranges(cx), None);
364
365 // Start a new IME composition with multiple cursors.
366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
367 s.select_ranges([
368 OffsetUtf16(1)..OffsetUtf16(1),
369 OffsetUtf16(3)..OffsetUtf16(3),
370 OffsetUtf16(5)..OffsetUtf16(5),
371 ])
372 });
373 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
374 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
375 assert_eq!(
376 editor.marked_text_ranges(cx),
377 Some(vec![
378 OffsetUtf16(0)..OffsetUtf16(3),
379 OffsetUtf16(4)..OffsetUtf16(7),
380 OffsetUtf16(8)..OffsetUtf16(11)
381 ])
382 );
383
384 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
385 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
386 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
387 assert_eq!(
388 editor.marked_text_ranges(cx),
389 Some(vec![
390 OffsetUtf16(1)..OffsetUtf16(2),
391 OffsetUtf16(5)..OffsetUtf16(6),
392 OffsetUtf16(9)..OffsetUtf16(10)
393 ])
394 );
395
396 // Finalize IME composition with multiple cursors.
397 editor.replace_text_in_range(Some(9..10), "2", window, cx);
398 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
399 assert_eq!(editor.marked_text_ranges(cx), None);
400
401 editor
402 });
403}
404
405#[gpui::test]
406fn test_selection_with_mouse(cx: &mut TestAppContext) {
407 init_test(cx, |_| {});
408
409 let editor = cx.add_window(|window, cx| {
410 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
411 build_editor(buffer, window, cx)
412 });
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
416 });
417 assert_eq!(
418 editor
419 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
420 .unwrap(),
421 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
422 );
423
424 _ = editor.update(cx, |editor, window, cx| {
425 editor.update_selection(
426 DisplayPoint::new(DisplayRow(3), 3),
427 0,
428 gpui::Point::<f32>::default(),
429 window,
430 cx,
431 );
432 });
433
434 assert_eq!(
435 editor
436 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
437 .unwrap(),
438 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
439 );
440
441 _ = editor.update(cx, |editor, window, cx| {
442 editor.update_selection(
443 DisplayPoint::new(DisplayRow(1), 1),
444 0,
445 gpui::Point::<f32>::default(),
446 window,
447 cx,
448 );
449 });
450
451 assert_eq!(
452 editor
453 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
454 .unwrap(),
455 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
456 );
457
458 _ = editor.update(cx, |editor, window, cx| {
459 editor.end_selection(window, cx);
460 editor.update_selection(
461 DisplayPoint::new(DisplayRow(3), 3),
462 0,
463 gpui::Point::<f32>::default(),
464 window,
465 cx,
466 );
467 });
468
469 assert_eq!(
470 editor
471 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
472 .unwrap(),
473 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
478 editor.update_selection(
479 DisplayPoint::new(DisplayRow(0), 0),
480 0,
481 gpui::Point::<f32>::default(),
482 window,
483 cx,
484 );
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
493 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.end_selection(window, cx);
499 });
500
501 assert_eq!(
502 editor
503 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
504 .unwrap(),
505 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
506 );
507}
508
509#[gpui::test]
510fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
511 init_test(cx, |_| {});
512
513 let editor = cx.add_window(|window, cx| {
514 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
515 build_editor(buffer, window, cx)
516 });
517
518 _ = editor.update(cx, |editor, window, cx| {
519 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.end_selection(window, cx);
532 });
533
534 assert_eq!(
535 editor
536 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
537 .unwrap(),
538 [
539 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
540 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
541 ]
542 );
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.end_selection(window, cx);
550 });
551
552 assert_eq!(
553 editor
554 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
555 .unwrap(),
556 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
557 );
558}
559
560#[gpui::test]
561fn test_canceling_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575 });
576
577 _ = editor.update(cx, |editor, window, cx| {
578 editor.update_selection(
579 DisplayPoint::new(DisplayRow(3), 3),
580 0,
581 gpui::Point::<f32>::default(),
582 window,
583 cx,
584 );
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
588 );
589 });
590
591 _ = editor.update(cx, |editor, window, cx| {
592 editor.cancel(&Cancel, window, cx);
593 editor.update_selection(
594 DisplayPoint::new(DisplayRow(1), 1),
595 0,
596 gpui::Point::<f32>::default(),
597 window,
598 cx,
599 );
600 assert_eq!(
601 editor.selections.display_ranges(cx),
602 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
603 );
604 });
605}
606
607#[gpui::test]
608fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
609 init_test(cx, |_| {});
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
618 assert_eq!(
619 editor.selections.display_ranges(cx),
620 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
621 );
622
623 editor.move_down(&Default::default(), window, cx);
624 assert_eq!(
625 editor.selections.display_ranges(cx),
626 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
627 );
628
629 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
630 assert_eq!(
631 editor.selections.display_ranges(cx),
632 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
633 );
634
635 editor.move_up(&Default::default(), window, cx);
636 assert_eq!(
637 editor.selections.display_ranges(cx),
638 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
639 );
640 });
641}
642
643#[gpui::test]
644fn test_extending_selection(cx: &mut TestAppContext) {
645 init_test(cx, |_| {});
646
647 let editor = cx.add_window(|window, cx| {
648 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
649 build_editor(buffer, window, cx)
650 });
651
652 _ = editor.update(cx, |editor, window, cx| {
653 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
654 editor.end_selection(window, cx);
655 assert_eq!(
656 editor.selections.display_ranges(cx),
657 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
658 );
659
660 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 editor.selections.display_ranges(cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
670 assert_eq!(
671 editor.selections.display_ranges(cx),
672 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
673 );
674
675 editor.update_selection(
676 DisplayPoint::new(DisplayRow(0), 1),
677 0,
678 gpui::Point::<f32>::default(),
679 window,
680 cx,
681 );
682 editor.end_selection(window, cx);
683 assert_eq!(
684 editor.selections.display_ranges(cx),
685 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
686 );
687
688 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
689 editor.end_selection(window, cx);
690 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
691 editor.end_selection(window, cx);
692 assert_eq!(
693 editor.selections.display_ranges(cx),
694 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
695 );
696
697 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
698 assert_eq!(
699 editor.selections.display_ranges(cx),
700 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
701 );
702
703 editor.update_selection(
704 DisplayPoint::new(DisplayRow(0), 6),
705 0,
706 gpui::Point::<f32>::default(),
707 window,
708 cx,
709 );
710 assert_eq!(
711 editor.selections.display_ranges(cx),
712 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
713 );
714
715 editor.update_selection(
716 DisplayPoint::new(DisplayRow(0), 1),
717 0,
718 gpui::Point::<f32>::default(),
719 window,
720 cx,
721 );
722 editor.end_selection(window, cx);
723 assert_eq!(
724 editor.selections.display_ranges(cx),
725 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
726 );
727 });
728}
729
730#[gpui::test]
731fn test_clone(cx: &mut TestAppContext) {
732 init_test(cx, |_| {});
733
734 let (text, selection_ranges) = marked_text_ranges(
735 indoc! {"
736 one
737 two
738 threeˇ
739 four
740 fiveˇ
741 "},
742 true,
743 );
744
745 let editor = cx.add_window(|window, cx| {
746 let buffer = MultiBuffer::build_simple(&text, cx);
747 build_editor(buffer, window, cx)
748 });
749
750 _ = editor.update(cx, |editor, window, cx| {
751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
752 s.select_ranges(selection_ranges.clone())
753 });
754 editor.fold_creases(
755 vec![
756 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
757 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
758 ],
759 true,
760 window,
761 cx,
762 );
763 });
764
765 let cloned_editor = editor
766 .update(cx, |editor, _, cx| {
767 cx.open_window(Default::default(), |window, cx| {
768 cx.new(|cx| editor.clone(window, cx))
769 })
770 })
771 .unwrap()
772 .unwrap();
773
774 let snapshot = editor
775 .update(cx, |e, window, cx| e.snapshot(window, cx))
776 .unwrap();
777 let cloned_snapshot = cloned_editor
778 .update(cx, |e, window, cx| e.snapshot(window, cx))
779 .unwrap();
780
781 assert_eq!(
782 cloned_editor
783 .update(cx, |e, _, cx| e.display_text(cx))
784 .unwrap(),
785 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
786 );
787 assert_eq!(
788 cloned_snapshot
789 .folds_in_range(0..text.len())
790 .collect::<Vec<_>>(),
791 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
792 );
793 assert_set_eq!(
794 cloned_editor
795 .update(cx, |editor, _, cx| editor
796 .selections
797 .ranges::<Point>(&editor.display_snapshot(cx)))
798 .unwrap(),
799 editor
800 .update(cx, |editor, _, cx| editor
801 .selections
802 .ranges(&editor.display_snapshot(cx)))
803 .unwrap()
804 );
805 assert_set_eq!(
806 cloned_editor
807 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
808 .unwrap(),
809 editor
810 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
811 .unwrap()
812 );
813}
814
815#[gpui::test]
816async fn test_navigation_history(cx: &mut TestAppContext) {
817 init_test(cx, |_| {});
818
819 use workspace::item::Item;
820
821 let fs = FakeFs::new(cx.executor());
822 let project = Project::test(fs, [], cx).await;
823 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
824 let pane = workspace
825 .update(cx, |workspace, _, _| workspace.active_pane().clone())
826 .unwrap();
827
828 _ = workspace.update(cx, |_v, window, cx| {
829 cx.new(|cx| {
830 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
831 let mut editor = build_editor(buffer, window, cx);
832 let handle = cx.entity();
833 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
834
835 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
836 editor.nav_history.as_mut().unwrap().pop_backward(cx)
837 }
838
839 // Move the cursor a small distance.
840 // Nothing is added to the navigation history.
841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
842 s.select_display_ranges([
843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
844 ])
845 });
846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
847 s.select_display_ranges([
848 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
849 ])
850 });
851 assert!(pop_history(&mut editor, cx).is_none());
852
853 // Move the cursor a large distance.
854 // The history can jump back to the previous position.
855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
856 s.select_display_ranges([
857 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
858 ])
859 });
860 let nav_entry = pop_history(&mut editor, cx).unwrap();
861 editor.navigate(nav_entry.data.unwrap(), window, cx);
862 assert_eq!(nav_entry.item.id(), cx.entity_id());
863 assert_eq!(
864 editor.selections.display_ranges(cx),
865 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
866 );
867 assert!(pop_history(&mut editor, cx).is_none());
868
869 // Move the cursor a small distance via the mouse.
870 // Nothing is added to the navigation history.
871 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
872 editor.end_selection(window, cx);
873 assert_eq!(
874 editor.selections.display_ranges(cx),
875 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
876 );
877 assert!(pop_history(&mut editor, cx).is_none());
878
879 // Move the cursor a large distance via the mouse.
880 // The history can jump back to the previous position.
881 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
882 editor.end_selection(window, cx);
883 assert_eq!(
884 editor.selections.display_ranges(cx),
885 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
886 );
887 let nav_entry = pop_history(&mut editor, cx).unwrap();
888 editor.navigate(nav_entry.data.unwrap(), window, cx);
889 assert_eq!(nav_entry.item.id(), cx.entity_id());
890 assert_eq!(
891 editor.selections.display_ranges(cx),
892 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
893 );
894 assert!(pop_history(&mut editor, cx).is_none());
895
896 // Set scroll position to check later
897 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
898 let original_scroll_position = editor.scroll_manager.anchor();
899
900 // Jump to the end of the document and adjust scroll
901 editor.move_to_end(&MoveToEnd, window, cx);
902 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
903 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
904
905 let nav_entry = pop_history(&mut editor, cx).unwrap();
906 editor.navigate(nav_entry.data.unwrap(), window, cx);
907 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
908
909 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
910 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
911 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
912 let invalid_point = Point::new(9999, 0);
913 editor.navigate(
914 Box::new(NavigationData {
915 cursor_anchor: invalid_anchor,
916 cursor_position: invalid_point,
917 scroll_anchor: ScrollAnchor {
918 anchor: invalid_anchor,
919 offset: Default::default(),
920 },
921 scroll_top_row: invalid_point.row,
922 }),
923 window,
924 cx,
925 );
926 assert_eq!(
927 editor.selections.display_ranges(cx),
928 &[editor.max_point(cx)..editor.max_point(cx)]
929 );
930 assert_eq!(
931 editor.scroll_position(cx),
932 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
933 );
934
935 editor
936 })
937 });
938}
939
940#[gpui::test]
941fn test_cancel(cx: &mut TestAppContext) {
942 init_test(cx, |_| {});
943
944 let editor = cx.add_window(|window, cx| {
945 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
946 build_editor(buffer, window, cx)
947 });
948
949 _ = editor.update(cx, |editor, window, cx| {
950 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
951 editor.update_selection(
952 DisplayPoint::new(DisplayRow(1), 1),
953 0,
954 gpui::Point::<f32>::default(),
955 window,
956 cx,
957 );
958 editor.end_selection(window, cx);
959
960 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
961 editor.update_selection(
962 DisplayPoint::new(DisplayRow(0), 3),
963 0,
964 gpui::Point::<f32>::default(),
965 window,
966 cx,
967 );
968 editor.end_selection(window, cx);
969 assert_eq!(
970 editor.selections.display_ranges(cx),
971 [
972 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
973 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
974 ]
975 );
976 });
977
978 _ = editor.update(cx, |editor, window, cx| {
979 editor.cancel(&Cancel, window, cx);
980 assert_eq!(
981 editor.selections.display_ranges(cx),
982 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
983 );
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.cancel(&Cancel, window, cx);
988 assert_eq!(
989 editor.selections.display_ranges(cx),
990 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
991 );
992 });
993}
994
995#[gpui::test]
996fn test_fold_action(cx: &mut TestAppContext) {
997 init_test(cx, |_| {});
998
999 let editor = cx.add_window(|window, cx| {
1000 let buffer = MultiBuffer::build_simple(
1001 &"
1002 impl Foo {
1003 // Hello!
1004
1005 fn a() {
1006 1
1007 }
1008
1009 fn b() {
1010 2
1011 }
1012
1013 fn c() {
1014 3
1015 }
1016 }
1017 "
1018 .unindent(),
1019 cx,
1020 );
1021 build_editor(buffer, window, cx)
1022 });
1023
1024 _ = editor.update(cx, |editor, window, cx| {
1025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1026 s.select_display_ranges([
1027 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1028 ]);
1029 });
1030 editor.fold(&Fold, window, cx);
1031 assert_eq!(
1032 editor.display_text(cx),
1033 "
1034 impl Foo {
1035 // Hello!
1036
1037 fn a() {
1038 1
1039 }
1040
1041 fn b() {⋯
1042 }
1043
1044 fn c() {⋯
1045 }
1046 }
1047 "
1048 .unindent(),
1049 );
1050
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {⋯
1056 }
1057 "
1058 .unindent(),
1059 );
1060
1061 editor.unfold_lines(&UnfoldLines, window, cx);
1062 assert_eq!(
1063 editor.display_text(cx),
1064 "
1065 impl Foo {
1066 // Hello!
1067
1068 fn a() {
1069 1
1070 }
1071
1072 fn b() {⋯
1073 }
1074
1075 fn c() {⋯
1076 }
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 editor.buffer.read(cx).read(cx).text()
1086 );
1087 });
1088}
1089
1090#[gpui::test]
1091fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1092 init_test(cx, |_| {});
1093
1094 let editor = cx.add_window(|window, cx| {
1095 let buffer = MultiBuffer::build_simple(
1096 &"
1097 class Foo:
1098 # Hello!
1099
1100 def a():
1101 print(1)
1102
1103 def b():
1104 print(2)
1105
1106 def c():
1107 print(3)
1108 "
1109 .unindent(),
1110 cx,
1111 );
1112 build_editor(buffer, window, cx)
1113 });
1114
1115 _ = editor.update(cx, |editor, window, cx| {
1116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1117 s.select_display_ranges([
1118 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1119 ]);
1120 });
1121 editor.fold(&Fold, window, cx);
1122 assert_eq!(
1123 editor.display_text(cx),
1124 "
1125 class Foo:
1126 # Hello!
1127
1128 def a():
1129 print(1)
1130
1131 def b():⋯
1132
1133 def c():⋯
1134 "
1135 .unindent(),
1136 );
1137
1138 editor.fold(&Fold, window, cx);
1139 assert_eq!(
1140 editor.display_text(cx),
1141 "
1142 class Foo:⋯
1143 "
1144 .unindent(),
1145 );
1146
1147 editor.unfold_lines(&UnfoldLines, window, cx);
1148 assert_eq!(
1149 editor.display_text(cx),
1150 "
1151 class Foo:
1152 # Hello!
1153
1154 def a():
1155 print(1)
1156
1157 def b():⋯
1158
1159 def c():⋯
1160 "
1161 .unindent(),
1162 );
1163
1164 editor.unfold_lines(&UnfoldLines, window, cx);
1165 assert_eq!(
1166 editor.display_text(cx),
1167 editor.buffer.read(cx).read(cx).text()
1168 );
1169 });
1170}
1171
1172#[gpui::test]
1173fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1174 init_test(cx, |_| {});
1175
1176 let editor = cx.add_window(|window, cx| {
1177 let buffer = MultiBuffer::build_simple(
1178 &"
1179 class Foo:
1180 # Hello!
1181
1182 def a():
1183 print(1)
1184
1185 def b():
1186 print(2)
1187
1188
1189 def c():
1190 print(3)
1191
1192
1193 "
1194 .unindent(),
1195 cx,
1196 );
1197 build_editor(buffer, window, cx)
1198 });
1199
1200 _ = editor.update(cx, |editor, window, cx| {
1201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1202 s.select_display_ranges([
1203 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1204 ]);
1205 });
1206 editor.fold(&Fold, window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():⋯
1217
1218
1219 def c():⋯
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.fold(&Fold, window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:⋯
1231
1232
1233 "
1234 .unindent(),
1235 );
1236
1237 editor.unfold_lines(&UnfoldLines, window, cx);
1238 assert_eq!(
1239 editor.display_text(cx),
1240 "
1241 class Foo:
1242 # Hello!
1243
1244 def a():
1245 print(1)
1246
1247 def b():⋯
1248
1249
1250 def c():⋯
1251
1252
1253 "
1254 .unindent(),
1255 );
1256
1257 editor.unfold_lines(&UnfoldLines, window, cx);
1258 assert_eq!(
1259 editor.display_text(cx),
1260 editor.buffer.read(cx).read(cx).text()
1261 );
1262 });
1263}
1264
1265#[gpui::test]
1266fn test_fold_at_level(cx: &mut TestAppContext) {
1267 init_test(cx, |_| {});
1268
1269 let editor = cx.add_window(|window, cx| {
1270 let buffer = MultiBuffer::build_simple(
1271 &"
1272 class Foo:
1273 # Hello!
1274
1275 def a():
1276 print(1)
1277
1278 def b():
1279 print(2)
1280
1281
1282 class Bar:
1283 # World!
1284
1285 def a():
1286 print(1)
1287
1288 def b():
1289 print(2)
1290
1291
1292 "
1293 .unindent(),
1294 cx,
1295 );
1296 build_editor(buffer, window, cx)
1297 });
1298
1299 _ = editor.update(cx, |editor, window, cx| {
1300 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1301 assert_eq!(
1302 editor.display_text(cx),
1303 "
1304 class Foo:
1305 # Hello!
1306
1307 def a():⋯
1308
1309 def b():⋯
1310
1311
1312 class Bar:
1313 # World!
1314
1315 def a():⋯
1316
1317 def b():⋯
1318
1319
1320 "
1321 .unindent(),
1322 );
1323
1324 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1325 assert_eq!(
1326 editor.display_text(cx),
1327 "
1328 class Foo:⋯
1329
1330
1331 class Bar:⋯
1332
1333
1334 "
1335 .unindent(),
1336 );
1337
1338 editor.unfold_all(&UnfoldAll, window, cx);
1339 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1340 assert_eq!(
1341 editor.display_text(cx),
1342 "
1343 class Foo:
1344 # Hello!
1345
1346 def a():
1347 print(1)
1348
1349 def b():
1350 print(2)
1351
1352
1353 class Bar:
1354 # World!
1355
1356 def a():
1357 print(1)
1358
1359 def b():
1360 print(2)
1361
1362
1363 "
1364 .unindent(),
1365 );
1366
1367 assert_eq!(
1368 editor.display_text(cx),
1369 editor.buffer.read(cx).read(cx).text()
1370 );
1371 let (_, positions) = marked_text_ranges(
1372 &"
1373 class Foo:
1374 # Hello!
1375
1376 def a():
1377 print(1)
1378
1379 def b():
1380 p«riˇ»nt(2)
1381
1382
1383 class Bar:
1384 # World!
1385
1386 def a():
1387 «ˇprint(1)
1388
1389 def b():
1390 print(2)»
1391
1392
1393 "
1394 .unindent(),
1395 true,
1396 );
1397
1398 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1399 s.select_ranges(positions)
1400 });
1401
1402 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1403 assert_eq!(
1404 editor.display_text(cx),
1405 "
1406 class Foo:
1407 # Hello!
1408
1409 def a():⋯
1410
1411 def b():
1412 print(2)
1413
1414
1415 class Bar:
1416 # World!
1417
1418 def a():
1419 print(1)
1420
1421 def b():
1422 print(2)
1423
1424
1425 "
1426 .unindent(),
1427 );
1428 });
1429}
1430
1431#[gpui::test]
1432fn test_move_cursor(cx: &mut TestAppContext) {
1433 init_test(cx, |_| {});
1434
1435 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1436 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1437
1438 buffer.update(cx, |buffer, cx| {
1439 buffer.edit(
1440 vec![
1441 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1442 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1443 ],
1444 None,
1445 cx,
1446 );
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1458 );
1459
1460 editor.move_right(&MoveRight, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1464 );
1465
1466 editor.move_left(&MoveLeft, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1470 );
1471
1472 editor.move_up(&MoveUp, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1476 );
1477
1478 editor.move_to_end(&MoveToEnd, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1482 );
1483
1484 editor.move_to_beginning(&MoveToBeginning, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1488 );
1489
1490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1491 s.select_display_ranges([
1492 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1493 ]);
1494 });
1495 editor.select_to_beginning(&SelectToBeginning, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1499 );
1500
1501 editor.select_to_end(&SelectToEnd, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1505 );
1506 });
1507}
1508
1509#[gpui::test]
1510fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1511 init_test(cx, |_| {});
1512
1513 let editor = cx.add_window(|window, cx| {
1514 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1515 build_editor(buffer, window, cx)
1516 });
1517
1518 assert_eq!('🟥'.len_utf8(), 4);
1519 assert_eq!('α'.len_utf8(), 2);
1520
1521 _ = editor.update(cx, |editor, window, cx| {
1522 editor.fold_creases(
1523 vec![
1524 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1525 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1526 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1527 ],
1528 true,
1529 window,
1530 cx,
1531 );
1532 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1533
1534 editor.move_right(&MoveRight, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(0, "🟥".len())]
1538 );
1539 editor.move_right(&MoveRight, window, cx);
1540 assert_eq!(
1541 editor.selections.display_ranges(cx),
1542 &[empty_range(0, "🟥🟧".len())]
1543 );
1544 editor.move_right(&MoveRight, window, cx);
1545 assert_eq!(
1546 editor.selections.display_ranges(cx),
1547 &[empty_range(0, "🟥🟧⋯".len())]
1548 );
1549
1550 editor.move_down(&MoveDown, window, cx);
1551 assert_eq!(
1552 editor.selections.display_ranges(cx),
1553 &[empty_range(1, "ab⋯e".len())]
1554 );
1555 editor.move_left(&MoveLeft, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[empty_range(1, "ab⋯".len())]
1559 );
1560 editor.move_left(&MoveLeft, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(1, "ab".len())]
1564 );
1565 editor.move_left(&MoveLeft, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[empty_range(1, "a".len())]
1569 );
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[empty_range(2, "α".len())]
1575 );
1576 editor.move_right(&MoveRight, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[empty_range(2, "αβ".len())]
1580 );
1581 editor.move_right(&MoveRight, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[empty_range(2, "αβ⋯".len())]
1585 );
1586 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(
1588 editor.selections.display_ranges(cx),
1589 &[empty_range(2, "αβ⋯ε".len())]
1590 );
1591
1592 editor.move_up(&MoveUp, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[empty_range(1, "ab⋯e".len())]
1596 );
1597 editor.move_down(&MoveDown, window, cx);
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[empty_range(2, "αβ⋯ε".len())]
1601 );
1602 editor.move_up(&MoveUp, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[empty_range(1, "ab⋯e".len())]
1606 );
1607
1608 editor.move_up(&MoveUp, window, cx);
1609 assert_eq!(
1610 editor.selections.display_ranges(cx),
1611 &[empty_range(0, "🟥🟧".len())]
1612 );
1613 editor.move_left(&MoveLeft, window, cx);
1614 assert_eq!(
1615 editor.selections.display_ranges(cx),
1616 &[empty_range(0, "🟥".len())]
1617 );
1618 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(
1620 editor.selections.display_ranges(cx),
1621 &[empty_range(0, "".len())]
1622 );
1623 });
1624}
1625
1626#[gpui::test]
1627fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1628 init_test(cx, |_| {});
1629
1630 let editor = cx.add_window(|window, cx| {
1631 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1632 build_editor(buffer, window, cx)
1633 });
1634 _ = editor.update(cx, |editor, window, cx| {
1635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1636 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1637 });
1638
1639 // moving above start of document should move selection to start of document,
1640 // but the next move down should still be at the original goal_x
1641 editor.move_up(&MoveUp, window, cx);
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[empty_range(0, "".len())]
1645 );
1646
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[empty_range(1, "abcd".len())]
1651 );
1652
1653 editor.move_down(&MoveDown, window, cx);
1654 assert_eq!(
1655 editor.selections.display_ranges(cx),
1656 &[empty_range(2, "αβγ".len())]
1657 );
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[empty_range(3, "abcd".len())]
1663 );
1664
1665 editor.move_down(&MoveDown, window, cx);
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1669 );
1670
1671 // moving past end of document should not change goal_x
1672 editor.move_down(&MoveDown, window, cx);
1673 assert_eq!(
1674 editor.selections.display_ranges(cx),
1675 &[empty_range(5, "".len())]
1676 );
1677
1678 editor.move_down(&MoveDown, window, cx);
1679 assert_eq!(
1680 editor.selections.display_ranges(cx),
1681 &[empty_range(5, "".len())]
1682 );
1683
1684 editor.move_up(&MoveUp, window, cx);
1685 assert_eq!(
1686 editor.selections.display_ranges(cx),
1687 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1688 );
1689
1690 editor.move_up(&MoveUp, window, cx);
1691 assert_eq!(
1692 editor.selections.display_ranges(cx),
1693 &[empty_range(3, "abcd".len())]
1694 );
1695
1696 editor.move_up(&MoveUp, window, cx);
1697 assert_eq!(
1698 editor.selections.display_ranges(cx),
1699 &[empty_range(2, "αβγ".len())]
1700 );
1701 });
1702}
1703
1704#[gpui::test]
1705fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1706 init_test(cx, |_| {});
1707 let move_to_beg = MoveToBeginningOfLine {
1708 stop_at_soft_wraps: true,
1709 stop_at_indent: true,
1710 };
1711
1712 let delete_to_beg = DeleteToBeginningOfLine {
1713 stop_at_indent: false,
1714 };
1715
1716 let move_to_end = MoveToEndOfLine {
1717 stop_at_soft_wraps: true,
1718 };
1719
1720 let editor = cx.add_window(|window, cx| {
1721 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1722 build_editor(buffer, window, cx)
1723 });
1724 _ = editor.update(cx, |editor, window, cx| {
1725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1726 s.select_display_ranges([
1727 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1728 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1729 ]);
1730 });
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1735 assert_eq!(
1736 editor.selections.display_ranges(cx),
1737 &[
1738 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1740 ]
1741 );
1742 });
1743
1744 _ = editor.update(cx, |editor, window, cx| {
1745 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1746 assert_eq!(
1747 editor.selections.display_ranges(cx),
1748 &[
1749 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1750 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1751 ]
1752 );
1753 });
1754
1755 _ = editor.update(cx, |editor, window, cx| {
1756 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1757 assert_eq!(
1758 editor.selections.display_ranges(cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.move_to_end_of_line(&move_to_end, window, cx);
1768 assert_eq!(
1769 editor.selections.display_ranges(cx),
1770 &[
1771 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1772 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1773 ]
1774 );
1775 });
1776
1777 // Moving to the end of line again is a no-op.
1778 _ = editor.update(cx, |editor, window, cx| {
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 editor.selections.display_ranges(cx),
1782 &[
1783 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1784 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1785 ]
1786 );
1787 });
1788
1789 _ = editor.update(cx, |editor, window, cx| {
1790 editor.move_left(&MoveLeft, window, cx);
1791 editor.select_to_beginning_of_line(
1792 &SelectToBeginningOfLine {
1793 stop_at_soft_wraps: true,
1794 stop_at_indent: true,
1795 },
1796 window,
1797 cx,
1798 );
1799 assert_eq!(
1800 editor.selections.display_ranges(cx),
1801 &[
1802 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1803 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1804 ]
1805 );
1806 });
1807
1808 _ = editor.update(cx, |editor, window, cx| {
1809 editor.select_to_beginning_of_line(
1810 &SelectToBeginningOfLine {
1811 stop_at_soft_wraps: true,
1812 stop_at_indent: true,
1813 },
1814 window,
1815 cx,
1816 );
1817 assert_eq!(
1818 editor.selections.display_ranges(cx),
1819 &[
1820 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1822 ]
1823 );
1824 });
1825
1826 _ = editor.update(cx, |editor, window, cx| {
1827 editor.select_to_beginning_of_line(
1828 &SelectToBeginningOfLine {
1829 stop_at_soft_wraps: true,
1830 stop_at_indent: true,
1831 },
1832 window,
1833 cx,
1834 );
1835 assert_eq!(
1836 editor.selections.display_ranges(cx),
1837 &[
1838 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1839 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1840 ]
1841 );
1842 });
1843
1844 _ = editor.update(cx, |editor, window, cx| {
1845 editor.select_to_end_of_line(
1846 &SelectToEndOfLine {
1847 stop_at_soft_wraps: true,
1848 },
1849 window,
1850 cx,
1851 );
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1856 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1857 ]
1858 );
1859 });
1860
1861 _ = editor.update(cx, |editor, window, cx| {
1862 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1863 assert_eq!(editor.display_text(cx), "ab\n de");
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1869 ]
1870 );
1871 });
1872
1873 _ = editor.update(cx, |editor, window, cx| {
1874 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1875 assert_eq!(editor.display_text(cx), "\n");
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883 });
1884}
1885
1886#[gpui::test]
1887fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1888 init_test(cx, |_| {});
1889 let move_to_beg = MoveToBeginningOfLine {
1890 stop_at_soft_wraps: false,
1891 stop_at_indent: false,
1892 };
1893
1894 let move_to_end = MoveToEndOfLine {
1895 stop_at_soft_wraps: false,
1896 };
1897
1898 let editor = cx.add_window(|window, cx| {
1899 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1900 build_editor(buffer, window, cx)
1901 });
1902
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.set_wrap_width(Some(140.0.into()), cx);
1905
1906 // We expect the following lines after wrapping
1907 // ```
1908 // thequickbrownfox
1909 // jumpedoverthelazydo
1910 // gs
1911 // ```
1912 // The final `gs` was soft-wrapped onto a new line.
1913 assert_eq!(
1914 "thequickbrownfox\njumpedoverthelaz\nydogs",
1915 editor.display_text(cx),
1916 );
1917
1918 // First, let's assert behavior on the first line, that was not soft-wrapped.
1919 // Start the cursor at the `k` on the first line
1920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1921 s.select_display_ranges([
1922 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1923 ]);
1924 });
1925
1926 // Moving to the beginning of the line should put us at the beginning of the line.
1927 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1928 assert_eq!(
1929 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1930 editor.selections.display_ranges(cx)
1931 );
1932
1933 // Moving to the end of the line should put us at the end of the line.
1934 editor.move_to_end_of_line(&move_to_end, window, cx);
1935 assert_eq!(
1936 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1937 editor.selections.display_ranges(cx)
1938 );
1939
1940 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1941 // Start the cursor at the last line (`y` that was wrapped to a new line)
1942 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1943 s.select_display_ranges([
1944 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1945 ]);
1946 });
1947
1948 // Moving to the beginning of the line should put us at the start of the second line of
1949 // display text, i.e., the `j`.
1950 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1951 assert_eq!(
1952 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1953 editor.selections.display_ranges(cx)
1954 );
1955
1956 // Moving to the beginning of the line again should be a no-op.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1960 editor.selections.display_ranges(cx)
1961 );
1962
1963 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1964 // next display line.
1965 editor.move_to_end_of_line(&move_to_end, window, cx);
1966 assert_eq!(
1967 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1968 editor.selections.display_ranges(cx)
1969 );
1970
1971 // Moving to the end of the line again should be a no-op.
1972 editor.move_to_end_of_line(&move_to_end, window, cx);
1973 assert_eq!(
1974 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1975 editor.selections.display_ranges(cx)
1976 );
1977 });
1978}
1979
1980#[gpui::test]
1981fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1982 init_test(cx, |_| {});
1983
1984 let move_to_beg = MoveToBeginningOfLine {
1985 stop_at_soft_wraps: true,
1986 stop_at_indent: true,
1987 };
1988
1989 let select_to_beg = SelectToBeginningOfLine {
1990 stop_at_soft_wraps: true,
1991 stop_at_indent: true,
1992 };
1993
1994 let delete_to_beg = DeleteToBeginningOfLine {
1995 stop_at_indent: true,
1996 };
1997
1998 let move_to_end = MoveToEndOfLine {
1999 stop_at_soft_wraps: false,
2000 };
2001
2002 let editor = cx.add_window(|window, cx| {
2003 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2004 build_editor(buffer, window, cx)
2005 });
2006
2007 _ = editor.update(cx, |editor, window, cx| {
2008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2009 s.select_display_ranges([
2010 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2011 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2012 ]);
2013 });
2014
2015 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2016 // and the second cursor at the first non-whitespace character in the line.
2017 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2018 assert_eq!(
2019 editor.selections.display_ranges(cx),
2020 &[
2021 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2022 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2023 ]
2024 );
2025
2026 // Moving to the beginning of the line again should be a no-op for the first cursor,
2027 // and should move the second cursor to the beginning of the line.
2028 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2029 assert_eq!(
2030 editor.selections.display_ranges(cx),
2031 &[
2032 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2033 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2034 ]
2035 );
2036
2037 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2038 // and should move the second cursor back to the first non-whitespace character in the line.
2039 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2040 assert_eq!(
2041 editor.selections.display_ranges(cx),
2042 &[
2043 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2044 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2045 ]
2046 );
2047
2048 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2049 // and to the first non-whitespace character in the line for the second cursor.
2050 editor.move_to_end_of_line(&move_to_end, window, cx);
2051 editor.move_left(&MoveLeft, window, cx);
2052 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2053 assert_eq!(
2054 editor.selections.display_ranges(cx),
2055 &[
2056 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2057 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2058 ]
2059 );
2060
2061 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2062 // and should select to the beginning of the line for the second cursor.
2063 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2064 assert_eq!(
2065 editor.selections.display_ranges(cx),
2066 &[
2067 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2068 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2069 ]
2070 );
2071
2072 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2073 // and should delete to the first non-whitespace character in the line for the second cursor.
2074 editor.move_to_end_of_line(&move_to_end, window, cx);
2075 editor.move_left(&MoveLeft, window, cx);
2076 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2077 assert_eq!(editor.text(cx), "c\n f");
2078 });
2079}
2080
2081#[gpui::test]
2082fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2083 init_test(cx, |_| {});
2084
2085 let move_to_beg = MoveToBeginningOfLine {
2086 stop_at_soft_wraps: true,
2087 stop_at_indent: true,
2088 };
2089
2090 let editor = cx.add_window(|window, cx| {
2091 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2092 build_editor(buffer, window, cx)
2093 });
2094
2095 _ = editor.update(cx, |editor, window, cx| {
2096 // test cursor between line_start and indent_start
2097 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2098 s.select_display_ranges([
2099 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2100 ]);
2101 });
2102
2103 // cursor should move to line_start
2104 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2105 assert_eq!(
2106 editor.selections.display_ranges(cx),
2107 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2108 );
2109
2110 // cursor should move to indent_start
2111 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2112 assert_eq!(
2113 editor.selections.display_ranges(cx),
2114 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2115 );
2116
2117 // cursor should move to back to line_start
2118 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2119 assert_eq!(
2120 editor.selections.display_ranges(cx),
2121 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2122 );
2123 });
2124}
2125
2126#[gpui::test]
2127fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2128 init_test(cx, |_| {});
2129
2130 let editor = cx.add_window(|window, cx| {
2131 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2132 build_editor(buffer, window, cx)
2133 });
2134 _ = editor.update(cx, |editor, window, cx| {
2135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2136 s.select_display_ranges([
2137 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2138 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2139 ])
2140 });
2141 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2142 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2143
2144 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2145 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2146
2147 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2148 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2149
2150 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2151 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2152
2153 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2154 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2155
2156 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2157 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2158
2159 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2160 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2161
2162 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2163 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2164
2165 editor.move_right(&MoveRight, window, cx);
2166 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2167 assert_selection_ranges(
2168 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2169 editor,
2170 cx,
2171 );
2172
2173 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2174 assert_selection_ranges(
2175 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2176 editor,
2177 cx,
2178 );
2179
2180 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2181 assert_selection_ranges(
2182 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2183 editor,
2184 cx,
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192
2193 let editor = cx.add_window(|window, cx| {
2194 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2195 build_editor(buffer, window, cx)
2196 });
2197
2198 _ = editor.update(cx, |editor, window, cx| {
2199 editor.set_wrap_width(Some(140.0.into()), cx);
2200 assert_eq!(
2201 editor.display_text(cx),
2202 "use one::{\n two::three::\n four::five\n};"
2203 );
2204
2205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2206 s.select_display_ranges([
2207 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2208 ]);
2209 });
2210
2211 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2212 assert_eq!(
2213 editor.selections.display_ranges(cx),
2214 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2215 );
2216
2217 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2218 assert_eq!(
2219 editor.selections.display_ranges(cx),
2220 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2221 );
2222
2223 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2224 assert_eq!(
2225 editor.selections.display_ranges(cx),
2226 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2227 );
2228
2229 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2230 assert_eq!(
2231 editor.selections.display_ranges(cx),
2232 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2233 );
2234
2235 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2236 assert_eq!(
2237 editor.selections.display_ranges(cx),
2238 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2239 );
2240
2241 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2242 assert_eq!(
2243 editor.selections.display_ranges(cx),
2244 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2245 );
2246 });
2247}
2248
2249#[gpui::test]
2250async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2251 init_test(cx, |_| {});
2252 let mut cx = EditorTestContext::new(cx).await;
2253
2254 let line_height = cx.editor(|editor, window, _| {
2255 editor
2256 .style()
2257 .unwrap()
2258 .text
2259 .line_height_in_pixels(window.rem_size())
2260 });
2261 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2262
2263 cx.set_state(
2264 &r#"ˇone
2265 two
2266
2267 three
2268 fourˇ
2269 five
2270
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285 ˇ
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"one
2295 two
2296
2297 three
2298 four
2299 five
2300 ˇ
2301 sixˇ"#
2302 .unindent(),
2303 );
2304
2305 cx.update_editor(|editor, window, cx| {
2306 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2307 });
2308 cx.assert_editor_state(
2309 &r#"one
2310 two
2311
2312 three
2313 four
2314 five
2315
2316 sixˇ"#
2317 .unindent(),
2318 );
2319
2320 cx.update_editor(|editor, window, cx| {
2321 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2322 });
2323 cx.assert_editor_state(
2324 &r#"one
2325 two
2326
2327 three
2328 four
2329 five
2330 ˇ
2331 six"#
2332 .unindent(),
2333 );
2334
2335 cx.update_editor(|editor, window, cx| {
2336 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2337 });
2338 cx.assert_editor_state(
2339 &r#"one
2340 two
2341 ˇ
2342 three
2343 four
2344 five
2345
2346 six"#
2347 .unindent(),
2348 );
2349
2350 cx.update_editor(|editor, window, cx| {
2351 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2352 });
2353 cx.assert_editor_state(
2354 &r#"ˇone
2355 two
2356
2357 three
2358 four
2359 five
2360
2361 six"#
2362 .unindent(),
2363 );
2364}
2365
2366#[gpui::test]
2367async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2368 init_test(cx, |_| {});
2369 let mut cx = EditorTestContext::new(cx).await;
2370 let line_height = cx.editor(|editor, window, _| {
2371 editor
2372 .style()
2373 .unwrap()
2374 .text
2375 .line_height_in_pixels(window.rem_size())
2376 });
2377 let window = cx.window;
2378 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2379
2380 cx.set_state(
2381 r#"ˇone
2382 two
2383 three
2384 four
2385 five
2386 six
2387 seven
2388 eight
2389 nine
2390 ten
2391 "#,
2392 );
2393
2394 cx.update_editor(|editor, window, cx| {
2395 assert_eq!(
2396 editor.snapshot(window, cx).scroll_position(),
2397 gpui::Point::new(0., 0.)
2398 );
2399 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2400 assert_eq!(
2401 editor.snapshot(window, cx).scroll_position(),
2402 gpui::Point::new(0., 3.)
2403 );
2404 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2405 assert_eq!(
2406 editor.snapshot(window, cx).scroll_position(),
2407 gpui::Point::new(0., 6.)
2408 );
2409 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2410 assert_eq!(
2411 editor.snapshot(window, cx).scroll_position(),
2412 gpui::Point::new(0., 3.)
2413 );
2414
2415 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 1.)
2419 );
2420 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2421 assert_eq!(
2422 editor.snapshot(window, cx).scroll_position(),
2423 gpui::Point::new(0., 3.)
2424 );
2425 });
2426}
2427
2428#[gpui::test]
2429async fn test_autoscroll(cx: &mut TestAppContext) {
2430 init_test(cx, |_| {});
2431 let mut cx = EditorTestContext::new(cx).await;
2432
2433 let line_height = cx.update_editor(|editor, window, cx| {
2434 editor.set_vertical_scroll_margin(2, cx);
2435 editor
2436 .style()
2437 .unwrap()
2438 .text
2439 .line_height_in_pixels(window.rem_size())
2440 });
2441 let window = cx.window;
2442 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2443
2444 cx.set_state(
2445 r#"ˇone
2446 two
2447 three
2448 four
2449 five
2450 six
2451 seven
2452 eight
2453 nine
2454 ten
2455 "#,
2456 );
2457 cx.update_editor(|editor, window, cx| {
2458 assert_eq!(
2459 editor.snapshot(window, cx).scroll_position(),
2460 gpui::Point::new(0., 0.0)
2461 );
2462 });
2463
2464 // Add a cursor below the visible area. Since both cursors cannot fit
2465 // on screen, the editor autoscrolls to reveal the newest cursor, and
2466 // allows the vertical scroll margin below that cursor.
2467 cx.update_editor(|editor, window, cx| {
2468 editor.change_selections(Default::default(), window, cx, |selections| {
2469 selections.select_ranges([
2470 Point::new(0, 0)..Point::new(0, 0),
2471 Point::new(6, 0)..Point::new(6, 0),
2472 ]);
2473 })
2474 });
2475 cx.update_editor(|editor, window, cx| {
2476 assert_eq!(
2477 editor.snapshot(window, cx).scroll_position(),
2478 gpui::Point::new(0., 3.0)
2479 );
2480 });
2481
2482 // Move down. The editor cursor scrolls down to track the newest cursor.
2483 cx.update_editor(|editor, window, cx| {
2484 editor.move_down(&Default::default(), window, cx);
2485 });
2486 cx.update_editor(|editor, window, cx| {
2487 assert_eq!(
2488 editor.snapshot(window, cx).scroll_position(),
2489 gpui::Point::new(0., 4.0)
2490 );
2491 });
2492
2493 // Add a cursor above the visible area. Since both cursors fit on screen,
2494 // the editor scrolls to show both.
2495 cx.update_editor(|editor, window, cx| {
2496 editor.change_selections(Default::default(), window, cx, |selections| {
2497 selections.select_ranges([
2498 Point::new(1, 0)..Point::new(1, 0),
2499 Point::new(6, 0)..Point::new(6, 0),
2500 ]);
2501 })
2502 });
2503 cx.update_editor(|editor, window, cx| {
2504 assert_eq!(
2505 editor.snapshot(window, cx).scroll_position(),
2506 gpui::Point::new(0., 1.0)
2507 );
2508 });
2509}
2510
2511#[gpui::test]
2512async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2513 init_test(cx, |_| {});
2514 let mut cx = EditorTestContext::new(cx).await;
2515
2516 let line_height = cx.editor(|editor, window, _cx| {
2517 editor
2518 .style()
2519 .unwrap()
2520 .text
2521 .line_height_in_pixels(window.rem_size())
2522 });
2523 let window = cx.window;
2524 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2525 cx.set_state(
2526 &r#"
2527 ˇone
2528 two
2529 threeˇ
2530 four
2531 five
2532 six
2533 seven
2534 eight
2535 nine
2536 ten
2537 "#
2538 .unindent(),
2539 );
2540
2541 cx.update_editor(|editor, window, cx| {
2542 editor.move_page_down(&MovePageDown::default(), window, cx)
2543 });
2544 cx.assert_editor_state(
2545 &r#"
2546 one
2547 two
2548 three
2549 ˇfour
2550 five
2551 sixˇ
2552 seven
2553 eight
2554 nine
2555 ten
2556 "#
2557 .unindent(),
2558 );
2559
2560 cx.update_editor(|editor, window, cx| {
2561 editor.move_page_down(&MovePageDown::default(), window, cx)
2562 });
2563 cx.assert_editor_state(
2564 &r#"
2565 one
2566 two
2567 three
2568 four
2569 five
2570 six
2571 ˇseven
2572 eight
2573 nineˇ
2574 ten
2575 "#
2576 .unindent(),
2577 );
2578
2579 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2580 cx.assert_editor_state(
2581 &r#"
2582 one
2583 two
2584 three
2585 ˇfour
2586 five
2587 sixˇ
2588 seven
2589 eight
2590 nine
2591 ten
2592 "#
2593 .unindent(),
2594 );
2595
2596 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2597 cx.assert_editor_state(
2598 &r#"
2599 ˇone
2600 two
2601 threeˇ
2602 four
2603 five
2604 six
2605 seven
2606 eight
2607 nine
2608 ten
2609 "#
2610 .unindent(),
2611 );
2612
2613 // Test select collapsing
2614 cx.update_editor(|editor, window, cx| {
2615 editor.move_page_down(&MovePageDown::default(), window, cx);
2616 editor.move_page_down(&MovePageDown::default(), window, cx);
2617 editor.move_page_down(&MovePageDown::default(), window, cx);
2618 });
2619 cx.assert_editor_state(
2620 &r#"
2621 one
2622 two
2623 three
2624 four
2625 five
2626 six
2627 seven
2628 eight
2629 nine
2630 ˇten
2631 ˇ"#
2632 .unindent(),
2633 );
2634}
2635
2636#[gpui::test]
2637async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2638 init_test(cx, |_| {});
2639 let mut cx = EditorTestContext::new(cx).await;
2640 cx.set_state("one «two threeˇ» four");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_beginning_of_line(
2643 &DeleteToBeginningOfLine {
2644 stop_at_indent: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 assert_eq!(editor.text(cx), " four");
2650 });
2651}
2652
2653#[gpui::test]
2654async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 // For an empty selection, the preceding word fragment is deleted.
2660 // For non-empty selections, only selected characters are deleted.
2661 cx.set_state("onˇe two t«hreˇ»e four");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_previous_word_start(
2664 &DeleteToPreviousWordStart {
2665 ignore_newlines: false,
2666 ignore_brackets: false,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 cx.assert_editor_state("ˇe two tˇe four");
2673
2674 cx.set_state("e tˇwo te «fˇ»our");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: false,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("e tˇ te ˇour");
2686}
2687
2688#[gpui::test]
2689async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2690 init_test(cx, |_| {});
2691
2692 let mut cx = EditorTestContext::new(cx).await;
2693
2694 cx.set_state("here is some text ˇwith a space");
2695 cx.update_editor(|editor, window, cx| {
2696 editor.delete_to_previous_word_start(
2697 &DeleteToPreviousWordStart {
2698 ignore_newlines: false,
2699 ignore_brackets: true,
2700 },
2701 window,
2702 cx,
2703 );
2704 });
2705 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2706 cx.assert_editor_state("here is some textˇwith a space");
2707
2708 cx.set_state("here is some text ˇwith a space");
2709 cx.update_editor(|editor, window, cx| {
2710 editor.delete_to_previous_word_start(
2711 &DeleteToPreviousWordStart {
2712 ignore_newlines: false,
2713 ignore_brackets: false,
2714 },
2715 window,
2716 cx,
2717 );
2718 });
2719 cx.assert_editor_state("here is some textˇwith a space");
2720
2721 cx.set_state("here is some textˇ with a space");
2722 cx.update_editor(|editor, window, cx| {
2723 editor.delete_to_next_word_end(
2724 &DeleteToNextWordEnd {
2725 ignore_newlines: false,
2726 ignore_brackets: true,
2727 },
2728 window,
2729 cx,
2730 );
2731 });
2732 // Same happens in the other direction.
2733 cx.assert_editor_state("here is some textˇwith a space");
2734
2735 cx.set_state("here is some textˇ with a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_next_word_end(
2738 &DeleteToNextWordEnd {
2739 ignore_newlines: false,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 cx.assert_editor_state("here is some textˇwith a space");
2747
2748 cx.set_state("here is some textˇ with a space");
2749 cx.update_editor(|editor, window, cx| {
2750 editor.delete_to_next_word_end(
2751 &DeleteToNextWordEnd {
2752 ignore_newlines: true,
2753 ignore_brackets: false,
2754 },
2755 window,
2756 cx,
2757 );
2758 });
2759 cx.assert_editor_state("here is some textˇwith a space");
2760 cx.update_editor(|editor, window, cx| {
2761 editor.delete_to_previous_word_start(
2762 &DeleteToPreviousWordStart {
2763 ignore_newlines: true,
2764 ignore_brackets: false,
2765 },
2766 window,
2767 cx,
2768 );
2769 });
2770 cx.assert_editor_state("here is some ˇwith a space");
2771 cx.update_editor(|editor, window, cx| {
2772 editor.delete_to_previous_word_start(
2773 &DeleteToPreviousWordStart {
2774 ignore_newlines: true,
2775 ignore_brackets: false,
2776 },
2777 window,
2778 cx,
2779 );
2780 });
2781 // Single whitespaces are removed with the word behind them.
2782 cx.assert_editor_state("here is ˇwith a space");
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_previous_word_start(
2785 &DeleteToPreviousWordStart {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 cx.assert_editor_state("here ˇwith a space");
2794 cx.update_editor(|editor, window, cx| {
2795 editor.delete_to_previous_word_start(
2796 &DeleteToPreviousWordStart {
2797 ignore_newlines: true,
2798 ignore_brackets: false,
2799 },
2800 window,
2801 cx,
2802 );
2803 });
2804 cx.assert_editor_state("ˇwith a space");
2805 cx.update_editor(|editor, window, cx| {
2806 editor.delete_to_previous_word_start(
2807 &DeleteToPreviousWordStart {
2808 ignore_newlines: true,
2809 ignore_brackets: false,
2810 },
2811 window,
2812 cx,
2813 );
2814 });
2815 cx.assert_editor_state("ˇwith a space");
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 // Same happens in the other direction.
2827 cx.assert_editor_state("ˇ a space");
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state("ˇ space");
2839 cx.update_editor(|editor, window, cx| {
2840 editor.delete_to_next_word_end(
2841 &DeleteToNextWordEnd {
2842 ignore_newlines: true,
2843 ignore_brackets: false,
2844 },
2845 window,
2846 cx,
2847 );
2848 });
2849 cx.assert_editor_state("ˇ");
2850 cx.update_editor(|editor, window, cx| {
2851 editor.delete_to_next_word_end(
2852 &DeleteToNextWordEnd {
2853 ignore_newlines: true,
2854 ignore_brackets: false,
2855 },
2856 window,
2857 cx,
2858 );
2859 });
2860 cx.assert_editor_state("ˇ");
2861 cx.update_editor(|editor, window, cx| {
2862 editor.delete_to_previous_word_start(
2863 &DeleteToPreviousWordStart {
2864 ignore_newlines: true,
2865 ignore_brackets: false,
2866 },
2867 window,
2868 cx,
2869 );
2870 });
2871 cx.assert_editor_state("ˇ");
2872}
2873
2874#[gpui::test]
2875async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2876 init_test(cx, |_| {});
2877
2878 let language = Arc::new(
2879 Language::new(
2880 LanguageConfig {
2881 brackets: BracketPairConfig {
2882 pairs: vec![
2883 BracketPair {
2884 start: "\"".to_string(),
2885 end: "\"".to_string(),
2886 close: true,
2887 surround: true,
2888 newline: false,
2889 },
2890 BracketPair {
2891 start: "(".to_string(),
2892 end: ")".to_string(),
2893 close: true,
2894 surround: true,
2895 newline: true,
2896 },
2897 ],
2898 ..BracketPairConfig::default()
2899 },
2900 ..LanguageConfig::default()
2901 },
2902 Some(tree_sitter_rust::LANGUAGE.into()),
2903 )
2904 .with_brackets_query(
2905 r#"
2906 ("(" @open ")" @close)
2907 ("\"" @open "\"" @close)
2908 "#,
2909 )
2910 .unwrap(),
2911 );
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2915
2916 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_previous_word_start(
2919 &DeleteToPreviousWordStart {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 // Deletion stops before brackets if asked to not ignore them.
2928 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2929 cx.update_editor(|editor, window, cx| {
2930 editor.delete_to_previous_word_start(
2931 &DeleteToPreviousWordStart {
2932 ignore_newlines: true,
2933 ignore_brackets: false,
2934 },
2935 window,
2936 cx,
2937 );
2938 });
2939 // Deletion has to remove a single bracket and then stop again.
2940 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2941
2942 cx.update_editor(|editor, window, cx| {
2943 editor.delete_to_previous_word_start(
2944 &DeleteToPreviousWordStart {
2945 ignore_newlines: true,
2946 ignore_brackets: false,
2947 },
2948 window,
2949 cx,
2950 );
2951 });
2952 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2953
2954 cx.update_editor(|editor, window, cx| {
2955 editor.delete_to_previous_word_start(
2956 &DeleteToPreviousWordStart {
2957 ignore_newlines: true,
2958 ignore_brackets: false,
2959 },
2960 window,
2961 cx,
2962 );
2963 });
2964 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2965
2966 cx.update_editor(|editor, window, cx| {
2967 editor.delete_to_previous_word_start(
2968 &DeleteToPreviousWordStart {
2969 ignore_newlines: true,
2970 ignore_brackets: false,
2971 },
2972 window,
2973 cx,
2974 );
2975 });
2976 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2977
2978 cx.update_editor(|editor, window, cx| {
2979 editor.delete_to_next_word_end(
2980 &DeleteToNextWordEnd {
2981 ignore_newlines: true,
2982 ignore_brackets: false,
2983 },
2984 window,
2985 cx,
2986 );
2987 });
2988 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2989 cx.assert_editor_state(r#"ˇ");"#);
2990
2991 cx.update_editor(|editor, window, cx| {
2992 editor.delete_to_next_word_end(
2993 &DeleteToNextWordEnd {
2994 ignore_newlines: true,
2995 ignore_brackets: false,
2996 },
2997 window,
2998 cx,
2999 );
3000 });
3001 cx.assert_editor_state(r#"ˇ"#);
3002
3003 cx.update_editor(|editor, window, cx| {
3004 editor.delete_to_next_word_end(
3005 &DeleteToNextWordEnd {
3006 ignore_newlines: true,
3007 ignore_brackets: false,
3008 },
3009 window,
3010 cx,
3011 );
3012 });
3013 cx.assert_editor_state(r#"ˇ"#);
3014
3015 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
3016 cx.update_editor(|editor, window, cx| {
3017 editor.delete_to_previous_word_start(
3018 &DeleteToPreviousWordStart {
3019 ignore_newlines: true,
3020 ignore_brackets: true,
3021 },
3022 window,
3023 cx,
3024 );
3025 });
3026 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3027}
3028
3029#[gpui::test]
3030fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3031 init_test(cx, |_| {});
3032
3033 let editor = cx.add_window(|window, cx| {
3034 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3035 build_editor(buffer, window, cx)
3036 });
3037 let del_to_prev_word_start = DeleteToPreviousWordStart {
3038 ignore_newlines: false,
3039 ignore_brackets: false,
3040 };
3041 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3042 ignore_newlines: true,
3043 ignore_brackets: false,
3044 };
3045
3046 _ = editor.update(cx, |editor, window, cx| {
3047 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3048 s.select_display_ranges([
3049 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3050 ])
3051 });
3052 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3053 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3054 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3055 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3056 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3057 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3058 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3059 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3060 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3061 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3062 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3063 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3064 });
3065}
3066
3067#[gpui::test]
3068fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3069 init_test(cx, |_| {});
3070
3071 let editor = cx.add_window(|window, cx| {
3072 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3073 build_editor(buffer, window, cx)
3074 });
3075 let del_to_next_word_end = DeleteToNextWordEnd {
3076 ignore_newlines: false,
3077 ignore_brackets: false,
3078 };
3079 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3080 ignore_newlines: true,
3081 ignore_brackets: false,
3082 };
3083
3084 _ = editor.update(cx, |editor, window, cx| {
3085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3086 s.select_display_ranges([
3087 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3088 ])
3089 });
3090 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3091 assert_eq!(
3092 editor.buffer.read(cx).read(cx).text(),
3093 "one\n two\nthree\n four"
3094 );
3095 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3096 assert_eq!(
3097 editor.buffer.read(cx).read(cx).text(),
3098 "\n two\nthree\n four"
3099 );
3100 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3101 assert_eq!(
3102 editor.buffer.read(cx).read(cx).text(),
3103 "two\nthree\n four"
3104 );
3105 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3106 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3107 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3108 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3109 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3110 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3111 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3112 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3113 });
3114}
3115
3116#[gpui::test]
3117fn test_newline(cx: &mut TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let editor = cx.add_window(|window, cx| {
3121 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3122 build_editor(buffer, window, cx)
3123 });
3124
3125 _ = editor.update(cx, |editor, window, cx| {
3126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3127 s.select_display_ranges([
3128 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3129 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3130 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3131 ])
3132 });
3133
3134 editor.newline(&Newline, window, cx);
3135 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3136 });
3137}
3138
3139#[gpui::test]
3140fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3141 init_test(cx, |_| {});
3142
3143 let editor = cx.add_window(|window, cx| {
3144 let buffer = MultiBuffer::build_simple(
3145 "
3146 a
3147 b(
3148 X
3149 )
3150 c(
3151 X
3152 )
3153 "
3154 .unindent()
3155 .as_str(),
3156 cx,
3157 );
3158 let mut editor = build_editor(buffer, window, cx);
3159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3160 s.select_ranges([
3161 Point::new(2, 4)..Point::new(2, 5),
3162 Point::new(5, 4)..Point::new(5, 5),
3163 ])
3164 });
3165 editor
3166 });
3167
3168 _ = editor.update(cx, |editor, window, cx| {
3169 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3170 editor.buffer.update(cx, |buffer, cx| {
3171 buffer.edit(
3172 [
3173 (Point::new(1, 2)..Point::new(3, 0), ""),
3174 (Point::new(4, 2)..Point::new(6, 0), ""),
3175 ],
3176 None,
3177 cx,
3178 );
3179 assert_eq!(
3180 buffer.read(cx).text(),
3181 "
3182 a
3183 b()
3184 c()
3185 "
3186 .unindent()
3187 );
3188 });
3189 assert_eq!(
3190 editor.selections.ranges(&editor.display_snapshot(cx)),
3191 &[
3192 Point::new(1, 2)..Point::new(1, 2),
3193 Point::new(2, 2)..Point::new(2, 2),
3194 ],
3195 );
3196
3197 editor.newline(&Newline, window, cx);
3198 assert_eq!(
3199 editor.text(cx),
3200 "
3201 a
3202 b(
3203 )
3204 c(
3205 )
3206 "
3207 .unindent()
3208 );
3209
3210 // The selections are moved after the inserted newlines
3211 assert_eq!(
3212 editor.selections.ranges(&editor.display_snapshot(cx)),
3213 &[
3214 Point::new(2, 0)..Point::new(2, 0),
3215 Point::new(4, 0)..Point::new(4, 0),
3216 ],
3217 );
3218 });
3219}
3220
3221#[gpui::test]
3222async fn test_newline_above(cx: &mut TestAppContext) {
3223 init_test(cx, |settings| {
3224 settings.defaults.tab_size = NonZeroU32::new(4)
3225 });
3226
3227 let language = Arc::new(
3228 Language::new(
3229 LanguageConfig::default(),
3230 Some(tree_sitter_rust::LANGUAGE.into()),
3231 )
3232 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3233 .unwrap(),
3234 );
3235
3236 let mut cx = EditorTestContext::new(cx).await;
3237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3238 cx.set_state(indoc! {"
3239 const a: ˇA = (
3240 (ˇ
3241 «const_functionˇ»(ˇ),
3242 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3243 )ˇ
3244 ˇ);ˇ
3245 "});
3246
3247 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3248 cx.assert_editor_state(indoc! {"
3249 ˇ
3250 const a: A = (
3251 ˇ
3252 (
3253 ˇ
3254 ˇ
3255 const_function(),
3256 ˇ
3257 ˇ
3258 ˇ
3259 ˇ
3260 something_else,
3261 ˇ
3262 )
3263 ˇ
3264 ˇ
3265 );
3266 "});
3267}
3268
3269#[gpui::test]
3270async fn test_newline_below(cx: &mut TestAppContext) {
3271 init_test(cx, |settings| {
3272 settings.defaults.tab_size = NonZeroU32::new(4)
3273 });
3274
3275 let language = Arc::new(
3276 Language::new(
3277 LanguageConfig::default(),
3278 Some(tree_sitter_rust::LANGUAGE.into()),
3279 )
3280 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3281 .unwrap(),
3282 );
3283
3284 let mut cx = EditorTestContext::new(cx).await;
3285 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3286 cx.set_state(indoc! {"
3287 const a: ˇA = (
3288 (ˇ
3289 «const_functionˇ»(ˇ),
3290 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3291 )ˇ
3292 ˇ);ˇ
3293 "});
3294
3295 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: A = (
3298 ˇ
3299 (
3300 ˇ
3301 const_function(),
3302 ˇ
3303 ˇ
3304 something_else,
3305 ˇ
3306 ˇ
3307 ˇ
3308 ˇ
3309 )
3310 ˇ
3311 );
3312 ˇ
3313 ˇ
3314 "});
3315}
3316
3317#[gpui::test]
3318async fn test_newline_comments(cx: &mut TestAppContext) {
3319 init_test(cx, |settings| {
3320 settings.defaults.tab_size = NonZeroU32::new(4)
3321 });
3322
3323 let language = Arc::new(Language::new(
3324 LanguageConfig {
3325 line_comments: vec!["// ".into()],
3326 ..LanguageConfig::default()
3327 },
3328 None,
3329 ));
3330 {
3331 let mut cx = EditorTestContext::new(cx).await;
3332 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3333 cx.set_state(indoc! {"
3334 // Fooˇ
3335 "});
3336
3337 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3338 cx.assert_editor_state(indoc! {"
3339 // Foo
3340 // ˇ
3341 "});
3342 // Ensure that we add comment prefix when existing line contains space
3343 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3344 cx.assert_editor_state(
3345 indoc! {"
3346 // Foo
3347 //s
3348 // ˇ
3349 "}
3350 .replace("s", " ") // s is used as space placeholder to prevent format on save
3351 .as_str(),
3352 );
3353 // Ensure that we add comment prefix when existing line does not contain space
3354 cx.set_state(indoc! {"
3355 // Foo
3356 //ˇ
3357 "});
3358 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3359 cx.assert_editor_state(indoc! {"
3360 // Foo
3361 //
3362 // ˇ
3363 "});
3364 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3365 cx.set_state(indoc! {"
3366 ˇ// Foo
3367 "});
3368 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3369 cx.assert_editor_state(indoc! {"
3370
3371 ˇ// Foo
3372 "});
3373 }
3374 // Ensure that comment continuations can be disabled.
3375 update_test_language_settings(cx, |settings| {
3376 settings.defaults.extend_comment_on_newline = Some(false);
3377 });
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.set_state(indoc! {"
3380 // Fooˇ
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 // Foo
3385 ˇ
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4)
3393 });
3394
3395 let language = Arc::new(Language::new(
3396 LanguageConfig {
3397 line_comments: vec!["// ".into(), "/// ".into()],
3398 ..LanguageConfig::default()
3399 },
3400 None,
3401 ));
3402 {
3403 let mut cx = EditorTestContext::new(cx).await;
3404 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3405 cx.set_state(indoc! {"
3406 //ˇ
3407 "});
3408 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 //
3411 // ˇ
3412 "});
3413
3414 cx.set_state(indoc! {"
3415 ///ˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 ///
3420 /// ˇ
3421 "});
3422 }
3423}
3424
3425#[gpui::test]
3426async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3427 init_test(cx, |settings| {
3428 settings.defaults.tab_size = NonZeroU32::new(4)
3429 });
3430
3431 let language = Arc::new(
3432 Language::new(
3433 LanguageConfig {
3434 documentation_comment: Some(language::BlockCommentConfig {
3435 start: "/**".into(),
3436 end: "*/".into(),
3437 prefix: "* ".into(),
3438 tab_size: 1,
3439 }),
3440
3441 ..LanguageConfig::default()
3442 },
3443 Some(tree_sitter_rust::LANGUAGE.into()),
3444 )
3445 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3446 .unwrap(),
3447 );
3448
3449 {
3450 let mut cx = EditorTestContext::new(cx).await;
3451 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3452 cx.set_state(indoc! {"
3453 /**ˇ
3454 "});
3455
3456 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3457 cx.assert_editor_state(indoc! {"
3458 /**
3459 * ˇ
3460 "});
3461 // Ensure that if cursor is before the comment start,
3462 // we do not actually insert a comment prefix.
3463 cx.set_state(indoc! {"
3464 ˇ/**
3465 "});
3466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3467 cx.assert_editor_state(indoc! {"
3468
3469 ˇ/**
3470 "});
3471 // Ensure that if cursor is between it doesn't add comment prefix.
3472 cx.set_state(indoc! {"
3473 /*ˇ*
3474 "});
3475 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 /*
3478 ˇ*
3479 "});
3480 // Ensure that if suffix exists on same line after cursor it adds new line.
3481 cx.set_state(indoc! {"
3482 /**ˇ*/
3483 "});
3484 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3485 cx.assert_editor_state(indoc! {"
3486 /**
3487 * ˇ
3488 */
3489 "});
3490 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3491 cx.set_state(indoc! {"
3492 /**ˇ */
3493 "});
3494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 /**
3497 * ˇ
3498 */
3499 "});
3500 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3501 cx.set_state(indoc! {"
3502 /** ˇ*/
3503 "});
3504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3505 cx.assert_editor_state(
3506 indoc! {"
3507 /**s
3508 * ˇ
3509 */
3510 "}
3511 .replace("s", " ") // s is used as space placeholder to prevent format on save
3512 .as_str(),
3513 );
3514 // Ensure that delimiter space is preserved when newline on already
3515 // spaced delimiter.
3516 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3517 cx.assert_editor_state(
3518 indoc! {"
3519 /**s
3520 *s
3521 * ˇ
3522 */
3523 "}
3524 .replace("s", " ") // s is used as space placeholder to prevent format on save
3525 .as_str(),
3526 );
3527 // Ensure that delimiter space is preserved when space is not
3528 // on existing delimiter.
3529 cx.set_state(indoc! {"
3530 /**
3531 *ˇ
3532 */
3533 "});
3534 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3535 cx.assert_editor_state(indoc! {"
3536 /**
3537 *
3538 * ˇ
3539 */
3540 "});
3541 // Ensure that if suffix exists on same line after cursor it
3542 // doesn't add extra new line if prefix is not on same line.
3543 cx.set_state(indoc! {"
3544 /**
3545 ˇ*/
3546 "});
3547 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3548 cx.assert_editor_state(indoc! {"
3549 /**
3550
3551 ˇ*/
3552 "});
3553 // Ensure that it detects suffix after existing prefix.
3554 cx.set_state(indoc! {"
3555 /**ˇ/
3556 "});
3557 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3558 cx.assert_editor_state(indoc! {"
3559 /**
3560 ˇ/
3561 "});
3562 // Ensure that if suffix exists on same line before
3563 // cursor it does not add comment prefix.
3564 cx.set_state(indoc! {"
3565 /** */ˇ
3566 "});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 /** */
3570 ˇ
3571 "});
3572 // Ensure that if suffix exists on same line before
3573 // cursor it does not add comment prefix.
3574 cx.set_state(indoc! {"
3575 /**
3576 *
3577 */ˇ
3578 "});
3579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 /**
3582 *
3583 */
3584 ˇ
3585 "});
3586
3587 // Ensure that inline comment followed by code
3588 // doesn't add comment prefix on newline
3589 cx.set_state(indoc! {"
3590 /** */ textˇ
3591 "});
3592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 /** */ text
3595 ˇ
3596 "});
3597
3598 // Ensure that text after comment end tag
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /**
3602 *
3603 */ˇtext
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /**
3608 *
3609 */
3610 ˇtext
3611 "});
3612
3613 // Ensure if not comment block it doesn't
3614 // add comment prefix on newline
3615 cx.set_state(indoc! {"
3616 * textˇ
3617 "});
3618 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3619 cx.assert_editor_state(indoc! {"
3620 * text
3621 ˇ
3622 "});
3623 }
3624 // Ensure that comment continuations can be disabled.
3625 update_test_language_settings(cx, |settings| {
3626 settings.defaults.extend_comment_on_newline = Some(false);
3627 });
3628 let mut cx = EditorTestContext::new(cx).await;
3629 cx.set_state(indoc! {"
3630 /**ˇ
3631 "});
3632 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 /**
3635 ˇ
3636 "});
3637}
3638
3639#[gpui::test]
3640async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3641 init_test(cx, |settings| {
3642 settings.defaults.tab_size = NonZeroU32::new(4)
3643 });
3644
3645 let lua_language = Arc::new(Language::new(
3646 LanguageConfig {
3647 line_comments: vec!["--".into()],
3648 block_comment: Some(language::BlockCommentConfig {
3649 start: "--[[".into(),
3650 prefix: "".into(),
3651 end: "]]".into(),
3652 tab_size: 0,
3653 }),
3654 ..LanguageConfig::default()
3655 },
3656 None,
3657 ));
3658
3659 let mut cx = EditorTestContext::new(cx).await;
3660 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3661
3662 // Line with line comment should extend
3663 cx.set_state(indoc! {"
3664 --ˇ
3665 "});
3666 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3667 cx.assert_editor_state(indoc! {"
3668 --
3669 --ˇ
3670 "});
3671
3672 // Line with block comment that matches line comment should not extend
3673 cx.set_state(indoc! {"
3674 --[[ˇ
3675 "});
3676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 --[[
3679 ˇ
3680 "});
3681}
3682
3683#[gpui::test]
3684fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3685 init_test(cx, |_| {});
3686
3687 let editor = cx.add_window(|window, cx| {
3688 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3689 let mut editor = build_editor(buffer, window, cx);
3690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3691 s.select_ranges([3..4, 11..12, 19..20])
3692 });
3693 editor
3694 });
3695
3696 _ = editor.update(cx, |editor, window, cx| {
3697 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3698 editor.buffer.update(cx, |buffer, cx| {
3699 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3700 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3701 });
3702 assert_eq!(
3703 editor.selections.ranges(&editor.display_snapshot(cx)),
3704 &[2..2, 7..7, 12..12],
3705 );
3706
3707 editor.insert("Z", window, cx);
3708 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3709
3710 // The selections are moved after the inserted characters
3711 assert_eq!(
3712 editor.selections.ranges(&editor.display_snapshot(cx)),
3713 &[3..3, 9..9, 15..15],
3714 );
3715 });
3716}
3717
3718#[gpui::test]
3719async fn test_tab(cx: &mut TestAppContext) {
3720 init_test(cx, |settings| {
3721 settings.defaults.tab_size = NonZeroU32::new(3)
3722 });
3723
3724 let mut cx = EditorTestContext::new(cx).await;
3725 cx.set_state(indoc! {"
3726 ˇabˇc
3727 ˇ🏀ˇ🏀ˇefg
3728 dˇ
3729 "});
3730 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3731 cx.assert_editor_state(indoc! {"
3732 ˇab ˇc
3733 ˇ🏀 ˇ🏀 ˇefg
3734 d ˇ
3735 "});
3736
3737 cx.set_state(indoc! {"
3738 a
3739 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 a
3744 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3745 "});
3746}
3747
3748#[gpui::test]
3749async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3750 init_test(cx, |_| {});
3751
3752 let mut cx = EditorTestContext::new(cx).await;
3753 let language = Arc::new(
3754 Language::new(
3755 LanguageConfig::default(),
3756 Some(tree_sitter_rust::LANGUAGE.into()),
3757 )
3758 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3759 .unwrap(),
3760 );
3761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3762
3763 // test when all cursors are not at suggested indent
3764 // then simply move to their suggested indent location
3765 cx.set_state(indoc! {"
3766 const a: B = (
3767 c(
3768 ˇ
3769 ˇ )
3770 );
3771 "});
3772 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3773 cx.assert_editor_state(indoc! {"
3774 const a: B = (
3775 c(
3776 ˇ
3777 ˇ)
3778 );
3779 "});
3780
3781 // test cursor already at suggested indent not moving when
3782 // other cursors are yet to reach their suggested indents
3783 cx.set_state(indoc! {"
3784 ˇ
3785 const a: B = (
3786 c(
3787 d(
3788 ˇ
3789 )
3790 ˇ
3791 ˇ )
3792 );
3793 "});
3794 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3795 cx.assert_editor_state(indoc! {"
3796 ˇ
3797 const a: B = (
3798 c(
3799 d(
3800 ˇ
3801 )
3802 ˇ
3803 ˇ)
3804 );
3805 "});
3806 // test when all cursors are at suggested indent then tab is inserted
3807 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 ˇ
3810 const a: B = (
3811 c(
3812 d(
3813 ˇ
3814 )
3815 ˇ
3816 ˇ)
3817 );
3818 "});
3819
3820 // test when current indent is less than suggested indent,
3821 // we adjust line to match suggested indent and move cursor to it
3822 //
3823 // when no other cursor is at word boundary, all of them should move
3824 cx.set_state(indoc! {"
3825 const a: B = (
3826 c(
3827 d(
3828 ˇ
3829 ˇ )
3830 ˇ )
3831 );
3832 "});
3833 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3834 cx.assert_editor_state(indoc! {"
3835 const a: B = (
3836 c(
3837 d(
3838 ˇ
3839 ˇ)
3840 ˇ)
3841 );
3842 "});
3843
3844 // test when current indent is less than suggested indent,
3845 // we adjust line to match suggested indent and move cursor to it
3846 //
3847 // when some other cursor is at word boundary, it should not move
3848 cx.set_state(indoc! {"
3849 const a: B = (
3850 c(
3851 d(
3852 ˇ
3853 ˇ )
3854 ˇ)
3855 );
3856 "});
3857 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3858 cx.assert_editor_state(indoc! {"
3859 const a: B = (
3860 c(
3861 d(
3862 ˇ
3863 ˇ)
3864 ˇ)
3865 );
3866 "});
3867
3868 // test when current indent is more than suggested indent,
3869 // we just move cursor to current indent instead of suggested indent
3870 //
3871 // when no other cursor is at word boundary, all of them should move
3872 cx.set_state(indoc! {"
3873 const a: B = (
3874 c(
3875 d(
3876 ˇ
3877 ˇ )
3878 ˇ )
3879 );
3880 "});
3881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3882 cx.assert_editor_state(indoc! {"
3883 const a: B = (
3884 c(
3885 d(
3886 ˇ
3887 ˇ)
3888 ˇ)
3889 );
3890 "});
3891 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3892 cx.assert_editor_state(indoc! {"
3893 const a: B = (
3894 c(
3895 d(
3896 ˇ
3897 ˇ)
3898 ˇ)
3899 );
3900 "});
3901
3902 // test when current indent is more than suggested indent,
3903 // we just move cursor to current indent instead of suggested indent
3904 //
3905 // when some other cursor is at word boundary, it doesn't move
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 d(
3910 ˇ
3911 ˇ )
3912 ˇ)
3913 );
3914 "});
3915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3916 cx.assert_editor_state(indoc! {"
3917 const a: B = (
3918 c(
3919 d(
3920 ˇ
3921 ˇ)
3922 ˇ)
3923 );
3924 "});
3925
3926 // handle auto-indent when there are multiple cursors on the same line
3927 cx.set_state(indoc! {"
3928 const a: B = (
3929 c(
3930 ˇ ˇ
3931 ˇ )
3932 );
3933 "});
3934 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3935 cx.assert_editor_state(indoc! {"
3936 const a: B = (
3937 c(
3938 ˇ
3939 ˇ)
3940 );
3941 "});
3942}
3943
3944#[gpui::test]
3945async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3946 init_test(cx, |settings| {
3947 settings.defaults.tab_size = NonZeroU32::new(3)
3948 });
3949
3950 let mut cx = EditorTestContext::new(cx).await;
3951 cx.set_state(indoc! {"
3952 ˇ
3953 \t ˇ
3954 \t ˇ
3955 \t ˇ
3956 \t \t\t \t \t\t \t\t \t \t ˇ
3957 "});
3958
3959 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 ˇ
3962 \t ˇ
3963 \t ˇ
3964 \t ˇ
3965 \t \t\t \t \t\t \t\t \t \t ˇ
3966 "});
3967}
3968
3969#[gpui::test]
3970async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3971 init_test(cx, |settings| {
3972 settings.defaults.tab_size = NonZeroU32::new(4)
3973 });
3974
3975 let language = Arc::new(
3976 Language::new(
3977 LanguageConfig::default(),
3978 Some(tree_sitter_rust::LANGUAGE.into()),
3979 )
3980 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3981 .unwrap(),
3982 );
3983
3984 let mut cx = EditorTestContext::new(cx).await;
3985 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3986 cx.set_state(indoc! {"
3987 fn a() {
3988 if b {
3989 \t ˇc
3990 }
3991 }
3992 "});
3993
3994 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3995 cx.assert_editor_state(indoc! {"
3996 fn a() {
3997 if b {
3998 ˇc
3999 }
4000 }
4001 "});
4002}
4003
4004#[gpui::test]
4005async fn test_indent_outdent(cx: &mut TestAppContext) {
4006 init_test(cx, |settings| {
4007 settings.defaults.tab_size = NonZeroU32::new(4);
4008 });
4009
4010 let mut cx = EditorTestContext::new(cx).await;
4011
4012 cx.set_state(indoc! {"
4013 «oneˇ» «twoˇ»
4014 three
4015 four
4016 "});
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 «oneˇ» «twoˇ»
4020 three
4021 four
4022 "});
4023
4024 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4025 cx.assert_editor_state(indoc! {"
4026 «oneˇ» «twoˇ»
4027 three
4028 four
4029 "});
4030
4031 // select across line ending
4032 cx.set_state(indoc! {"
4033 one two
4034 t«hree
4035 ˇ» four
4036 "});
4037 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 t«hree
4041 ˇ» four
4042 "});
4043
4044 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4045 cx.assert_editor_state(indoc! {"
4046 one two
4047 t«hree
4048 ˇ» four
4049 "});
4050
4051 // Ensure that indenting/outdenting works when the cursor is at column 0.
4052 cx.set_state(indoc! {"
4053 one two
4054 ˇthree
4055 four
4056 "});
4057 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4058 cx.assert_editor_state(indoc! {"
4059 one two
4060 ˇthree
4061 four
4062 "});
4063
4064 cx.set_state(indoc! {"
4065 one two
4066 ˇ three
4067 four
4068 "});
4069 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4070 cx.assert_editor_state(indoc! {"
4071 one two
4072 ˇthree
4073 four
4074 "});
4075}
4076
4077#[gpui::test]
4078async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4079 // This is a regression test for issue #33761
4080 init_test(cx, |_| {});
4081
4082 let mut cx = EditorTestContext::new(cx).await;
4083 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4085
4086 cx.set_state(
4087 r#"ˇ# ingress:
4088ˇ# api:
4089ˇ# enabled: false
4090ˇ# pathType: Prefix
4091ˇ# console:
4092ˇ# enabled: false
4093ˇ# pathType: Prefix
4094"#,
4095 );
4096
4097 // Press tab to indent all lines
4098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4099
4100 cx.assert_editor_state(
4101 r#" ˇ# ingress:
4102 ˇ# api:
4103 ˇ# enabled: false
4104 ˇ# pathType: Prefix
4105 ˇ# console:
4106 ˇ# enabled: false
4107 ˇ# pathType: Prefix
4108"#,
4109 );
4110}
4111
4112#[gpui::test]
4113async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4114 // This is a test to make sure our fix for issue #33761 didn't break anything
4115 init_test(cx, |_| {});
4116
4117 let mut cx = EditorTestContext::new(cx).await;
4118 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4119 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4120
4121 cx.set_state(
4122 r#"ˇingress:
4123ˇ api:
4124ˇ enabled: false
4125ˇ pathType: Prefix
4126"#,
4127 );
4128
4129 // Press tab to indent all lines
4130 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4131
4132 cx.assert_editor_state(
4133 r#"ˇingress:
4134 ˇapi:
4135 ˇenabled: false
4136 ˇpathType: Prefix
4137"#,
4138 );
4139}
4140
4141#[gpui::test]
4142async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4143 init_test(cx, |settings| {
4144 settings.defaults.hard_tabs = Some(true);
4145 });
4146
4147 let mut cx = EditorTestContext::new(cx).await;
4148
4149 // select two ranges on one line
4150 cx.set_state(indoc! {"
4151 «oneˇ» «twoˇ»
4152 three
4153 four
4154 "});
4155 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4156 cx.assert_editor_state(indoc! {"
4157 \t«oneˇ» «twoˇ»
4158 three
4159 four
4160 "});
4161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4162 cx.assert_editor_state(indoc! {"
4163 \t\t«oneˇ» «twoˇ»
4164 three
4165 four
4166 "});
4167 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4168 cx.assert_editor_state(indoc! {"
4169 \t«oneˇ» «twoˇ»
4170 three
4171 four
4172 "});
4173 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4174 cx.assert_editor_state(indoc! {"
4175 «oneˇ» «twoˇ»
4176 three
4177 four
4178 "});
4179
4180 // select across a line ending
4181 cx.set_state(indoc! {"
4182 one two
4183 t«hree
4184 ˇ»four
4185 "});
4186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4187 cx.assert_editor_state(indoc! {"
4188 one two
4189 \tt«hree
4190 ˇ»four
4191 "});
4192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4193 cx.assert_editor_state(indoc! {"
4194 one two
4195 \t\tt«hree
4196 ˇ»four
4197 "});
4198 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 \tt«hree
4202 ˇ»four
4203 "});
4204 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4205 cx.assert_editor_state(indoc! {"
4206 one two
4207 t«hree
4208 ˇ»four
4209 "});
4210
4211 // Ensure that indenting/outdenting works when the cursor is at column 0.
4212 cx.set_state(indoc! {"
4213 one two
4214 ˇthree
4215 four
4216 "});
4217 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 one two
4220 ˇthree
4221 four
4222 "});
4223 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4224 cx.assert_editor_state(indoc! {"
4225 one two
4226 \tˇthree
4227 four
4228 "});
4229 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4230 cx.assert_editor_state(indoc! {"
4231 one two
4232 ˇthree
4233 four
4234 "});
4235}
4236
4237#[gpui::test]
4238fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4239 init_test(cx, |settings| {
4240 settings.languages.0.extend([
4241 (
4242 "TOML".into(),
4243 LanguageSettingsContent {
4244 tab_size: NonZeroU32::new(2),
4245 ..Default::default()
4246 },
4247 ),
4248 (
4249 "Rust".into(),
4250 LanguageSettingsContent {
4251 tab_size: NonZeroU32::new(4),
4252 ..Default::default()
4253 },
4254 ),
4255 ]);
4256 });
4257
4258 let toml_language = Arc::new(Language::new(
4259 LanguageConfig {
4260 name: "TOML".into(),
4261 ..Default::default()
4262 },
4263 None,
4264 ));
4265 let rust_language = Arc::new(Language::new(
4266 LanguageConfig {
4267 name: "Rust".into(),
4268 ..Default::default()
4269 },
4270 None,
4271 ));
4272
4273 let toml_buffer =
4274 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4275 let rust_buffer =
4276 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4277 let multibuffer = cx.new(|cx| {
4278 let mut multibuffer = MultiBuffer::new(ReadWrite);
4279 multibuffer.push_excerpts(
4280 toml_buffer.clone(),
4281 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4282 cx,
4283 );
4284 multibuffer.push_excerpts(
4285 rust_buffer.clone(),
4286 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4287 cx,
4288 );
4289 multibuffer
4290 });
4291
4292 cx.add_window(|window, cx| {
4293 let mut editor = build_editor(multibuffer, window, cx);
4294
4295 assert_eq!(
4296 editor.text(cx),
4297 indoc! {"
4298 a = 1
4299 b = 2
4300
4301 const c: usize = 3;
4302 "}
4303 );
4304
4305 select_ranges(
4306 &mut editor,
4307 indoc! {"
4308 «aˇ» = 1
4309 b = 2
4310
4311 «const c:ˇ» usize = 3;
4312 "},
4313 window,
4314 cx,
4315 );
4316
4317 editor.tab(&Tab, window, cx);
4318 assert_text_with_selections(
4319 &mut editor,
4320 indoc! {"
4321 «aˇ» = 1
4322 b = 2
4323
4324 «const c:ˇ» usize = 3;
4325 "},
4326 cx,
4327 );
4328 editor.backtab(&Backtab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339
4340 editor
4341 });
4342}
4343
4344#[gpui::test]
4345async fn test_backspace(cx: &mut TestAppContext) {
4346 init_test(cx, |_| {});
4347
4348 let mut cx = EditorTestContext::new(cx).await;
4349
4350 // Basic backspace
4351 cx.set_state(indoc! {"
4352 onˇe two three
4353 fou«rˇ» five six
4354 seven «ˇeight nine
4355 »ten
4356 "});
4357 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 oˇe two three
4360 fouˇ five six
4361 seven ˇten
4362 "});
4363
4364 // Test backspace inside and around indents
4365 cx.set_state(indoc! {"
4366 zero
4367 ˇone
4368 ˇtwo
4369 ˇ ˇ ˇ three
4370 ˇ ˇ four
4371 "});
4372 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4373 cx.assert_editor_state(indoc! {"
4374 zero
4375 ˇone
4376 ˇtwo
4377 ˇ threeˇ four
4378 "});
4379}
4380
4381#[gpui::test]
4382async fn test_delete(cx: &mut TestAppContext) {
4383 init_test(cx, |_| {});
4384
4385 let mut cx = EditorTestContext::new(cx).await;
4386 cx.set_state(indoc! {"
4387 onˇe two three
4388 fou«rˇ» five six
4389 seven «ˇeight nine
4390 »ten
4391 "});
4392 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4393 cx.assert_editor_state(indoc! {"
4394 onˇ two three
4395 fouˇ five six
4396 seven ˇten
4397 "});
4398}
4399
4400#[gpui::test]
4401fn test_delete_line(cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let editor = cx.add_window(|window, cx| {
4405 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4406 build_editor(buffer, window, cx)
4407 });
4408 _ = editor.update(cx, |editor, window, cx| {
4409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4410 s.select_display_ranges([
4411 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4412 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4413 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4414 ])
4415 });
4416 editor.delete_line(&DeleteLine, window, cx);
4417 assert_eq!(editor.display_text(cx), "ghi");
4418 assert_eq!(
4419 editor.selections.display_ranges(cx),
4420 vec![
4421 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 ]
4424 );
4425 });
4426
4427 let editor = cx.add_window(|window, cx| {
4428 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4429 build_editor(buffer, window, cx)
4430 });
4431 _ = editor.update(cx, |editor, window, cx| {
4432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4433 s.select_display_ranges([
4434 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4435 ])
4436 });
4437 editor.delete_line(&DeleteLine, window, cx);
4438 assert_eq!(editor.display_text(cx), "ghi\n");
4439 assert_eq!(
4440 editor.selections.display_ranges(cx),
4441 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4442 );
4443 });
4444
4445 let editor = cx.add_window(|window, cx| {
4446 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4447 build_editor(buffer, window, cx)
4448 });
4449 _ = editor.update(cx, |editor, window, cx| {
4450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4451 s.select_display_ranges([
4452 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4453 ])
4454 });
4455 editor.delete_line(&DeleteLine, window, cx);
4456 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4457 assert_eq!(
4458 editor.selections.display_ranges(cx),
4459 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4460 );
4461 });
4462}
4463
4464#[gpui::test]
4465fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4466 init_test(cx, |_| {});
4467
4468 cx.add_window(|window, cx| {
4469 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4470 let mut editor = build_editor(buffer.clone(), window, cx);
4471 let buffer = buffer.read(cx).as_singleton().unwrap();
4472
4473 assert_eq!(
4474 editor
4475 .selections
4476 .ranges::<Point>(&editor.display_snapshot(cx)),
4477 &[Point::new(0, 0)..Point::new(0, 0)]
4478 );
4479
4480 // When on single line, replace newline at end by space
4481 editor.join_lines(&JoinLines, window, cx);
4482 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4483 assert_eq!(
4484 editor
4485 .selections
4486 .ranges::<Point>(&editor.display_snapshot(cx)),
4487 &[Point::new(0, 3)..Point::new(0, 3)]
4488 );
4489
4490 // When multiple lines are selected, remove newlines that are spanned by the selection
4491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4492 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4493 });
4494 editor.join_lines(&JoinLines, window, cx);
4495 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4496 assert_eq!(
4497 editor
4498 .selections
4499 .ranges::<Point>(&editor.display_snapshot(cx)),
4500 &[Point::new(0, 11)..Point::new(0, 11)]
4501 );
4502
4503 // Undo should be transactional
4504 editor.undo(&Undo, window, cx);
4505 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4506 assert_eq!(
4507 editor
4508 .selections
4509 .ranges::<Point>(&editor.display_snapshot(cx)),
4510 &[Point::new(0, 5)..Point::new(2, 2)]
4511 );
4512
4513 // When joining an empty line don't insert a space
4514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4515 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4516 });
4517 editor.join_lines(&JoinLines, window, cx);
4518 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4519 assert_eq!(
4520 editor
4521 .selections
4522 .ranges::<Point>(&editor.display_snapshot(cx)),
4523 [Point::new(2, 3)..Point::new(2, 3)]
4524 );
4525
4526 // We can remove trailing newlines
4527 editor.join_lines(&JoinLines, window, cx);
4528 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 [Point::new(2, 3)..Point::new(2, 3)]
4534 );
4535
4536 // We don't blow up on the last line
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 [Point::new(2, 3)..Point::new(2, 3)]
4544 );
4545
4546 // reset to test indentation
4547 editor.buffer.update(cx, |buffer, cx| {
4548 buffer.edit(
4549 [
4550 (Point::new(1, 0)..Point::new(1, 2), " "),
4551 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4552 ],
4553 None,
4554 cx,
4555 )
4556 });
4557
4558 // We remove any leading spaces
4559 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4560 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4561 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4562 });
4563 editor.join_lines(&JoinLines, window, cx);
4564 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4565
4566 // We don't insert a space for a line containing only spaces
4567 editor.join_lines(&JoinLines, window, cx);
4568 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4569
4570 // We ignore any leading tabs
4571 editor.join_lines(&JoinLines, window, cx);
4572 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4573
4574 editor
4575 });
4576}
4577
4578#[gpui::test]
4579fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4580 init_test(cx, |_| {});
4581
4582 cx.add_window(|window, cx| {
4583 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4584 let mut editor = build_editor(buffer.clone(), window, cx);
4585 let buffer = buffer.read(cx).as_singleton().unwrap();
4586
4587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4588 s.select_ranges([
4589 Point::new(0, 2)..Point::new(1, 1),
4590 Point::new(1, 2)..Point::new(1, 2),
4591 Point::new(3, 1)..Point::new(3, 2),
4592 ])
4593 });
4594
4595 editor.join_lines(&JoinLines, window, cx);
4596 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4597
4598 assert_eq!(
4599 editor
4600 .selections
4601 .ranges::<Point>(&editor.display_snapshot(cx)),
4602 [
4603 Point::new(0, 7)..Point::new(0, 7),
4604 Point::new(1, 3)..Point::new(1, 3)
4605 ]
4606 );
4607 editor
4608 });
4609}
4610
4611#[gpui::test]
4612async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4613 init_test(cx, |_| {});
4614
4615 let mut cx = EditorTestContext::new(cx).await;
4616
4617 let diff_base = r#"
4618 Line 0
4619 Line 1
4620 Line 2
4621 Line 3
4622 "#
4623 .unindent();
4624
4625 cx.set_state(
4626 &r#"
4627 ˇLine 0
4628 Line 1
4629 Line 2
4630 Line 3
4631 "#
4632 .unindent(),
4633 );
4634
4635 cx.set_head_text(&diff_base);
4636 executor.run_until_parked();
4637
4638 // Join lines
4639 cx.update_editor(|editor, window, cx| {
4640 editor.join_lines(&JoinLines, window, cx);
4641 });
4642 executor.run_until_parked();
4643
4644 cx.assert_editor_state(
4645 &r#"
4646 Line 0ˇ Line 1
4647 Line 2
4648 Line 3
4649 "#
4650 .unindent(),
4651 );
4652 // Join again
4653 cx.update_editor(|editor, window, cx| {
4654 editor.join_lines(&JoinLines, window, cx);
4655 });
4656 executor.run_until_parked();
4657
4658 cx.assert_editor_state(
4659 &r#"
4660 Line 0 Line 1ˇ Line 2
4661 Line 3
4662 "#
4663 .unindent(),
4664 );
4665}
4666
4667#[gpui::test]
4668async fn test_custom_newlines_cause_no_false_positive_diffs(
4669 executor: BackgroundExecutor,
4670 cx: &mut TestAppContext,
4671) {
4672 init_test(cx, |_| {});
4673 let mut cx = EditorTestContext::new(cx).await;
4674 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4675 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4676 executor.run_until_parked();
4677
4678 cx.update_editor(|editor, window, cx| {
4679 let snapshot = editor.snapshot(window, cx);
4680 assert_eq!(
4681 snapshot
4682 .buffer_snapshot()
4683 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4684 .collect::<Vec<_>>(),
4685 Vec::new(),
4686 "Should not have any diffs for files with custom newlines"
4687 );
4688 });
4689}
4690
4691#[gpui::test]
4692async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4693 init_test(cx, |_| {});
4694
4695 let mut cx = EditorTestContext::new(cx).await;
4696
4697 // Test sort_lines_case_insensitive()
4698 cx.set_state(indoc! {"
4699 «z
4700 y
4701 x
4702 Z
4703 Y
4704 Xˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| {
4707 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4708 });
4709 cx.assert_editor_state(indoc! {"
4710 «x
4711 X
4712 y
4713 Y
4714 z
4715 Zˇ»
4716 "});
4717
4718 // Test sort_lines_by_length()
4719 //
4720 // Demonstrates:
4721 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4722 // - sort is stable
4723 cx.set_state(indoc! {"
4724 «123
4725 æ
4726 12
4727 ∞
4728 1
4729 æˇ»
4730 "});
4731 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4732 cx.assert_editor_state(indoc! {"
4733 «æ
4734 ∞
4735 1
4736 æ
4737 12
4738 123ˇ»
4739 "});
4740
4741 // Test reverse_lines()
4742 cx.set_state(indoc! {"
4743 «5
4744 4
4745 3
4746 2
4747 1ˇ»
4748 "});
4749 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4750 cx.assert_editor_state(indoc! {"
4751 «1
4752 2
4753 3
4754 4
4755 5ˇ»
4756 "});
4757
4758 // Skip testing shuffle_line()
4759
4760 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4761 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4762
4763 // Don't manipulate when cursor is on single line, but expand the selection
4764 cx.set_state(indoc! {"
4765 ddˇdd
4766 ccc
4767 bb
4768 a
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «ddddˇ»
4775 ccc
4776 bb
4777 a
4778 "});
4779
4780 // Basic manipulate case
4781 // Start selection moves to column 0
4782 // End of selection shrinks to fit shorter line
4783 cx.set_state(indoc! {"
4784 dd«d
4785 ccc
4786 bb
4787 aaaaaˇ»
4788 "});
4789 cx.update_editor(|e, window, cx| {
4790 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4791 });
4792 cx.assert_editor_state(indoc! {"
4793 «aaaaa
4794 bb
4795 ccc
4796 dddˇ»
4797 "});
4798
4799 // Manipulate case with newlines
4800 cx.set_state(indoc! {"
4801 dd«d
4802 ccc
4803
4804 bb
4805 aaaaa
4806
4807 ˇ»
4808 "});
4809 cx.update_editor(|e, window, cx| {
4810 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4811 });
4812 cx.assert_editor_state(indoc! {"
4813 «
4814
4815 aaaaa
4816 bb
4817 ccc
4818 dddˇ»
4819
4820 "});
4821
4822 // Adding new line
4823 cx.set_state(indoc! {"
4824 aa«a
4825 bbˇ»b
4826 "});
4827 cx.update_editor(|e, window, cx| {
4828 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4829 });
4830 cx.assert_editor_state(indoc! {"
4831 «aaa
4832 bbb
4833 added_lineˇ»
4834 "});
4835
4836 // Removing line
4837 cx.set_state(indoc! {"
4838 aa«a
4839 bbbˇ»
4840 "});
4841 cx.update_editor(|e, window, cx| {
4842 e.manipulate_immutable_lines(window, cx, |lines| {
4843 lines.pop();
4844 })
4845 });
4846 cx.assert_editor_state(indoc! {"
4847 «aaaˇ»
4848 "});
4849
4850 // Removing all lines
4851 cx.set_state(indoc! {"
4852 aa«a
4853 bbbˇ»
4854 "});
4855 cx.update_editor(|e, window, cx| {
4856 e.manipulate_immutable_lines(window, cx, |lines| {
4857 lines.drain(..);
4858 })
4859 });
4860 cx.assert_editor_state(indoc! {"
4861 ˇ
4862 "});
4863}
4864
4865#[gpui::test]
4866async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4867 init_test(cx, |_| {});
4868
4869 let mut cx = EditorTestContext::new(cx).await;
4870
4871 // Consider continuous selection as single selection
4872 cx.set_state(indoc! {"
4873 Aaa«aa
4874 cˇ»c«c
4875 bb
4876 aaaˇ»aa
4877 "});
4878 cx.update_editor(|e, window, cx| {
4879 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4880 });
4881 cx.assert_editor_state(indoc! {"
4882 «Aaaaa
4883 ccc
4884 bb
4885 aaaaaˇ»
4886 "});
4887
4888 cx.set_state(indoc! {"
4889 Aaa«aa
4890 cˇ»c«c
4891 bb
4892 aaaˇ»aa
4893 "});
4894 cx.update_editor(|e, window, cx| {
4895 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4896 });
4897 cx.assert_editor_state(indoc! {"
4898 «Aaaaa
4899 ccc
4900 bbˇ»
4901 "});
4902
4903 // Consider non continuous selection as distinct dedup operations
4904 cx.set_state(indoc! {"
4905 «aaaaa
4906 bb
4907 aaaaa
4908 aaaaaˇ»
4909
4910 aaa«aaˇ»
4911 "});
4912 cx.update_editor(|e, window, cx| {
4913 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4914 });
4915 cx.assert_editor_state(indoc! {"
4916 «aaaaa
4917 bbˇ»
4918
4919 «aaaaaˇ»
4920 "});
4921}
4922
4923#[gpui::test]
4924async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4925 init_test(cx, |_| {});
4926
4927 let mut cx = EditorTestContext::new(cx).await;
4928
4929 cx.set_state(indoc! {"
4930 «Aaa
4931 aAa
4932 Aaaˇ»
4933 "});
4934 cx.update_editor(|e, window, cx| {
4935 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4936 });
4937 cx.assert_editor_state(indoc! {"
4938 «Aaa
4939 aAaˇ»
4940 "});
4941
4942 cx.set_state(indoc! {"
4943 «Aaa
4944 aAa
4945 aaAˇ»
4946 "});
4947 cx.update_editor(|e, window, cx| {
4948 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4949 });
4950 cx.assert_editor_state(indoc! {"
4951 «Aaaˇ»
4952 "});
4953}
4954
4955#[gpui::test]
4956async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4957 init_test(cx, |_| {});
4958
4959 let mut cx = EditorTestContext::new(cx).await;
4960
4961 let js_language = Arc::new(Language::new(
4962 LanguageConfig {
4963 name: "JavaScript".into(),
4964 wrap_characters: Some(language::WrapCharactersConfig {
4965 start_prefix: "<".into(),
4966 start_suffix: ">".into(),
4967 end_prefix: "</".into(),
4968 end_suffix: ">".into(),
4969 }),
4970 ..LanguageConfig::default()
4971 },
4972 None,
4973 ));
4974
4975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4976
4977 cx.set_state(indoc! {"
4978 «testˇ»
4979 "});
4980 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4981 cx.assert_editor_state(indoc! {"
4982 <«ˇ»>test</«ˇ»>
4983 "});
4984
4985 cx.set_state(indoc! {"
4986 «test
4987 testˇ»
4988 "});
4989 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4990 cx.assert_editor_state(indoc! {"
4991 <«ˇ»>test
4992 test</«ˇ»>
4993 "});
4994
4995 cx.set_state(indoc! {"
4996 teˇst
4997 "});
4998 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4999 cx.assert_editor_state(indoc! {"
5000 te<«ˇ»></«ˇ»>st
5001 "});
5002}
5003
5004#[gpui::test]
5005async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5006 init_test(cx, |_| {});
5007
5008 let mut cx = EditorTestContext::new(cx).await;
5009
5010 let js_language = Arc::new(Language::new(
5011 LanguageConfig {
5012 name: "JavaScript".into(),
5013 wrap_characters: Some(language::WrapCharactersConfig {
5014 start_prefix: "<".into(),
5015 start_suffix: ">".into(),
5016 end_prefix: "</".into(),
5017 end_suffix: ">".into(),
5018 }),
5019 ..LanguageConfig::default()
5020 },
5021 None,
5022 ));
5023
5024 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5025
5026 cx.set_state(indoc! {"
5027 «testˇ»
5028 «testˇ» «testˇ»
5029 «testˇ»
5030 "});
5031 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5032 cx.assert_editor_state(indoc! {"
5033 <«ˇ»>test</«ˇ»>
5034 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5035 <«ˇ»>test</«ˇ»>
5036 "});
5037
5038 cx.set_state(indoc! {"
5039 «test
5040 testˇ»
5041 «test
5042 testˇ»
5043 "});
5044 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5045 cx.assert_editor_state(indoc! {"
5046 <«ˇ»>test
5047 test</«ˇ»>
5048 <«ˇ»>test
5049 test</«ˇ»>
5050 "});
5051}
5052
5053#[gpui::test]
5054async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5055 init_test(cx, |_| {});
5056
5057 let mut cx = EditorTestContext::new(cx).await;
5058
5059 let plaintext_language = Arc::new(Language::new(
5060 LanguageConfig {
5061 name: "Plain Text".into(),
5062 ..LanguageConfig::default()
5063 },
5064 None,
5065 ));
5066
5067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5068
5069 cx.set_state(indoc! {"
5070 «testˇ»
5071 "});
5072 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5073 cx.assert_editor_state(indoc! {"
5074 «testˇ»
5075 "});
5076}
5077
5078#[gpui::test]
5079async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5080 init_test(cx, |_| {});
5081
5082 let mut cx = EditorTestContext::new(cx).await;
5083
5084 // Manipulate with multiple selections on a single line
5085 cx.set_state(indoc! {"
5086 dd«dd
5087 cˇ»c«c
5088 bb
5089 aaaˇ»aa
5090 "});
5091 cx.update_editor(|e, window, cx| {
5092 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5093 });
5094 cx.assert_editor_state(indoc! {"
5095 «aaaaa
5096 bb
5097 ccc
5098 ddddˇ»
5099 "});
5100
5101 // Manipulate with multiple disjoin selections
5102 cx.set_state(indoc! {"
5103 5«
5104 4
5105 3
5106 2
5107 1ˇ»
5108
5109 dd«dd
5110 ccc
5111 bb
5112 aaaˇ»aa
5113 "});
5114 cx.update_editor(|e, window, cx| {
5115 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5116 });
5117 cx.assert_editor_state(indoc! {"
5118 «1
5119 2
5120 3
5121 4
5122 5ˇ»
5123
5124 «aaaaa
5125 bb
5126 ccc
5127 ddddˇ»
5128 "});
5129
5130 // Adding lines on each selection
5131 cx.set_state(indoc! {"
5132 2«
5133 1ˇ»
5134
5135 bb«bb
5136 aaaˇ»aa
5137 "});
5138 cx.update_editor(|e, window, cx| {
5139 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5140 });
5141 cx.assert_editor_state(indoc! {"
5142 «2
5143 1
5144 added lineˇ»
5145
5146 «bbbb
5147 aaaaa
5148 added lineˇ»
5149 "});
5150
5151 // Removing lines on each selection
5152 cx.set_state(indoc! {"
5153 2«
5154 1ˇ»
5155
5156 bb«bb
5157 aaaˇ»aa
5158 "});
5159 cx.update_editor(|e, window, cx| {
5160 e.manipulate_immutable_lines(window, cx, |lines| {
5161 lines.pop();
5162 })
5163 });
5164 cx.assert_editor_state(indoc! {"
5165 «2ˇ»
5166
5167 «bbbbˇ»
5168 "});
5169}
5170
5171#[gpui::test]
5172async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5173 init_test(cx, |settings| {
5174 settings.defaults.tab_size = NonZeroU32::new(3)
5175 });
5176
5177 let mut cx = EditorTestContext::new(cx).await;
5178
5179 // MULTI SELECTION
5180 // Ln.1 "«" tests empty lines
5181 // Ln.9 tests just leading whitespace
5182 cx.set_state(indoc! {"
5183 «
5184 abc // No indentationˇ»
5185 «\tabc // 1 tabˇ»
5186 \t\tabc « ˇ» // 2 tabs
5187 \t ab«c // Tab followed by space
5188 \tabc // Space followed by tab (3 spaces should be the result)
5189 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5190 abˇ»ˇc ˇ ˇ // Already space indented«
5191 \t
5192 \tabc\tdef // Only the leading tab is manipulatedˇ»
5193 "});
5194 cx.update_editor(|e, window, cx| {
5195 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5196 });
5197 cx.assert_editor_state(
5198 indoc! {"
5199 «
5200 abc // No indentation
5201 abc // 1 tab
5202 abc // 2 tabs
5203 abc // Tab followed by space
5204 abc // Space followed by tab (3 spaces should be the result)
5205 abc // Mixed indentation (tab conversion depends on the column)
5206 abc // Already space indented
5207 ·
5208 abc\tdef // Only the leading tab is manipulatedˇ»
5209 "}
5210 .replace("·", "")
5211 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5212 );
5213
5214 // Test on just a few lines, the others should remain unchanged
5215 // Only lines (3, 5, 10, 11) should change
5216 cx.set_state(
5217 indoc! {"
5218 ·
5219 abc // No indentation
5220 \tabcˇ // 1 tab
5221 \t\tabc // 2 tabs
5222 \t abcˇ // Tab followed by space
5223 \tabc // Space followed by tab (3 spaces should be the result)
5224 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5225 abc // Already space indented
5226 «\t
5227 \tabc\tdef // Only the leading tab is manipulatedˇ»
5228 "}
5229 .replace("·", "")
5230 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5231 );
5232 cx.update_editor(|e, window, cx| {
5233 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5234 });
5235 cx.assert_editor_state(
5236 indoc! {"
5237 ·
5238 abc // No indentation
5239 « abc // 1 tabˇ»
5240 \t\tabc // 2 tabs
5241 « abc // Tab followed by spaceˇ»
5242 \tabc // Space followed by tab (3 spaces should be the result)
5243 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5244 abc // Already space indented
5245 « ·
5246 abc\tdef // Only the leading tab is manipulatedˇ»
5247 "}
5248 .replace("·", "")
5249 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5250 );
5251
5252 // SINGLE SELECTION
5253 // Ln.1 "«" tests empty lines
5254 // Ln.9 tests just leading whitespace
5255 cx.set_state(indoc! {"
5256 «
5257 abc // No indentation
5258 \tabc // 1 tab
5259 \t\tabc // 2 tabs
5260 \t abc // Tab followed by space
5261 \tabc // Space followed by tab (3 spaces should be the result)
5262 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5263 abc // Already space indented
5264 \t
5265 \tabc\tdef // Only the leading tab is manipulatedˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| {
5268 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5269 });
5270 cx.assert_editor_state(
5271 indoc! {"
5272 «
5273 abc // No indentation
5274 abc // 1 tab
5275 abc // 2 tabs
5276 abc // Tab followed by space
5277 abc // Space followed by tab (3 spaces should be the result)
5278 abc // Mixed indentation (tab conversion depends on the column)
5279 abc // Already space indented
5280 ·
5281 abc\tdef // Only the leading tab is manipulatedˇ»
5282 "}
5283 .replace("·", "")
5284 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5285 );
5286}
5287
5288#[gpui::test]
5289async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5290 init_test(cx, |settings| {
5291 settings.defaults.tab_size = NonZeroU32::new(3)
5292 });
5293
5294 let mut cx = EditorTestContext::new(cx).await;
5295
5296 // MULTI SELECTION
5297 // Ln.1 "«" tests empty lines
5298 // Ln.11 tests just leading whitespace
5299 cx.set_state(indoc! {"
5300 «
5301 abˇ»ˇc // No indentation
5302 abc ˇ ˇ // 1 space (< 3 so dont convert)
5303 abc « // 2 spaces (< 3 so dont convert)
5304 abc // 3 spaces (convert)
5305 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5306 «\tˇ»\t«\tˇ»abc // Already tab indented
5307 «\t abc // Tab followed by space
5308 \tabc // Space followed by tab (should be consumed due to tab)
5309 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5310 \tˇ» «\t
5311 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5312 "});
5313 cx.update_editor(|e, window, cx| {
5314 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5315 });
5316 cx.assert_editor_state(indoc! {"
5317 «
5318 abc // No indentation
5319 abc // 1 space (< 3 so dont convert)
5320 abc // 2 spaces (< 3 so dont convert)
5321 \tabc // 3 spaces (convert)
5322 \t abc // 5 spaces (1 tab + 2 spaces)
5323 \t\t\tabc // Already tab indented
5324 \t abc // Tab followed by space
5325 \tabc // Space followed by tab (should be consumed due to tab)
5326 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5327 \t\t\t
5328 \tabc \t // Only the leading spaces should be convertedˇ»
5329 "});
5330
5331 // Test on just a few lines, the other should remain unchanged
5332 // Only lines (4, 8, 11, 12) should change
5333 cx.set_state(
5334 indoc! {"
5335 ·
5336 abc // No indentation
5337 abc // 1 space (< 3 so dont convert)
5338 abc // 2 spaces (< 3 so dont convert)
5339 « abc // 3 spaces (convert)ˇ»
5340 abc // 5 spaces (1 tab + 2 spaces)
5341 \t\t\tabc // Already tab indented
5342 \t abc // Tab followed by space
5343 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5344 \t\t \tabc // Mixed indentation
5345 \t \t \t \tabc // Mixed indentation
5346 \t \tˇ
5347 « abc \t // Only the leading spaces should be convertedˇ»
5348 "}
5349 .replace("·", "")
5350 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5351 );
5352 cx.update_editor(|e, window, cx| {
5353 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5354 });
5355 cx.assert_editor_state(
5356 indoc! {"
5357 ·
5358 abc // No indentation
5359 abc // 1 space (< 3 so dont convert)
5360 abc // 2 spaces (< 3 so dont convert)
5361 «\tabc // 3 spaces (convert)ˇ»
5362 abc // 5 spaces (1 tab + 2 spaces)
5363 \t\t\tabc // Already tab indented
5364 \t abc // Tab followed by space
5365 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5366 \t\t \tabc // Mixed indentation
5367 \t \t \t \tabc // Mixed indentation
5368 «\t\t\t
5369 \tabc \t // Only the leading spaces should be convertedˇ»
5370 "}
5371 .replace("·", "")
5372 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5373 );
5374
5375 // SINGLE SELECTION
5376 // Ln.1 "«" tests empty lines
5377 // Ln.11 tests just leading whitespace
5378 cx.set_state(indoc! {"
5379 «
5380 abc // No indentation
5381 abc // 1 space (< 3 so dont convert)
5382 abc // 2 spaces (< 3 so dont convert)
5383 abc // 3 spaces (convert)
5384 abc // 5 spaces (1 tab + 2 spaces)
5385 \t\t\tabc // Already tab indented
5386 \t abc // Tab followed by space
5387 \tabc // Space followed by tab (should be consumed due to tab)
5388 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5389 \t \t
5390 abc \t // Only the leading spaces should be convertedˇ»
5391 "});
5392 cx.update_editor(|e, window, cx| {
5393 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5394 });
5395 cx.assert_editor_state(indoc! {"
5396 «
5397 abc // No indentation
5398 abc // 1 space (< 3 so dont convert)
5399 abc // 2 spaces (< 3 so dont convert)
5400 \tabc // 3 spaces (convert)
5401 \t abc // 5 spaces (1 tab + 2 spaces)
5402 \t\t\tabc // Already tab indented
5403 \t abc // Tab followed by space
5404 \tabc // Space followed by tab (should be consumed due to tab)
5405 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5406 \t\t\t
5407 \tabc \t // Only the leading spaces should be convertedˇ»
5408 "});
5409}
5410
5411#[gpui::test]
5412async fn test_toggle_case(cx: &mut TestAppContext) {
5413 init_test(cx, |_| {});
5414
5415 let mut cx = EditorTestContext::new(cx).await;
5416
5417 // If all lower case -> upper case
5418 cx.set_state(indoc! {"
5419 «hello worldˇ»
5420 "});
5421 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5422 cx.assert_editor_state(indoc! {"
5423 «HELLO WORLDˇ»
5424 "});
5425
5426 // If all upper case -> lower case
5427 cx.set_state(indoc! {"
5428 «HELLO WORLDˇ»
5429 "});
5430 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5431 cx.assert_editor_state(indoc! {"
5432 «hello worldˇ»
5433 "});
5434
5435 // If any upper case characters are identified -> lower case
5436 // This matches JetBrains IDEs
5437 cx.set_state(indoc! {"
5438 «hEllo worldˇ»
5439 "});
5440 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5441 cx.assert_editor_state(indoc! {"
5442 «hello worldˇ»
5443 "});
5444}
5445
5446#[gpui::test]
5447async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5448 init_test(cx, |_| {});
5449
5450 let mut cx = EditorTestContext::new(cx).await;
5451
5452 cx.set_state(indoc! {"
5453 «implement-windows-supportˇ»
5454 "});
5455 cx.update_editor(|e, window, cx| {
5456 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5457 });
5458 cx.assert_editor_state(indoc! {"
5459 «Implement windows supportˇ»
5460 "});
5461}
5462
5463#[gpui::test]
5464async fn test_manipulate_text(cx: &mut TestAppContext) {
5465 init_test(cx, |_| {});
5466
5467 let mut cx = EditorTestContext::new(cx).await;
5468
5469 // Test convert_to_upper_case()
5470 cx.set_state(indoc! {"
5471 «hello worldˇ»
5472 "});
5473 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5474 cx.assert_editor_state(indoc! {"
5475 «HELLO WORLDˇ»
5476 "});
5477
5478 // Test convert_to_lower_case()
5479 cx.set_state(indoc! {"
5480 «HELLO WORLDˇ»
5481 "});
5482 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5483 cx.assert_editor_state(indoc! {"
5484 «hello worldˇ»
5485 "});
5486
5487 // Test multiple line, single selection case
5488 cx.set_state(indoc! {"
5489 «The quick brown
5490 fox jumps over
5491 the lazy dogˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «The Quick Brown
5496 Fox Jumps Over
5497 The Lazy Dogˇ»
5498 "});
5499
5500 // Test multiple line, single selection case
5501 cx.set_state(indoc! {"
5502 «The quick brown
5503 fox jumps over
5504 the lazy dogˇ»
5505 "});
5506 cx.update_editor(|e, window, cx| {
5507 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5508 });
5509 cx.assert_editor_state(indoc! {"
5510 «TheQuickBrown
5511 FoxJumpsOver
5512 TheLazyDogˇ»
5513 "});
5514
5515 // From here on out, test more complex cases of manipulate_text()
5516
5517 // Test no selection case - should affect words cursors are in
5518 // Cursor at beginning, middle, and end of word
5519 cx.set_state(indoc! {"
5520 ˇhello big beauˇtiful worldˇ
5521 "});
5522 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5523 cx.assert_editor_state(indoc! {"
5524 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5525 "});
5526
5527 // Test multiple selections on a single line and across multiple lines
5528 cx.set_state(indoc! {"
5529 «Theˇ» quick «brown
5530 foxˇ» jumps «overˇ»
5531 the «lazyˇ» dog
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «THEˇ» quick «BROWN
5536 FOXˇ» jumps «OVERˇ»
5537 the «LAZYˇ» dog
5538 "});
5539
5540 // Test case where text length grows
5541 cx.set_state(indoc! {"
5542 «tschüߡ»
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «TSCHÜSSˇ»
5547 "});
5548
5549 // Test to make sure we don't crash when text shrinks
5550 cx.set_state(indoc! {"
5551 aaa_bbbˇ
5552 "});
5553 cx.update_editor(|e, window, cx| {
5554 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5555 });
5556 cx.assert_editor_state(indoc! {"
5557 «aaaBbbˇ»
5558 "});
5559
5560 // Test to make sure we all aware of the fact that each word can grow and shrink
5561 // Final selections should be aware of this fact
5562 cx.set_state(indoc! {"
5563 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5564 "});
5565 cx.update_editor(|e, window, cx| {
5566 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5567 });
5568 cx.assert_editor_state(indoc! {"
5569 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5570 "});
5571
5572 cx.set_state(indoc! {"
5573 «hElLo, WoRld!ˇ»
5574 "});
5575 cx.update_editor(|e, window, cx| {
5576 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5577 });
5578 cx.assert_editor_state(indoc! {"
5579 «HeLlO, wOrLD!ˇ»
5580 "});
5581
5582 // Test selections with `line_mode() = true`.
5583 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5584 cx.set_state(indoc! {"
5585 «The quick brown
5586 fox jumps over
5587 tˇ»he lazy dog
5588 "});
5589 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 «THE QUICK BROWN
5592 FOX JUMPS OVER
5593 THE LAZY DOGˇ»
5594 "});
5595}
5596
5597#[gpui::test]
5598fn test_duplicate_line(cx: &mut TestAppContext) {
5599 init_test(cx, |_| {});
5600
5601 let editor = cx.add_window(|window, cx| {
5602 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5603 build_editor(buffer, window, cx)
5604 });
5605 _ = editor.update(cx, |editor, window, cx| {
5606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5607 s.select_display_ranges([
5608 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5609 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5610 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5611 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5612 ])
5613 });
5614 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5615 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5616 assert_eq!(
5617 editor.selections.display_ranges(cx),
5618 vec![
5619 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5620 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5621 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5622 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5623 ]
5624 );
5625 });
5626
5627 let editor = cx.add_window(|window, cx| {
5628 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5629 build_editor(buffer, window, cx)
5630 });
5631 _ = editor.update(cx, |editor, window, cx| {
5632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5633 s.select_display_ranges([
5634 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5635 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5636 ])
5637 });
5638 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5639 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5640 assert_eq!(
5641 editor.selections.display_ranges(cx),
5642 vec![
5643 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5644 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5645 ]
5646 );
5647 });
5648
5649 // With `move_upwards` the selections stay in place, except for
5650 // the lines inserted above them
5651 let editor = cx.add_window(|window, cx| {
5652 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5653 build_editor(buffer, window, cx)
5654 });
5655 _ = editor.update(cx, |editor, window, cx| {
5656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5657 s.select_display_ranges([
5658 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5659 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5660 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5661 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5662 ])
5663 });
5664 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5665 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5666 assert_eq!(
5667 editor.selections.display_ranges(cx),
5668 vec![
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5672 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5673 ]
5674 );
5675 });
5676
5677 let editor = cx.add_window(|window, cx| {
5678 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5679 build_editor(buffer, window, cx)
5680 });
5681 _ = editor.update(cx, |editor, window, cx| {
5682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5683 s.select_display_ranges([
5684 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5685 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5686 ])
5687 });
5688 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5689 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5690 assert_eq!(
5691 editor.selections.display_ranges(cx),
5692 vec![
5693 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5694 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5695 ]
5696 );
5697 });
5698
5699 let editor = cx.add_window(|window, cx| {
5700 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5701 build_editor(buffer, window, cx)
5702 });
5703 _ = editor.update(cx, |editor, window, cx| {
5704 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5705 s.select_display_ranges([
5706 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5707 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5708 ])
5709 });
5710 editor.duplicate_selection(&DuplicateSelection, window, cx);
5711 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5712 assert_eq!(
5713 editor.selections.display_ranges(cx),
5714 vec![
5715 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5716 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5717 ]
5718 );
5719 });
5720}
5721
5722#[gpui::test]
5723fn test_move_line_up_down(cx: &mut TestAppContext) {
5724 init_test(cx, |_| {});
5725
5726 let editor = cx.add_window(|window, cx| {
5727 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5728 build_editor(buffer, window, cx)
5729 });
5730 _ = editor.update(cx, |editor, window, cx| {
5731 editor.fold_creases(
5732 vec![
5733 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5734 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5735 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5736 ],
5737 true,
5738 window,
5739 cx,
5740 );
5741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5742 s.select_display_ranges([
5743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5744 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5745 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5746 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5747 ])
5748 });
5749 assert_eq!(
5750 editor.display_text(cx),
5751 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5752 );
5753
5754 editor.move_line_up(&MoveLineUp, window, cx);
5755 assert_eq!(
5756 editor.display_text(cx),
5757 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5758 );
5759 assert_eq!(
5760 editor.selections.display_ranges(cx),
5761 vec![
5762 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5763 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5764 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5765 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5766 ]
5767 );
5768 });
5769
5770 _ = editor.update(cx, |editor, window, cx| {
5771 editor.move_line_down(&MoveLineDown, window, cx);
5772 assert_eq!(
5773 editor.display_text(cx),
5774 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5775 );
5776 assert_eq!(
5777 editor.selections.display_ranges(cx),
5778 vec![
5779 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5780 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5781 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5782 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5783 ]
5784 );
5785 });
5786
5787 _ = editor.update(cx, |editor, window, cx| {
5788 editor.move_line_down(&MoveLineDown, window, cx);
5789 assert_eq!(
5790 editor.display_text(cx),
5791 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5792 );
5793 assert_eq!(
5794 editor.selections.display_ranges(cx),
5795 vec![
5796 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5797 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5798 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5799 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5800 ]
5801 );
5802 });
5803
5804 _ = editor.update(cx, |editor, window, cx| {
5805 editor.move_line_up(&MoveLineUp, window, cx);
5806 assert_eq!(
5807 editor.display_text(cx),
5808 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5809 );
5810 assert_eq!(
5811 editor.selections.display_ranges(cx),
5812 vec![
5813 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5814 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5815 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5816 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5817 ]
5818 );
5819 });
5820}
5821
5822#[gpui::test]
5823fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825 let editor = cx.add_window(|window, cx| {
5826 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5827 build_editor(buffer, window, cx)
5828 });
5829 _ = editor.update(cx, |editor, window, cx| {
5830 editor.fold_creases(
5831 vec![Crease::simple(
5832 Point::new(6, 4)..Point::new(7, 4),
5833 FoldPlaceholder::test(),
5834 )],
5835 true,
5836 window,
5837 cx,
5838 );
5839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5840 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5841 });
5842 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5843 editor.move_line_up(&MoveLineUp, window, cx);
5844 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5845 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5846 });
5847}
5848
5849#[gpui::test]
5850fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5851 init_test(cx, |_| {});
5852
5853 let editor = cx.add_window(|window, cx| {
5854 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5855 build_editor(buffer, window, cx)
5856 });
5857 _ = editor.update(cx, |editor, window, cx| {
5858 let snapshot = editor.buffer.read(cx).snapshot(cx);
5859 editor.insert_blocks(
5860 [BlockProperties {
5861 style: BlockStyle::Fixed,
5862 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5863 height: Some(1),
5864 render: Arc::new(|_| div().into_any()),
5865 priority: 0,
5866 }],
5867 Some(Autoscroll::fit()),
5868 cx,
5869 );
5870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5871 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5872 });
5873 editor.move_line_down(&MoveLineDown, window, cx);
5874 });
5875}
5876
5877#[gpui::test]
5878async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5879 init_test(cx, |_| {});
5880
5881 let mut cx = EditorTestContext::new(cx).await;
5882 cx.set_state(
5883 &"
5884 ˇzero
5885 one
5886 two
5887 three
5888 four
5889 five
5890 "
5891 .unindent(),
5892 );
5893
5894 // Create a four-line block that replaces three lines of text.
5895 cx.update_editor(|editor, window, cx| {
5896 let snapshot = editor.snapshot(window, cx);
5897 let snapshot = &snapshot.buffer_snapshot();
5898 let placement = BlockPlacement::Replace(
5899 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5900 );
5901 editor.insert_blocks(
5902 [BlockProperties {
5903 placement,
5904 height: Some(4),
5905 style: BlockStyle::Sticky,
5906 render: Arc::new(|_| gpui::div().into_any_element()),
5907 priority: 0,
5908 }],
5909 None,
5910 cx,
5911 );
5912 });
5913
5914 // Move down so that the cursor touches the block.
5915 cx.update_editor(|editor, window, cx| {
5916 editor.move_down(&Default::default(), window, cx);
5917 });
5918 cx.assert_editor_state(
5919 &"
5920 zero
5921 «one
5922 two
5923 threeˇ»
5924 four
5925 five
5926 "
5927 .unindent(),
5928 );
5929
5930 // Move down past the block.
5931 cx.update_editor(|editor, window, cx| {
5932 editor.move_down(&Default::default(), window, cx);
5933 });
5934 cx.assert_editor_state(
5935 &"
5936 zero
5937 one
5938 two
5939 three
5940 ˇfour
5941 five
5942 "
5943 .unindent(),
5944 );
5945}
5946
5947#[gpui::test]
5948fn test_transpose(cx: &mut TestAppContext) {
5949 init_test(cx, |_| {});
5950
5951 _ = cx.add_window(|window, cx| {
5952 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5953 editor.set_style(EditorStyle::default(), window, cx);
5954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5955 s.select_ranges([1..1])
5956 });
5957 editor.transpose(&Default::default(), window, cx);
5958 assert_eq!(editor.text(cx), "bac");
5959 assert_eq!(
5960 editor.selections.ranges(&editor.display_snapshot(cx)),
5961 [2..2]
5962 );
5963
5964 editor.transpose(&Default::default(), window, cx);
5965 assert_eq!(editor.text(cx), "bca");
5966 assert_eq!(
5967 editor.selections.ranges(&editor.display_snapshot(cx)),
5968 [3..3]
5969 );
5970
5971 editor.transpose(&Default::default(), window, cx);
5972 assert_eq!(editor.text(cx), "bac");
5973 assert_eq!(
5974 editor.selections.ranges(&editor.display_snapshot(cx)),
5975 [3..3]
5976 );
5977
5978 editor
5979 });
5980
5981 _ = cx.add_window(|window, cx| {
5982 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5983 editor.set_style(EditorStyle::default(), window, cx);
5984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5985 s.select_ranges([3..3])
5986 });
5987 editor.transpose(&Default::default(), window, cx);
5988 assert_eq!(editor.text(cx), "acb\nde");
5989 assert_eq!(
5990 editor.selections.ranges(&editor.display_snapshot(cx)),
5991 [3..3]
5992 );
5993
5994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5995 s.select_ranges([4..4])
5996 });
5997 editor.transpose(&Default::default(), window, cx);
5998 assert_eq!(editor.text(cx), "acbd\ne");
5999 assert_eq!(
6000 editor.selections.ranges(&editor.display_snapshot(cx)),
6001 [5..5]
6002 );
6003
6004 editor.transpose(&Default::default(), window, cx);
6005 assert_eq!(editor.text(cx), "acbde\n");
6006 assert_eq!(
6007 editor.selections.ranges(&editor.display_snapshot(cx)),
6008 [6..6]
6009 );
6010
6011 editor.transpose(&Default::default(), window, cx);
6012 assert_eq!(editor.text(cx), "acbd\ne");
6013 assert_eq!(
6014 editor.selections.ranges(&editor.display_snapshot(cx)),
6015 [6..6]
6016 );
6017
6018 editor
6019 });
6020
6021 _ = cx.add_window(|window, cx| {
6022 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6023 editor.set_style(EditorStyle::default(), window, cx);
6024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6025 s.select_ranges([1..1, 2..2, 4..4])
6026 });
6027 editor.transpose(&Default::default(), window, cx);
6028 assert_eq!(editor.text(cx), "bacd\ne");
6029 assert_eq!(
6030 editor.selections.ranges(&editor.display_snapshot(cx)),
6031 [2..2, 3..3, 5..5]
6032 );
6033
6034 editor.transpose(&Default::default(), window, cx);
6035 assert_eq!(editor.text(cx), "bcade\n");
6036 assert_eq!(
6037 editor.selections.ranges(&editor.display_snapshot(cx)),
6038 [3..3, 4..4, 6..6]
6039 );
6040
6041 editor.transpose(&Default::default(), window, cx);
6042 assert_eq!(editor.text(cx), "bcda\ne");
6043 assert_eq!(
6044 editor.selections.ranges(&editor.display_snapshot(cx)),
6045 [4..4, 6..6]
6046 );
6047
6048 editor.transpose(&Default::default(), window, cx);
6049 assert_eq!(editor.text(cx), "bcade\n");
6050 assert_eq!(
6051 editor.selections.ranges(&editor.display_snapshot(cx)),
6052 [4..4, 6..6]
6053 );
6054
6055 editor.transpose(&Default::default(), window, cx);
6056 assert_eq!(editor.text(cx), "bcaed\n");
6057 assert_eq!(
6058 editor.selections.ranges(&editor.display_snapshot(cx)),
6059 [5..5, 6..6]
6060 );
6061
6062 editor
6063 });
6064
6065 _ = cx.add_window(|window, cx| {
6066 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6067 editor.set_style(EditorStyle::default(), window, cx);
6068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6069 s.select_ranges([4..4])
6070 });
6071 editor.transpose(&Default::default(), window, cx);
6072 assert_eq!(editor.text(cx), "🏀🍐✋");
6073 assert_eq!(
6074 editor.selections.ranges(&editor.display_snapshot(cx)),
6075 [8..8]
6076 );
6077
6078 editor.transpose(&Default::default(), window, cx);
6079 assert_eq!(editor.text(cx), "🏀✋🍐");
6080 assert_eq!(
6081 editor.selections.ranges(&editor.display_snapshot(cx)),
6082 [11..11]
6083 );
6084
6085 editor.transpose(&Default::default(), window, cx);
6086 assert_eq!(editor.text(cx), "🏀🍐✋");
6087 assert_eq!(
6088 editor.selections.ranges(&editor.display_snapshot(cx)),
6089 [11..11]
6090 );
6091
6092 editor
6093 });
6094}
6095
6096#[gpui::test]
6097async fn test_rewrap(cx: &mut TestAppContext) {
6098 init_test(cx, |settings| {
6099 settings.languages.0.extend([
6100 (
6101 "Markdown".into(),
6102 LanguageSettingsContent {
6103 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6104 preferred_line_length: Some(40),
6105 ..Default::default()
6106 },
6107 ),
6108 (
6109 "Plain Text".into(),
6110 LanguageSettingsContent {
6111 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6112 preferred_line_length: Some(40),
6113 ..Default::default()
6114 },
6115 ),
6116 (
6117 "C++".into(),
6118 LanguageSettingsContent {
6119 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6120 preferred_line_length: Some(40),
6121 ..Default::default()
6122 },
6123 ),
6124 (
6125 "Python".into(),
6126 LanguageSettingsContent {
6127 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6128 preferred_line_length: Some(40),
6129 ..Default::default()
6130 },
6131 ),
6132 (
6133 "Rust".into(),
6134 LanguageSettingsContent {
6135 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6136 preferred_line_length: Some(40),
6137 ..Default::default()
6138 },
6139 ),
6140 ])
6141 });
6142
6143 let mut cx = EditorTestContext::new(cx).await;
6144
6145 let cpp_language = Arc::new(Language::new(
6146 LanguageConfig {
6147 name: "C++".into(),
6148 line_comments: vec!["// ".into()],
6149 ..LanguageConfig::default()
6150 },
6151 None,
6152 ));
6153 let python_language = Arc::new(Language::new(
6154 LanguageConfig {
6155 name: "Python".into(),
6156 line_comments: vec!["# ".into()],
6157 ..LanguageConfig::default()
6158 },
6159 None,
6160 ));
6161 let markdown_language = Arc::new(Language::new(
6162 LanguageConfig {
6163 name: "Markdown".into(),
6164 rewrap_prefixes: vec![
6165 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6166 regex::Regex::new("[-*+]\\s+").unwrap(),
6167 ],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let rust_language = Arc::new(
6173 Language::new(
6174 LanguageConfig {
6175 name: "Rust".into(),
6176 line_comments: vec!["// ".into(), "/// ".into()],
6177 ..LanguageConfig::default()
6178 },
6179 Some(tree_sitter_rust::LANGUAGE.into()),
6180 )
6181 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6182 .unwrap(),
6183 );
6184
6185 let plaintext_language = Arc::new(Language::new(
6186 LanguageConfig {
6187 name: "Plain Text".into(),
6188 ..LanguageConfig::default()
6189 },
6190 None,
6191 ));
6192
6193 // Test basic rewrapping of a long line with a cursor
6194 assert_rewrap(
6195 indoc! {"
6196 // ˇThis is a long comment that needs to be wrapped.
6197 "},
6198 indoc! {"
6199 // ˇThis is a long comment that needs to
6200 // be wrapped.
6201 "},
6202 cpp_language.clone(),
6203 &mut cx,
6204 );
6205
6206 // Test rewrapping a full selection
6207 assert_rewrap(
6208 indoc! {"
6209 «// This selected long comment needs to be wrapped.ˇ»"
6210 },
6211 indoc! {"
6212 «// This selected long comment needs to
6213 // be wrapped.ˇ»"
6214 },
6215 cpp_language.clone(),
6216 &mut cx,
6217 );
6218
6219 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6220 assert_rewrap(
6221 indoc! {"
6222 // ˇThis is the first line.
6223 // Thisˇ is the second line.
6224 // This is the thirdˇ line, all part of one paragraph.
6225 "},
6226 indoc! {"
6227 // ˇThis is the first line. Thisˇ is the
6228 // second line. This is the thirdˇ line,
6229 // all part of one paragraph.
6230 "},
6231 cpp_language.clone(),
6232 &mut cx,
6233 );
6234
6235 // Test multiple cursors in different paragraphs trigger separate rewraps
6236 assert_rewrap(
6237 indoc! {"
6238 // ˇThis is the first paragraph, first line.
6239 // ˇThis is the first paragraph, second line.
6240
6241 // ˇThis is the second paragraph, first line.
6242 // ˇThis is the second paragraph, second line.
6243 "},
6244 indoc! {"
6245 // ˇThis is the first paragraph, first
6246 // line. ˇThis is the first paragraph,
6247 // second line.
6248
6249 // ˇThis is the second paragraph, first
6250 // line. ˇThis is the second paragraph,
6251 // second line.
6252 "},
6253 cpp_language.clone(),
6254 &mut cx,
6255 );
6256
6257 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6258 assert_rewrap(
6259 indoc! {"
6260 «// A regular long long comment to be wrapped.
6261 /// A documentation long comment to be wrapped.ˇ»
6262 "},
6263 indoc! {"
6264 «// A regular long long comment to be
6265 // wrapped.
6266 /// A documentation long comment to be
6267 /// wrapped.ˇ»
6268 "},
6269 rust_language.clone(),
6270 &mut cx,
6271 );
6272
6273 // Test that change in indentation level trigger seperate rewraps
6274 assert_rewrap(
6275 indoc! {"
6276 fn foo() {
6277 «// This is a long comment at the base indent.
6278 // This is a long comment at the next indent.ˇ»
6279 }
6280 "},
6281 indoc! {"
6282 fn foo() {
6283 «// This is a long comment at the
6284 // base indent.
6285 // This is a long comment at the
6286 // next indent.ˇ»
6287 }
6288 "},
6289 rust_language.clone(),
6290 &mut cx,
6291 );
6292
6293 // Test that different comment prefix characters (e.g., '#') are handled correctly
6294 assert_rewrap(
6295 indoc! {"
6296 # ˇThis is a long comment using a pound sign.
6297 "},
6298 indoc! {"
6299 # ˇThis is a long comment using a pound
6300 # sign.
6301 "},
6302 python_language,
6303 &mut cx,
6304 );
6305
6306 // Test rewrapping only affects comments, not code even when selected
6307 assert_rewrap(
6308 indoc! {"
6309 «/// This doc comment is long and should be wrapped.
6310 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6311 "},
6312 indoc! {"
6313 «/// This doc comment is long and should
6314 /// be wrapped.
6315 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6316 "},
6317 rust_language.clone(),
6318 &mut cx,
6319 );
6320
6321 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6322 assert_rewrap(
6323 indoc! {"
6324 # Header
6325
6326 A long long long line of markdown text to wrap.ˇ
6327 "},
6328 indoc! {"
6329 # Header
6330
6331 A long long long line of markdown text
6332 to wrap.ˇ
6333 "},
6334 markdown_language.clone(),
6335 &mut cx,
6336 );
6337
6338 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6339 assert_rewrap(
6340 indoc! {"
6341 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6342 2. This is a numbered list item that is very long and needs to be wrapped properly.
6343 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6344 "},
6345 indoc! {"
6346 «1. This is a numbered list item that is
6347 very long and needs to be wrapped
6348 properly.
6349 2. This is a numbered list item that is
6350 very long and needs to be wrapped
6351 properly.
6352 - This is an unordered list item that is
6353 also very long and should not merge
6354 with the numbered item.ˇ»
6355 "},
6356 markdown_language.clone(),
6357 &mut cx,
6358 );
6359
6360 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6361 assert_rewrap(
6362 indoc! {"
6363 «1. This is a numbered list item that is
6364 very long and needs to be wrapped
6365 properly.
6366 2. This is a numbered list item that is
6367 very long and needs to be wrapped
6368 properly.
6369 - This is an unordered list item that is
6370 also very long and should not merge with
6371 the numbered item.ˇ»
6372 "},
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge
6382 with the numbered item.ˇ»
6383 "},
6384 markdown_language.clone(),
6385 &mut cx,
6386 );
6387
6388 // Test that rewrapping maintain indents even when they already exists.
6389 assert_rewrap(
6390 indoc! {"
6391 «1. This is a numbered list
6392 item that is very long and needs to be wrapped properly.
6393 2. This is a numbered list
6394 item that is very long and needs to be wrapped properly.
6395 - This is an unordered list item that is also very long and
6396 should not merge with the numbered item.ˇ»
6397 "},
6398 indoc! {"
6399 «1. This is a numbered list item that is
6400 very long and needs to be wrapped
6401 properly.
6402 2. This is a numbered list item that is
6403 very long and needs to be wrapped
6404 properly.
6405 - This is an unordered list item that is
6406 also very long and should not merge
6407 with the numbered item.ˇ»
6408 "},
6409 markdown_language,
6410 &mut cx,
6411 );
6412
6413 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6414 assert_rewrap(
6415 indoc! {"
6416 ˇThis is a very long line of plain text that will be wrapped.
6417 "},
6418 indoc! {"
6419 ˇThis is a very long line of plain text
6420 that will be wrapped.
6421 "},
6422 plaintext_language.clone(),
6423 &mut cx,
6424 );
6425
6426 // Test that non-commented code acts as a paragraph boundary within a selection
6427 assert_rewrap(
6428 indoc! {"
6429 «// This is the first long comment block to be wrapped.
6430 fn my_func(a: u32);
6431 // This is the second long comment block to be wrapped.ˇ»
6432 "},
6433 indoc! {"
6434 «// This is the first long comment block
6435 // to be wrapped.
6436 fn my_func(a: u32);
6437 // This is the second long comment block
6438 // to be wrapped.ˇ»
6439 "},
6440 rust_language,
6441 &mut cx,
6442 );
6443
6444 // Test rewrapping multiple selections, including ones with blank lines or tabs
6445 assert_rewrap(
6446 indoc! {"
6447 «ˇThis is a very long line that will be wrapped.
6448
6449 This is another paragraph in the same selection.»
6450
6451 «\tThis is a very long indented line that will be wrapped.ˇ»
6452 "},
6453 indoc! {"
6454 «ˇThis is a very long line that will be
6455 wrapped.
6456
6457 This is another paragraph in the same
6458 selection.»
6459
6460 «\tThis is a very long indented line
6461 \tthat will be wrapped.ˇ»
6462 "},
6463 plaintext_language,
6464 &mut cx,
6465 );
6466
6467 // Test that an empty comment line acts as a paragraph boundary
6468 assert_rewrap(
6469 indoc! {"
6470 // ˇThis is a long comment that will be wrapped.
6471 //
6472 // And this is another long comment that will also be wrapped.ˇ
6473 "},
6474 indoc! {"
6475 // ˇThis is a long comment that will be
6476 // wrapped.
6477 //
6478 // And this is another long comment that
6479 // will also be wrapped.ˇ
6480 "},
6481 cpp_language,
6482 &mut cx,
6483 );
6484
6485 #[track_caller]
6486 fn assert_rewrap(
6487 unwrapped_text: &str,
6488 wrapped_text: &str,
6489 language: Arc<Language>,
6490 cx: &mut EditorTestContext,
6491 ) {
6492 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6493 cx.set_state(unwrapped_text);
6494 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6495 cx.assert_editor_state(wrapped_text);
6496 }
6497}
6498
6499#[gpui::test]
6500async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6501 init_test(cx, |settings| {
6502 settings.languages.0.extend([(
6503 "Rust".into(),
6504 LanguageSettingsContent {
6505 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6506 preferred_line_length: Some(40),
6507 ..Default::default()
6508 },
6509 )])
6510 });
6511
6512 let mut cx = EditorTestContext::new(cx).await;
6513
6514 let rust_lang = Arc::new(
6515 Language::new(
6516 LanguageConfig {
6517 name: "Rust".into(),
6518 line_comments: vec!["// ".into()],
6519 block_comment: Some(BlockCommentConfig {
6520 start: "/*".into(),
6521 end: "*/".into(),
6522 prefix: "* ".into(),
6523 tab_size: 1,
6524 }),
6525 documentation_comment: Some(BlockCommentConfig {
6526 start: "/**".into(),
6527 end: "*/".into(),
6528 prefix: "* ".into(),
6529 tab_size: 1,
6530 }),
6531
6532 ..LanguageConfig::default()
6533 },
6534 Some(tree_sitter_rust::LANGUAGE.into()),
6535 )
6536 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6537 .unwrap(),
6538 );
6539
6540 // regular block comment
6541 assert_rewrap(
6542 indoc! {"
6543 /*
6544 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6545 */
6546 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6547 "},
6548 indoc! {"
6549 /*
6550 *ˇ Lorem ipsum dolor sit amet,
6551 * consectetur adipiscing elit.
6552 */
6553 /*
6554 *ˇ Lorem ipsum dolor sit amet,
6555 * consectetur adipiscing elit.
6556 */
6557 "},
6558 rust_lang.clone(),
6559 &mut cx,
6560 );
6561
6562 // indent is respected
6563 assert_rewrap(
6564 indoc! {"
6565 {}
6566 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6567 "},
6568 indoc! {"
6569 {}
6570 /*
6571 *ˇ Lorem ipsum dolor sit amet,
6572 * consectetur adipiscing elit.
6573 */
6574 "},
6575 rust_lang.clone(),
6576 &mut cx,
6577 );
6578
6579 // short block comments with inline delimiters
6580 assert_rewrap(
6581 indoc! {"
6582 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6583 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6584 */
6585 /*
6586 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6587 "},
6588 indoc! {"
6589 /*
6590 *ˇ Lorem ipsum dolor sit amet,
6591 * consectetur adipiscing elit.
6592 */
6593 /*
6594 *ˇ Lorem ipsum dolor sit amet,
6595 * consectetur adipiscing elit.
6596 */
6597 /*
6598 *ˇ Lorem ipsum dolor sit amet,
6599 * consectetur adipiscing elit.
6600 */
6601 "},
6602 rust_lang.clone(),
6603 &mut cx,
6604 );
6605
6606 // multiline block comment with inline start/end delimiters
6607 assert_rewrap(
6608 indoc! {"
6609 /*ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit. */
6611 "},
6612 indoc! {"
6613 /*
6614 *ˇ Lorem ipsum dolor sit amet,
6615 * consectetur adipiscing elit.
6616 */
6617 "},
6618 rust_lang.clone(),
6619 &mut cx,
6620 );
6621
6622 // block comment rewrap still respects paragraph bounds
6623 assert_rewrap(
6624 indoc! {"
6625 /*
6626 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6627 *
6628 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6629 */
6630 "},
6631 indoc! {"
6632 /*
6633 *ˇ Lorem ipsum dolor sit amet,
6634 * consectetur adipiscing elit.
6635 *
6636 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6637 */
6638 "},
6639 rust_lang.clone(),
6640 &mut cx,
6641 );
6642
6643 // documentation comments
6644 assert_rewrap(
6645 indoc! {"
6646 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6647 /**
6648 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6649 */
6650 "},
6651 indoc! {"
6652 /**
6653 *ˇ Lorem ipsum dolor sit amet,
6654 * consectetur adipiscing elit.
6655 */
6656 /**
6657 *ˇ Lorem ipsum dolor sit amet,
6658 * consectetur adipiscing elit.
6659 */
6660 "},
6661 rust_lang.clone(),
6662 &mut cx,
6663 );
6664
6665 // different, adjacent comments
6666 assert_rewrap(
6667 indoc! {"
6668 /**
6669 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6670 */
6671 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6672 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6673 "},
6674 indoc! {"
6675 /**
6676 *ˇ Lorem ipsum dolor sit amet,
6677 * consectetur adipiscing elit.
6678 */
6679 /*
6680 *ˇ Lorem ipsum dolor sit amet,
6681 * consectetur adipiscing elit.
6682 */
6683 //ˇ Lorem ipsum dolor sit amet,
6684 // consectetur adipiscing elit.
6685 "},
6686 rust_lang.clone(),
6687 &mut cx,
6688 );
6689
6690 // selection w/ single short block comment
6691 assert_rewrap(
6692 indoc! {"
6693 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6694 "},
6695 indoc! {"
6696 «/*
6697 * Lorem ipsum dolor sit amet,
6698 * consectetur adipiscing elit.
6699 */ˇ»
6700 "},
6701 rust_lang.clone(),
6702 &mut cx,
6703 );
6704
6705 // rewrapping a single comment w/ abutting comments
6706 assert_rewrap(
6707 indoc! {"
6708 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6709 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6710 "},
6711 indoc! {"
6712 /*
6713 * ˇLorem ipsum dolor sit amet,
6714 * consectetur adipiscing elit.
6715 */
6716 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6717 "},
6718 rust_lang.clone(),
6719 &mut cx,
6720 );
6721
6722 // selection w/ non-abutting short block comments
6723 assert_rewrap(
6724 indoc! {"
6725 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6726
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6728 "},
6729 indoc! {"
6730 «/*
6731 * Lorem ipsum dolor sit amet,
6732 * consectetur adipiscing elit.
6733 */
6734
6735 /*
6736 * Lorem ipsum dolor sit amet,
6737 * consectetur adipiscing elit.
6738 */ˇ»
6739 "},
6740 rust_lang.clone(),
6741 &mut cx,
6742 );
6743
6744 // selection of multiline block comments
6745 assert_rewrap(
6746 indoc! {"
6747 «/* Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit. */ˇ»
6749 "},
6750 indoc! {"
6751 «/*
6752 * Lorem ipsum dolor sit amet,
6753 * consectetur adipiscing elit.
6754 */ˇ»
6755 "},
6756 rust_lang.clone(),
6757 &mut cx,
6758 );
6759
6760 // partial selection of multiline block comments
6761 assert_rewrap(
6762 indoc! {"
6763 «/* Lorem ipsum dolor sit amet,ˇ»
6764 * consectetur adipiscing elit. */
6765 /* Lorem ipsum dolor sit amet,
6766 «* consectetur adipiscing elit. */ˇ»
6767 "},
6768 indoc! {"
6769 «/*
6770 * Lorem ipsum dolor sit amet,ˇ»
6771 * consectetur adipiscing elit. */
6772 /* Lorem ipsum dolor sit amet,
6773 «* consectetur adipiscing elit.
6774 */ˇ»
6775 "},
6776 rust_lang.clone(),
6777 &mut cx,
6778 );
6779
6780 // selection w/ abutting short block comments
6781 // TODO: should not be combined; should rewrap as 2 comments
6782 assert_rewrap(
6783 indoc! {"
6784 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6785 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6786 "},
6787 // desired behavior:
6788 // indoc! {"
6789 // «/*
6790 // * Lorem ipsum dolor sit amet,
6791 // * consectetur adipiscing elit.
6792 // */
6793 // /*
6794 // * Lorem ipsum dolor sit amet,
6795 // * consectetur adipiscing elit.
6796 // */ˇ»
6797 // "},
6798 // actual behaviour:
6799 indoc! {"
6800 «/*
6801 * Lorem ipsum dolor sit amet,
6802 * consectetur adipiscing elit. Lorem
6803 * ipsum dolor sit amet, consectetur
6804 * adipiscing elit.
6805 */ˇ»
6806 "},
6807 rust_lang.clone(),
6808 &mut cx,
6809 );
6810
6811 // TODO: same as above, but with delimiters on separate line
6812 // assert_rewrap(
6813 // indoc! {"
6814 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6815 // */
6816 // /*
6817 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6818 // "},
6819 // // desired:
6820 // // indoc! {"
6821 // // «/*
6822 // // * Lorem ipsum dolor sit amet,
6823 // // * consectetur adipiscing elit.
6824 // // */
6825 // // /*
6826 // // * Lorem ipsum dolor sit amet,
6827 // // * consectetur adipiscing elit.
6828 // // */ˇ»
6829 // // "},
6830 // // actual: (but with trailing w/s on the empty lines)
6831 // indoc! {"
6832 // «/*
6833 // * Lorem ipsum dolor sit amet,
6834 // * consectetur adipiscing elit.
6835 // *
6836 // */
6837 // /*
6838 // *
6839 // * Lorem ipsum dolor sit amet,
6840 // * consectetur adipiscing elit.
6841 // */ˇ»
6842 // "},
6843 // rust_lang.clone(),
6844 // &mut cx,
6845 // );
6846
6847 // TODO these are unhandled edge cases; not correct, just documenting known issues
6848 assert_rewrap(
6849 indoc! {"
6850 /*
6851 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6852 */
6853 /*
6854 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6855 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6856 "},
6857 // desired:
6858 // indoc! {"
6859 // /*
6860 // *ˇ Lorem ipsum dolor sit amet,
6861 // * consectetur adipiscing elit.
6862 // */
6863 // /*
6864 // *ˇ Lorem ipsum dolor sit amet,
6865 // * consectetur adipiscing elit.
6866 // */
6867 // /*
6868 // *ˇ Lorem ipsum dolor sit amet
6869 // */ /* consectetur adipiscing elit. */
6870 // "},
6871 // actual:
6872 indoc! {"
6873 /*
6874 //ˇ Lorem ipsum dolor sit amet,
6875 // consectetur adipiscing elit.
6876 */
6877 /*
6878 * //ˇ Lorem ipsum dolor sit amet,
6879 * consectetur adipiscing elit.
6880 */
6881 /*
6882 *ˇ Lorem ipsum dolor sit amet */ /*
6883 * consectetur adipiscing elit.
6884 */
6885 "},
6886 rust_lang,
6887 &mut cx,
6888 );
6889
6890 #[track_caller]
6891 fn assert_rewrap(
6892 unwrapped_text: &str,
6893 wrapped_text: &str,
6894 language: Arc<Language>,
6895 cx: &mut EditorTestContext,
6896 ) {
6897 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6898 cx.set_state(unwrapped_text);
6899 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6900 cx.assert_editor_state(wrapped_text);
6901 }
6902}
6903
6904#[gpui::test]
6905async fn test_hard_wrap(cx: &mut TestAppContext) {
6906 init_test(cx, |_| {});
6907 let mut cx = EditorTestContext::new(cx).await;
6908
6909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6910 cx.update_editor(|editor, _, cx| {
6911 editor.set_hard_wrap(Some(14), cx);
6912 });
6913
6914 cx.set_state(indoc!(
6915 "
6916 one two three ˇ
6917 "
6918 ));
6919 cx.simulate_input("four");
6920 cx.run_until_parked();
6921
6922 cx.assert_editor_state(indoc!(
6923 "
6924 one two three
6925 fourˇ
6926 "
6927 ));
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.newline(&Default::default(), window, cx);
6931 });
6932 cx.run_until_parked();
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 four
6937 ˇ
6938 "
6939 ));
6940
6941 cx.simulate_input("five");
6942 cx.run_until_parked();
6943 cx.assert_editor_state(indoc!(
6944 "
6945 one two three
6946 four
6947 fiveˇ
6948 "
6949 ));
6950
6951 cx.update_editor(|editor, window, cx| {
6952 editor.newline(&Default::default(), window, cx);
6953 });
6954 cx.run_until_parked();
6955 cx.simulate_input("# ");
6956 cx.run_until_parked();
6957 cx.assert_editor_state(indoc!(
6958 "
6959 one two three
6960 four
6961 five
6962 # ˇ
6963 "
6964 ));
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.newline(&Default::default(), window, cx);
6968 });
6969 cx.run_until_parked();
6970 cx.assert_editor_state(indoc!(
6971 "
6972 one two three
6973 four
6974 five
6975 #\x20
6976 #ˇ
6977 "
6978 ));
6979
6980 cx.simulate_input(" 6");
6981 cx.run_until_parked();
6982 cx.assert_editor_state(indoc!(
6983 "
6984 one two three
6985 four
6986 five
6987 #
6988 # 6ˇ
6989 "
6990 ));
6991}
6992
6993#[gpui::test]
6994async fn test_cut_line_ends(cx: &mut TestAppContext) {
6995 init_test(cx, |_| {});
6996
6997 let mut cx = EditorTestContext::new(cx).await;
6998
6999 cx.set_state(indoc! {"The quick brownˇ"});
7000 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7001 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7002
7003 cx.set_state(indoc! {"The emacs foxˇ"});
7004 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7005 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7006
7007 cx.set_state(indoc! {"
7008 The quick« brownˇ»
7009 fox jumps overˇ
7010 the lazy dog"});
7011 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7012 cx.assert_editor_state(indoc! {"
7013 The quickˇ
7014 ˇthe lazy dog"});
7015
7016 cx.set_state(indoc! {"
7017 The quick« brownˇ»
7018 fox jumps overˇ
7019 the lazy dog"});
7020 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7021 cx.assert_editor_state(indoc! {"
7022 The quickˇ
7023 fox jumps overˇthe lazy dog"});
7024
7025 cx.set_state(indoc! {"
7026 The quick« brownˇ»
7027 fox jumps overˇ
7028 the lazy dog"});
7029 cx.update_editor(|e, window, cx| {
7030 e.cut_to_end_of_line(
7031 &CutToEndOfLine {
7032 stop_at_newlines: true,
7033 },
7034 window,
7035 cx,
7036 )
7037 });
7038 cx.assert_editor_state(indoc! {"
7039 The quickˇ
7040 fox jumps overˇ
7041 the lazy dog"});
7042
7043 cx.set_state(indoc! {"
7044 The quick« brownˇ»
7045 fox jumps overˇ
7046 the lazy dog"});
7047 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7048 cx.assert_editor_state(indoc! {"
7049 The quickˇ
7050 fox jumps overˇthe lazy dog"});
7051}
7052
7053#[gpui::test]
7054async fn test_clipboard(cx: &mut TestAppContext) {
7055 init_test(cx, |_| {});
7056
7057 let mut cx = EditorTestContext::new(cx).await;
7058
7059 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7060 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7061 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7062
7063 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7064 cx.set_state("two ˇfour ˇsix ˇ");
7065 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7066 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7067
7068 // Paste again but with only two cursors. Since the number of cursors doesn't
7069 // match the number of slices in the clipboard, the entire clipboard text
7070 // is pasted at each cursor.
7071 cx.set_state("ˇtwo one✅ four three six five ˇ");
7072 cx.update_editor(|e, window, cx| {
7073 e.handle_input("( ", window, cx);
7074 e.paste(&Paste, window, cx);
7075 e.handle_input(") ", window, cx);
7076 });
7077 cx.assert_editor_state(
7078 &([
7079 "( one✅ ",
7080 "three ",
7081 "five ) ˇtwo one✅ four three six five ( one✅ ",
7082 "three ",
7083 "five ) ˇ",
7084 ]
7085 .join("\n")),
7086 );
7087
7088 // Cut with three selections, one of which is full-line.
7089 cx.set_state(indoc! {"
7090 1«2ˇ»3
7091 4ˇ567
7092 «8ˇ»9"});
7093 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7094 cx.assert_editor_state(indoc! {"
7095 1ˇ3
7096 ˇ9"});
7097
7098 // Paste with three selections, noticing how the copied selection that was full-line
7099 // gets inserted before the second cursor.
7100 cx.set_state(indoc! {"
7101 1ˇ3
7102 9ˇ
7103 «oˇ»ne"});
7104 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 12ˇ3
7107 4567
7108 9ˇ
7109 8ˇne"});
7110
7111 // Copy with a single cursor only, which writes the whole line into the clipboard.
7112 cx.set_state(indoc! {"
7113 The quick brown
7114 fox juˇmps over
7115 the lazy dog"});
7116 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7117 assert_eq!(
7118 cx.read_from_clipboard()
7119 .and_then(|item| item.text().as_deref().map(str::to_string)),
7120 Some("fox jumps over\n".to_string())
7121 );
7122
7123 // Paste with three selections, noticing how the copied full-line selection is inserted
7124 // before the empty selections but replaces the selection that is non-empty.
7125 cx.set_state(indoc! {"
7126 Tˇhe quick brown
7127 «foˇ»x jumps over
7128 tˇhe lazy dog"});
7129 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7130 cx.assert_editor_state(indoc! {"
7131 fox jumps over
7132 Tˇhe quick brown
7133 fox jumps over
7134 ˇx jumps over
7135 fox jumps over
7136 tˇhe lazy dog"});
7137}
7138
7139#[gpui::test]
7140async fn test_copy_trim(cx: &mut TestAppContext) {
7141 init_test(cx, |_| {});
7142
7143 let mut cx = EditorTestContext::new(cx).await;
7144 cx.set_state(
7145 r#" «for selection in selections.iter() {
7146 let mut start = selection.start;
7147 let mut end = selection.end;
7148 let is_entire_line = selection.is_empty();
7149 if is_entire_line {
7150 start = Point::new(start.row, 0);ˇ»
7151 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7152 }
7153 "#,
7154 );
7155 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7156 assert_eq!(
7157 cx.read_from_clipboard()
7158 .and_then(|item| item.text().as_deref().map(str::to_string)),
7159 Some(
7160 "for selection in selections.iter() {
7161 let mut start = selection.start;
7162 let mut end = selection.end;
7163 let is_entire_line = selection.is_empty();
7164 if is_entire_line {
7165 start = Point::new(start.row, 0);"
7166 .to_string()
7167 ),
7168 "Regular copying preserves all indentation selected",
7169 );
7170 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7171 assert_eq!(
7172 cx.read_from_clipboard()
7173 .and_then(|item| item.text().as_deref().map(str::to_string)),
7174 Some(
7175 "for selection in selections.iter() {
7176let mut start = selection.start;
7177let mut end = selection.end;
7178let is_entire_line = selection.is_empty();
7179if is_entire_line {
7180 start = Point::new(start.row, 0);"
7181 .to_string()
7182 ),
7183 "Copying with stripping should strip all leading whitespaces"
7184 );
7185
7186 cx.set_state(
7187 r#" « for selection in selections.iter() {
7188 let mut start = selection.start;
7189 let mut end = selection.end;
7190 let is_entire_line = selection.is_empty();
7191 if is_entire_line {
7192 start = Point::new(start.row, 0);ˇ»
7193 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7194 }
7195 "#,
7196 );
7197 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7198 assert_eq!(
7199 cx.read_from_clipboard()
7200 .and_then(|item| item.text().as_deref().map(str::to_string)),
7201 Some(
7202 " for selection in selections.iter() {
7203 let mut start = selection.start;
7204 let mut end = selection.end;
7205 let is_entire_line = selection.is_empty();
7206 if is_entire_line {
7207 start = Point::new(start.row, 0);"
7208 .to_string()
7209 ),
7210 "Regular copying preserves all indentation selected",
7211 );
7212 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7213 assert_eq!(
7214 cx.read_from_clipboard()
7215 .and_then(|item| item.text().as_deref().map(str::to_string)),
7216 Some(
7217 "for selection in selections.iter() {
7218let mut start = selection.start;
7219let mut end = selection.end;
7220let is_entire_line = selection.is_empty();
7221if is_entire_line {
7222 start = Point::new(start.row, 0);"
7223 .to_string()
7224 ),
7225 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7226 );
7227
7228 cx.set_state(
7229 r#" «ˇ for selection in selections.iter() {
7230 let mut start = selection.start;
7231 let mut end = selection.end;
7232 let is_entire_line = selection.is_empty();
7233 if is_entire_line {
7234 start = Point::new(start.row, 0);»
7235 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7236 }
7237 "#,
7238 );
7239 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7240 assert_eq!(
7241 cx.read_from_clipboard()
7242 .and_then(|item| item.text().as_deref().map(str::to_string)),
7243 Some(
7244 " for selection in selections.iter() {
7245 let mut start = selection.start;
7246 let mut end = selection.end;
7247 let is_entire_line = selection.is_empty();
7248 if is_entire_line {
7249 start = Point::new(start.row, 0);"
7250 .to_string()
7251 ),
7252 "Regular copying for reverse selection works the same",
7253 );
7254 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7255 assert_eq!(
7256 cx.read_from_clipboard()
7257 .and_then(|item| item.text().as_deref().map(str::to_string)),
7258 Some(
7259 "for selection in selections.iter() {
7260let mut start = selection.start;
7261let mut end = selection.end;
7262let is_entire_line = selection.is_empty();
7263if is_entire_line {
7264 start = Point::new(start.row, 0);"
7265 .to_string()
7266 ),
7267 "Copying with stripping for reverse selection works the same"
7268 );
7269
7270 cx.set_state(
7271 r#" for selection «in selections.iter() {
7272 let mut start = selection.start;
7273 let mut end = selection.end;
7274 let is_entire_line = selection.is_empty();
7275 if is_entire_line {
7276 start = Point::new(start.row, 0);ˇ»
7277 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7278 }
7279 "#,
7280 );
7281 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7282 assert_eq!(
7283 cx.read_from_clipboard()
7284 .and_then(|item| item.text().as_deref().map(str::to_string)),
7285 Some(
7286 "in selections.iter() {
7287 let mut start = selection.start;
7288 let mut end = selection.end;
7289 let is_entire_line = selection.is_empty();
7290 if is_entire_line {
7291 start = Point::new(start.row, 0);"
7292 .to_string()
7293 ),
7294 "When selecting past the indent, the copying works as usual",
7295 );
7296 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7297 assert_eq!(
7298 cx.read_from_clipboard()
7299 .and_then(|item| item.text().as_deref().map(str::to_string)),
7300 Some(
7301 "in selections.iter() {
7302 let mut start = selection.start;
7303 let mut end = selection.end;
7304 let is_entire_line = selection.is_empty();
7305 if is_entire_line {
7306 start = Point::new(start.row, 0);"
7307 .to_string()
7308 ),
7309 "When selecting past the indent, nothing is trimmed"
7310 );
7311
7312 cx.set_state(
7313 r#" «for selection in selections.iter() {
7314 let mut start = selection.start;
7315
7316 let mut end = selection.end;
7317 let is_entire_line = selection.is_empty();
7318 if is_entire_line {
7319 start = Point::new(start.row, 0);
7320ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7321 }
7322 "#,
7323 );
7324 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7325 assert_eq!(
7326 cx.read_from_clipboard()
7327 .and_then(|item| item.text().as_deref().map(str::to_string)),
7328 Some(
7329 "for selection in selections.iter() {
7330let mut start = selection.start;
7331
7332let mut end = selection.end;
7333let is_entire_line = selection.is_empty();
7334if is_entire_line {
7335 start = Point::new(start.row, 0);
7336"
7337 .to_string()
7338 ),
7339 "Copying with stripping should ignore empty lines"
7340 );
7341}
7342
7343#[gpui::test]
7344async fn test_paste_multiline(cx: &mut TestAppContext) {
7345 init_test(cx, |_| {});
7346
7347 let mut cx = EditorTestContext::new(cx).await;
7348 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7349
7350 // Cut an indented block, without the leading whitespace.
7351 cx.set_state(indoc! {"
7352 const a: B = (
7353 c(),
7354 «d(
7355 e,
7356 f
7357 )ˇ»
7358 );
7359 "});
7360 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7361 cx.assert_editor_state(indoc! {"
7362 const a: B = (
7363 c(),
7364 ˇ
7365 );
7366 "});
7367
7368 // Paste it at the same position.
7369 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7370 cx.assert_editor_state(indoc! {"
7371 const a: B = (
7372 c(),
7373 d(
7374 e,
7375 f
7376 )ˇ
7377 );
7378 "});
7379
7380 // Paste it at a line with a lower indent level.
7381 cx.set_state(indoc! {"
7382 ˇ
7383 const a: B = (
7384 c(),
7385 );
7386 "});
7387 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7388 cx.assert_editor_state(indoc! {"
7389 d(
7390 e,
7391 f
7392 )ˇ
7393 const a: B = (
7394 c(),
7395 );
7396 "});
7397
7398 // Cut an indented block, with the leading whitespace.
7399 cx.set_state(indoc! {"
7400 const a: B = (
7401 c(),
7402 « d(
7403 e,
7404 f
7405 )
7406 ˇ»);
7407 "});
7408 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7409 cx.assert_editor_state(indoc! {"
7410 const a: B = (
7411 c(),
7412 ˇ);
7413 "});
7414
7415 // Paste it at the same position.
7416 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7417 cx.assert_editor_state(indoc! {"
7418 const a: B = (
7419 c(),
7420 d(
7421 e,
7422 f
7423 )
7424 ˇ);
7425 "});
7426
7427 // Paste it at a line with a higher indent level.
7428 cx.set_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 fˇ
7434 )
7435 );
7436 "});
7437 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7438 cx.assert_editor_state(indoc! {"
7439 const a: B = (
7440 c(),
7441 d(
7442 e,
7443 f d(
7444 e,
7445 f
7446 )
7447 ˇ
7448 )
7449 );
7450 "});
7451
7452 // Copy an indented block, starting mid-line
7453 cx.set_state(indoc! {"
7454 const a: B = (
7455 c(),
7456 somethin«g(
7457 e,
7458 f
7459 )ˇ»
7460 );
7461 "});
7462 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7463
7464 // Paste it on a line with a lower indent level
7465 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7466 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7467 cx.assert_editor_state(indoc! {"
7468 const a: B = (
7469 c(),
7470 something(
7471 e,
7472 f
7473 )
7474 );
7475 g(
7476 e,
7477 f
7478 )ˇ"});
7479}
7480
7481#[gpui::test]
7482async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7483 init_test(cx, |_| {});
7484
7485 cx.write_to_clipboard(ClipboardItem::new_string(
7486 " d(\n e\n );\n".into(),
7487 ));
7488
7489 let mut cx = EditorTestContext::new(cx).await;
7490 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7491
7492 cx.set_state(indoc! {"
7493 fn a() {
7494 b();
7495 if c() {
7496 ˇ
7497 }
7498 }
7499 "});
7500
7501 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7502 cx.assert_editor_state(indoc! {"
7503 fn a() {
7504 b();
7505 if c() {
7506 d(
7507 e
7508 );
7509 ˇ
7510 }
7511 }
7512 "});
7513
7514 cx.set_state(indoc! {"
7515 fn a() {
7516 b();
7517 ˇ
7518 }
7519 "});
7520
7521 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7522 cx.assert_editor_state(indoc! {"
7523 fn a() {
7524 b();
7525 d(
7526 e
7527 );
7528 ˇ
7529 }
7530 "});
7531}
7532
7533#[gpui::test]
7534fn test_select_all(cx: &mut TestAppContext) {
7535 init_test(cx, |_| {});
7536
7537 let editor = cx.add_window(|window, cx| {
7538 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7539 build_editor(buffer, window, cx)
7540 });
7541 _ = editor.update(cx, |editor, window, cx| {
7542 editor.select_all(&SelectAll, window, cx);
7543 assert_eq!(
7544 editor.selections.display_ranges(cx),
7545 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7546 );
7547 });
7548}
7549
7550#[gpui::test]
7551fn test_select_line(cx: &mut TestAppContext) {
7552 init_test(cx, |_| {});
7553
7554 let editor = cx.add_window(|window, cx| {
7555 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7556 build_editor(buffer, window, cx)
7557 });
7558 _ = editor.update(cx, |editor, window, cx| {
7559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7560 s.select_display_ranges([
7561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7564 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7565 ])
7566 });
7567 editor.select_line(&SelectLine, window, cx);
7568 assert_eq!(
7569 editor.selections.display_ranges(cx),
7570 vec![
7571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7572 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7573 ]
7574 );
7575 });
7576
7577 _ = editor.update(cx, |editor, window, cx| {
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 editor.selections.display_ranges(cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 editor.selections.display_ranges(cx),
7592 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7593 );
7594 });
7595}
7596
7597#[gpui::test]
7598async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7599 init_test(cx, |_| {});
7600 let mut cx = EditorTestContext::new(cx).await;
7601
7602 #[track_caller]
7603 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7604 cx.set_state(initial_state);
7605 cx.update_editor(|e, window, cx| {
7606 e.split_selection_into_lines(&Default::default(), window, cx)
7607 });
7608 cx.assert_editor_state(expected_state);
7609 }
7610
7611 // Selection starts and ends at the middle of lines, left-to-right
7612 test(
7613 &mut cx,
7614 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7615 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7616 );
7617 // Same thing, right-to-left
7618 test(
7619 &mut cx,
7620 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7621 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7622 );
7623
7624 // Whole buffer, left-to-right, last line *doesn't* end with newline
7625 test(
7626 &mut cx,
7627 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7628 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7629 );
7630 // Same thing, right-to-left
7631 test(
7632 &mut cx,
7633 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7634 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7635 );
7636
7637 // Whole buffer, left-to-right, last line ends with newline
7638 test(
7639 &mut cx,
7640 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7641 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7642 );
7643 // Same thing, right-to-left
7644 test(
7645 &mut cx,
7646 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7647 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7648 );
7649
7650 // Starts at the end of a line, ends at the start of another
7651 test(
7652 &mut cx,
7653 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7654 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7655 );
7656}
7657
7658#[gpui::test]
7659async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7660 init_test(cx, |_| {});
7661
7662 let editor = cx.add_window(|window, cx| {
7663 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7664 build_editor(buffer, window, cx)
7665 });
7666
7667 // setup
7668 _ = editor.update(cx, |editor, window, cx| {
7669 editor.fold_creases(
7670 vec![
7671 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7672 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7673 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7674 ],
7675 true,
7676 window,
7677 cx,
7678 );
7679 assert_eq!(
7680 editor.display_text(cx),
7681 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7682 );
7683 });
7684
7685 _ = editor.update(cx, |editor, window, cx| {
7686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7687 s.select_display_ranges([
7688 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7689 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7691 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7692 ])
7693 });
7694 editor.split_selection_into_lines(&Default::default(), window, cx);
7695 assert_eq!(
7696 editor.display_text(cx),
7697 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7698 );
7699 });
7700 EditorTestContext::for_editor(editor, cx)
7701 .await
7702 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7703
7704 _ = editor.update(cx, |editor, window, cx| {
7705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7706 s.select_display_ranges([
7707 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7708 ])
7709 });
7710 editor.split_selection_into_lines(&Default::default(), window, cx);
7711 assert_eq!(
7712 editor.display_text(cx),
7713 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7714 );
7715 assert_eq!(
7716 editor.selections.display_ranges(cx),
7717 [
7718 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7719 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7720 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7721 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7722 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7723 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7724 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7725 ]
7726 );
7727 });
7728 EditorTestContext::for_editor(editor, cx)
7729 .await
7730 .assert_editor_state(
7731 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7732 );
7733}
7734
7735#[gpui::test]
7736async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7737 init_test(cx, |_| {});
7738
7739 let mut cx = EditorTestContext::new(cx).await;
7740
7741 cx.set_state(indoc!(
7742 r#"abc
7743 defˇghi
7744
7745 jk
7746 nlmo
7747 "#
7748 ));
7749
7750 cx.update_editor(|editor, window, cx| {
7751 editor.add_selection_above(&Default::default(), window, cx);
7752 });
7753
7754 cx.assert_editor_state(indoc!(
7755 r#"abcˇ
7756 defˇghi
7757
7758 jk
7759 nlmo
7760 "#
7761 ));
7762
7763 cx.update_editor(|editor, window, cx| {
7764 editor.add_selection_above(&Default::default(), window, cx);
7765 });
7766
7767 cx.assert_editor_state(indoc!(
7768 r#"abcˇ
7769 defˇghi
7770
7771 jk
7772 nlmo
7773 "#
7774 ));
7775
7776 cx.update_editor(|editor, window, cx| {
7777 editor.add_selection_below(&Default::default(), window, cx);
7778 });
7779
7780 cx.assert_editor_state(indoc!(
7781 r#"abc
7782 defˇghi
7783
7784 jk
7785 nlmo
7786 "#
7787 ));
7788
7789 cx.update_editor(|editor, window, cx| {
7790 editor.undo_selection(&Default::default(), window, cx);
7791 });
7792
7793 cx.assert_editor_state(indoc!(
7794 r#"abcˇ
7795 defˇghi
7796
7797 jk
7798 nlmo
7799 "#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.redo_selection(&Default::default(), window, cx);
7804 });
7805
7806 cx.assert_editor_state(indoc!(
7807 r#"abc
7808 defˇghi
7809
7810 jk
7811 nlmo
7812 "#
7813 ));
7814
7815 cx.update_editor(|editor, window, cx| {
7816 editor.add_selection_below(&Default::default(), window, cx);
7817 });
7818
7819 cx.assert_editor_state(indoc!(
7820 r#"abc
7821 defˇghi
7822 ˇ
7823 jk
7824 nlmo
7825 "#
7826 ));
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.add_selection_below(&Default::default(), window, cx);
7830 });
7831
7832 cx.assert_editor_state(indoc!(
7833 r#"abc
7834 defˇghi
7835 ˇ
7836 jkˇ
7837 nlmo
7838 "#
7839 ));
7840
7841 cx.update_editor(|editor, window, cx| {
7842 editor.add_selection_below(&Default::default(), window, cx);
7843 });
7844
7845 cx.assert_editor_state(indoc!(
7846 r#"abc
7847 defˇghi
7848 ˇ
7849 jkˇ
7850 nlmˇo
7851 "#
7852 ));
7853
7854 cx.update_editor(|editor, window, cx| {
7855 editor.add_selection_below(&Default::default(), window, cx);
7856 });
7857
7858 cx.assert_editor_state(indoc!(
7859 r#"abc
7860 defˇghi
7861 ˇ
7862 jkˇ
7863 nlmˇo
7864 ˇ"#
7865 ));
7866
7867 // change selections
7868 cx.set_state(indoc!(
7869 r#"abc
7870 def«ˇg»hi
7871
7872 jk
7873 nlmo
7874 "#
7875 ));
7876
7877 cx.update_editor(|editor, window, cx| {
7878 editor.add_selection_below(&Default::default(), window, cx);
7879 });
7880
7881 cx.assert_editor_state(indoc!(
7882 r#"abc
7883 def«ˇg»hi
7884
7885 jk
7886 nlm«ˇo»
7887 "#
7888 ));
7889
7890 cx.update_editor(|editor, window, cx| {
7891 editor.add_selection_below(&Default::default(), window, cx);
7892 });
7893
7894 cx.assert_editor_state(indoc!(
7895 r#"abc
7896 def«ˇg»hi
7897
7898 jk
7899 nlm«ˇo»
7900 "#
7901 ));
7902
7903 cx.update_editor(|editor, window, cx| {
7904 editor.add_selection_above(&Default::default(), window, cx);
7905 });
7906
7907 cx.assert_editor_state(indoc!(
7908 r#"abc
7909 def«ˇg»hi
7910
7911 jk
7912 nlmo
7913 "#
7914 ));
7915
7916 cx.update_editor(|editor, window, cx| {
7917 editor.add_selection_above(&Default::default(), window, cx);
7918 });
7919
7920 cx.assert_editor_state(indoc!(
7921 r#"abc
7922 def«ˇg»hi
7923
7924 jk
7925 nlmo
7926 "#
7927 ));
7928
7929 // Change selections again
7930 cx.set_state(indoc!(
7931 r#"a«bc
7932 defgˇ»hi
7933
7934 jk
7935 nlmo
7936 "#
7937 ));
7938
7939 cx.update_editor(|editor, window, cx| {
7940 editor.add_selection_below(&Default::default(), window, cx);
7941 });
7942
7943 cx.assert_editor_state(indoc!(
7944 r#"a«bcˇ»
7945 d«efgˇ»hi
7946
7947 j«kˇ»
7948 nlmo
7949 "#
7950 ));
7951
7952 cx.update_editor(|editor, window, cx| {
7953 editor.add_selection_below(&Default::default(), window, cx);
7954 });
7955 cx.assert_editor_state(indoc!(
7956 r#"a«bcˇ»
7957 d«efgˇ»hi
7958
7959 j«kˇ»
7960 n«lmoˇ»
7961 "#
7962 ));
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_above(&Default::default(), window, cx);
7965 });
7966
7967 cx.assert_editor_state(indoc!(
7968 r#"a«bcˇ»
7969 d«efgˇ»hi
7970
7971 j«kˇ»
7972 nlmo
7973 "#
7974 ));
7975
7976 // Change selections again
7977 cx.set_state(indoc!(
7978 r#"abc
7979 d«ˇefghi
7980
7981 jk
7982 nlm»o
7983 "#
7984 ));
7985
7986 cx.update_editor(|editor, window, cx| {
7987 editor.add_selection_above(&Default::default(), window, cx);
7988 });
7989
7990 cx.assert_editor_state(indoc!(
7991 r#"a«ˇbc»
7992 d«ˇef»ghi
7993
7994 j«ˇk»
7995 n«ˇlm»o
7996 "#
7997 ));
7998
7999 cx.update_editor(|editor, window, cx| {
8000 editor.add_selection_below(&Default::default(), window, cx);
8001 });
8002
8003 cx.assert_editor_state(indoc!(
8004 r#"abc
8005 d«ˇef»ghi
8006
8007 j«ˇk»
8008 n«ˇlm»o
8009 "#
8010 ));
8011}
8012
8013#[gpui::test]
8014async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8015 init_test(cx, |_| {});
8016 let mut cx = EditorTestContext::new(cx).await;
8017
8018 cx.set_state(indoc!(
8019 r#"line onˇe
8020 liˇne two
8021 line three
8022 line four"#
8023 ));
8024
8025 cx.update_editor(|editor, window, cx| {
8026 editor.add_selection_below(&Default::default(), window, cx);
8027 });
8028
8029 // test multiple cursors expand in the same direction
8030 cx.assert_editor_state(indoc!(
8031 r#"line onˇe
8032 liˇne twˇo
8033 liˇne three
8034 line four"#
8035 ));
8036
8037 cx.update_editor(|editor, window, cx| {
8038 editor.add_selection_below(&Default::default(), window, cx);
8039 });
8040
8041 cx.update_editor(|editor, window, cx| {
8042 editor.add_selection_below(&Default::default(), window, cx);
8043 });
8044
8045 // test multiple cursors expand below overflow
8046 cx.assert_editor_state(indoc!(
8047 r#"line onˇe
8048 liˇne twˇo
8049 liˇne thˇree
8050 liˇne foˇur"#
8051 ));
8052
8053 cx.update_editor(|editor, window, cx| {
8054 editor.add_selection_above(&Default::default(), window, cx);
8055 });
8056
8057 // test multiple cursors retrieves back correctly
8058 cx.assert_editor_state(indoc!(
8059 r#"line onˇe
8060 liˇne twˇo
8061 liˇne thˇree
8062 line four"#
8063 ));
8064
8065 cx.update_editor(|editor, window, cx| {
8066 editor.add_selection_above(&Default::default(), window, cx);
8067 });
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.add_selection_above(&Default::default(), window, cx);
8071 });
8072
8073 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8074 cx.assert_editor_state(indoc!(
8075 r#"liˇne onˇe
8076 liˇne two
8077 line three
8078 line four"#
8079 ));
8080
8081 cx.update_editor(|editor, window, cx| {
8082 editor.undo_selection(&Default::default(), window, cx);
8083 });
8084
8085 // test undo
8086 cx.assert_editor_state(indoc!(
8087 r#"line onˇe
8088 liˇne twˇo
8089 line three
8090 line four"#
8091 ));
8092
8093 cx.update_editor(|editor, window, cx| {
8094 editor.redo_selection(&Default::default(), window, cx);
8095 });
8096
8097 // test redo
8098 cx.assert_editor_state(indoc!(
8099 r#"liˇne onˇe
8100 liˇne two
8101 line three
8102 line four"#
8103 ));
8104
8105 cx.set_state(indoc!(
8106 r#"abcd
8107 ef«ghˇ»
8108 ijkl
8109 «mˇ»nop"#
8110 ));
8111
8112 cx.update_editor(|editor, window, cx| {
8113 editor.add_selection_above(&Default::default(), window, cx);
8114 });
8115
8116 // test multiple selections expand in the same direction
8117 cx.assert_editor_state(indoc!(
8118 r#"ab«cdˇ»
8119 ef«ghˇ»
8120 «iˇ»jkl
8121 «mˇ»nop"#
8122 ));
8123
8124 cx.update_editor(|editor, window, cx| {
8125 editor.add_selection_above(&Default::default(), window, cx);
8126 });
8127
8128 // test multiple selection upward overflow
8129 cx.assert_editor_state(indoc!(
8130 r#"ab«cdˇ»
8131 «eˇ»f«ghˇ»
8132 «iˇ»jkl
8133 «mˇ»nop"#
8134 ));
8135
8136 cx.update_editor(|editor, window, cx| {
8137 editor.add_selection_below(&Default::default(), window, cx);
8138 });
8139
8140 // test multiple selection retrieves back correctly
8141 cx.assert_editor_state(indoc!(
8142 r#"abcd
8143 ef«ghˇ»
8144 «iˇ»jkl
8145 «mˇ»nop"#
8146 ));
8147
8148 cx.update_editor(|editor, window, cx| {
8149 editor.add_selection_below(&Default::default(), window, cx);
8150 });
8151
8152 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8153 cx.assert_editor_state(indoc!(
8154 r#"abcd
8155 ef«ghˇ»
8156 ij«klˇ»
8157 «mˇ»nop"#
8158 ));
8159
8160 cx.update_editor(|editor, window, cx| {
8161 editor.undo_selection(&Default::default(), window, cx);
8162 });
8163
8164 // test undo
8165 cx.assert_editor_state(indoc!(
8166 r#"abcd
8167 ef«ghˇ»
8168 «iˇ»jkl
8169 «mˇ»nop"#
8170 ));
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.redo_selection(&Default::default(), window, cx);
8174 });
8175
8176 // test redo
8177 cx.assert_editor_state(indoc!(
8178 r#"abcd
8179 ef«ghˇ»
8180 ij«klˇ»
8181 «mˇ»nop"#
8182 ));
8183}
8184
8185#[gpui::test]
8186async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8187 init_test(cx, |_| {});
8188 let mut cx = EditorTestContext::new(cx).await;
8189
8190 cx.set_state(indoc!(
8191 r#"line onˇe
8192 liˇne two
8193 line three
8194 line four"#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 editor.add_selection_below(&Default::default(), window, cx);
8200 editor.add_selection_below(&Default::default(), window, cx);
8201 });
8202
8203 // initial state with two multi cursor groups
8204 cx.assert_editor_state(indoc!(
8205 r#"line onˇe
8206 liˇne twˇo
8207 liˇne thˇree
8208 liˇne foˇur"#
8209 ));
8210
8211 // add single cursor in middle - simulate opt click
8212 cx.update_editor(|editor, window, cx| {
8213 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8214 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8215 editor.end_selection(window, cx);
8216 });
8217
8218 cx.assert_editor_state(indoc!(
8219 r#"line onˇe
8220 liˇne twˇo
8221 liˇneˇ thˇree
8222 liˇne foˇur"#
8223 ));
8224
8225 cx.update_editor(|editor, window, cx| {
8226 editor.add_selection_above(&Default::default(), window, cx);
8227 });
8228
8229 // test new added selection expands above and existing selection shrinks
8230 cx.assert_editor_state(indoc!(
8231 r#"line onˇe
8232 liˇneˇ twˇo
8233 liˇneˇ thˇree
8234 line four"#
8235 ));
8236
8237 cx.update_editor(|editor, window, cx| {
8238 editor.add_selection_above(&Default::default(), window, cx);
8239 });
8240
8241 // test new added selection expands above and existing selection shrinks
8242 cx.assert_editor_state(indoc!(
8243 r#"lineˇ onˇe
8244 liˇneˇ twˇo
8245 lineˇ three
8246 line four"#
8247 ));
8248
8249 // intial state with two selection groups
8250 cx.set_state(indoc!(
8251 r#"abcd
8252 ef«ghˇ»
8253 ijkl
8254 «mˇ»nop"#
8255 ));
8256
8257 cx.update_editor(|editor, window, cx| {
8258 editor.add_selection_above(&Default::default(), window, cx);
8259 editor.add_selection_above(&Default::default(), window, cx);
8260 });
8261
8262 cx.assert_editor_state(indoc!(
8263 r#"ab«cdˇ»
8264 «eˇ»f«ghˇ»
8265 «iˇ»jkl
8266 «mˇ»nop"#
8267 ));
8268
8269 // add single selection in middle - simulate opt drag
8270 cx.update_editor(|editor, window, cx| {
8271 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8272 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8273 editor.update_selection(
8274 DisplayPoint::new(DisplayRow(2), 4),
8275 0,
8276 gpui::Point::<f32>::default(),
8277 window,
8278 cx,
8279 );
8280 editor.end_selection(window, cx);
8281 });
8282
8283 cx.assert_editor_state(indoc!(
8284 r#"ab«cdˇ»
8285 «eˇ»f«ghˇ»
8286 «iˇ»jk«lˇ»
8287 «mˇ»nop"#
8288 ));
8289
8290 cx.update_editor(|editor, window, cx| {
8291 editor.add_selection_below(&Default::default(), window, cx);
8292 });
8293
8294 // test new added selection expands below, others shrinks from above
8295 cx.assert_editor_state(indoc!(
8296 r#"abcd
8297 ef«ghˇ»
8298 «iˇ»jk«lˇ»
8299 «mˇ»no«pˇ»"#
8300 ));
8301}
8302
8303#[gpui::test]
8304async fn test_select_next(cx: &mut TestAppContext) {
8305 init_test(cx, |_| {});
8306
8307 let mut cx = EditorTestContext::new(cx).await;
8308 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8309
8310 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8311 .unwrap();
8312 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8313
8314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8315 .unwrap();
8316 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8317
8318 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8319 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8320
8321 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8322 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8323
8324 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8325 .unwrap();
8326 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8327
8328 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8329 .unwrap();
8330 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8331
8332 // Test selection direction should be preserved
8333 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8334
8335 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8336 .unwrap();
8337 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8338}
8339
8340#[gpui::test]
8341async fn test_select_all_matches(cx: &mut TestAppContext) {
8342 init_test(cx, |_| {});
8343
8344 let mut cx = EditorTestContext::new(cx).await;
8345
8346 // Test caret-only selections
8347 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8348 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8349 .unwrap();
8350 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8351
8352 // Test left-to-right selections
8353 cx.set_state("abc\n«abcˇ»\nabc");
8354 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8355 .unwrap();
8356 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8357
8358 // Test right-to-left selections
8359 cx.set_state("abc\n«ˇabc»\nabc");
8360 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8361 .unwrap();
8362 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8363
8364 // Test selecting whitespace with caret selection
8365 cx.set_state("abc\nˇ abc\nabc");
8366 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8367 .unwrap();
8368 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8369
8370 // Test selecting whitespace with left-to-right selection
8371 cx.set_state("abc\n«ˇ »abc\nabc");
8372 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8373 .unwrap();
8374 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8375
8376 // Test no matches with right-to-left selection
8377 cx.set_state("abc\n« ˇ»abc\nabc");
8378 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8379 .unwrap();
8380 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8381
8382 // Test with a single word and clip_at_line_ends=true (#29823)
8383 cx.set_state("aˇbc");
8384 cx.update_editor(|e, window, cx| {
8385 e.set_clip_at_line_ends(true, cx);
8386 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8387 e.set_clip_at_line_ends(false, cx);
8388 });
8389 cx.assert_editor_state("«abcˇ»");
8390}
8391
8392#[gpui::test]
8393async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let mut cx = EditorTestContext::new(cx).await;
8397
8398 let large_body_1 = "\nd".repeat(200);
8399 let large_body_2 = "\ne".repeat(200);
8400
8401 cx.set_state(&format!(
8402 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8403 ));
8404 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8405 let scroll_position = editor.scroll_position(cx);
8406 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8407 scroll_position
8408 });
8409
8410 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8411 .unwrap();
8412 cx.assert_editor_state(&format!(
8413 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8414 ));
8415 let scroll_position_after_selection =
8416 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8417 assert_eq!(
8418 initial_scroll_position, scroll_position_after_selection,
8419 "Scroll position should not change after selecting all matches"
8420 );
8421}
8422
8423#[gpui::test]
8424async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8425 init_test(cx, |_| {});
8426
8427 let mut cx = EditorLspTestContext::new_rust(
8428 lsp::ServerCapabilities {
8429 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8430 ..Default::default()
8431 },
8432 cx,
8433 )
8434 .await;
8435
8436 cx.set_state(indoc! {"
8437 line 1
8438 line 2
8439 linˇe 3
8440 line 4
8441 line 5
8442 "});
8443
8444 // Make an edit
8445 cx.update_editor(|editor, window, cx| {
8446 editor.handle_input("X", window, cx);
8447 });
8448
8449 // Move cursor to a different position
8450 cx.update_editor(|editor, window, cx| {
8451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8452 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8453 });
8454 });
8455
8456 cx.assert_editor_state(indoc! {"
8457 line 1
8458 line 2
8459 linXe 3
8460 line 4
8461 liˇne 5
8462 "});
8463
8464 cx.lsp
8465 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8466 Ok(Some(vec![lsp::TextEdit::new(
8467 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8468 "PREFIX ".to_string(),
8469 )]))
8470 });
8471
8472 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8473 .unwrap()
8474 .await
8475 .unwrap();
8476
8477 cx.assert_editor_state(indoc! {"
8478 PREFIX line 1
8479 line 2
8480 linXe 3
8481 line 4
8482 liˇne 5
8483 "});
8484
8485 // Undo formatting
8486 cx.update_editor(|editor, window, cx| {
8487 editor.undo(&Default::default(), window, cx);
8488 });
8489
8490 // Verify cursor moved back to position after edit
8491 cx.assert_editor_state(indoc! {"
8492 line 1
8493 line 2
8494 linXˇe 3
8495 line 4
8496 line 5
8497 "});
8498}
8499
8500#[gpui::test]
8501async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8502 init_test(cx, |_| {});
8503
8504 let mut cx = EditorTestContext::new(cx).await;
8505
8506 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8507 cx.update_editor(|editor, window, cx| {
8508 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8509 });
8510
8511 cx.set_state(indoc! {"
8512 line 1
8513 line 2
8514 linˇe 3
8515 line 4
8516 line 5
8517 line 6
8518 line 7
8519 line 8
8520 line 9
8521 line 10
8522 "});
8523
8524 let snapshot = cx.buffer_snapshot();
8525 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8526
8527 cx.update(|_, cx| {
8528 provider.update(cx, |provider, _| {
8529 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8530 id: None,
8531 edits: vec![(edit_position..edit_position, "X".into())],
8532 edit_preview: None,
8533 }))
8534 })
8535 });
8536
8537 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8538 cx.update_editor(|editor, window, cx| {
8539 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8540 });
8541
8542 cx.assert_editor_state(indoc! {"
8543 line 1
8544 line 2
8545 lineXˇ 3
8546 line 4
8547 line 5
8548 line 6
8549 line 7
8550 line 8
8551 line 9
8552 line 10
8553 "});
8554
8555 cx.update_editor(|editor, window, cx| {
8556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8557 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8558 });
8559 });
8560
8561 cx.assert_editor_state(indoc! {"
8562 line 1
8563 line 2
8564 lineX 3
8565 line 4
8566 line 5
8567 line 6
8568 line 7
8569 line 8
8570 line 9
8571 liˇne 10
8572 "});
8573
8574 cx.update_editor(|editor, window, cx| {
8575 editor.undo(&Default::default(), window, cx);
8576 });
8577
8578 cx.assert_editor_state(indoc! {"
8579 line 1
8580 line 2
8581 lineˇ 3
8582 line 4
8583 line 5
8584 line 6
8585 line 7
8586 line 8
8587 line 9
8588 line 10
8589 "});
8590}
8591
8592#[gpui::test]
8593async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8594 init_test(cx, |_| {});
8595
8596 let mut cx = EditorTestContext::new(cx).await;
8597 cx.set_state(
8598 r#"let foo = 2;
8599lˇet foo = 2;
8600let fooˇ = 2;
8601let foo = 2;
8602let foo = ˇ2;"#,
8603 );
8604
8605 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8606 .unwrap();
8607 cx.assert_editor_state(
8608 r#"let foo = 2;
8609«letˇ» foo = 2;
8610let «fooˇ» = 2;
8611let foo = 2;
8612let foo = «2ˇ»;"#,
8613 );
8614
8615 // noop for multiple selections with different contents
8616 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8617 .unwrap();
8618 cx.assert_editor_state(
8619 r#"let foo = 2;
8620«letˇ» foo = 2;
8621let «fooˇ» = 2;
8622let foo = 2;
8623let foo = «2ˇ»;"#,
8624 );
8625
8626 // Test last selection direction should be preserved
8627 cx.set_state(
8628 r#"let foo = 2;
8629let foo = 2;
8630let «fooˇ» = 2;
8631let «ˇfoo» = 2;
8632let foo = 2;"#,
8633 );
8634
8635 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8636 .unwrap();
8637 cx.assert_editor_state(
8638 r#"let foo = 2;
8639let foo = 2;
8640let «fooˇ» = 2;
8641let «ˇfoo» = 2;
8642let «ˇfoo» = 2;"#,
8643 );
8644}
8645
8646#[gpui::test]
8647async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8648 init_test(cx, |_| {});
8649
8650 let mut cx =
8651 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8652
8653 cx.assert_editor_state(indoc! {"
8654 ˇbbb
8655 ccc
8656
8657 bbb
8658 ccc
8659 "});
8660 cx.dispatch_action(SelectPrevious::default());
8661 cx.assert_editor_state(indoc! {"
8662 «bbbˇ»
8663 ccc
8664
8665 bbb
8666 ccc
8667 "});
8668 cx.dispatch_action(SelectPrevious::default());
8669 cx.assert_editor_state(indoc! {"
8670 «bbbˇ»
8671 ccc
8672
8673 «bbbˇ»
8674 ccc
8675 "});
8676}
8677
8678#[gpui::test]
8679async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8680 init_test(cx, |_| {});
8681
8682 let mut cx = EditorTestContext::new(cx).await;
8683 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8684
8685 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8686 .unwrap();
8687 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8688
8689 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8690 .unwrap();
8691 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8692
8693 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8694 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8695
8696 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8697 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8698
8699 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8700 .unwrap();
8701 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8702
8703 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8704 .unwrap();
8705 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8706}
8707
8708#[gpui::test]
8709async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8710 init_test(cx, |_| {});
8711
8712 let mut cx = EditorTestContext::new(cx).await;
8713 cx.set_state("aˇ");
8714
8715 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8716 .unwrap();
8717 cx.assert_editor_state("«aˇ»");
8718 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8719 .unwrap();
8720 cx.assert_editor_state("«aˇ»");
8721}
8722
8723#[gpui::test]
8724async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728 cx.set_state(
8729 r#"let foo = 2;
8730lˇet foo = 2;
8731let fooˇ = 2;
8732let foo = 2;
8733let foo = ˇ2;"#,
8734 );
8735
8736 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8737 .unwrap();
8738 cx.assert_editor_state(
8739 r#"let foo = 2;
8740«letˇ» foo = 2;
8741let «fooˇ» = 2;
8742let foo = 2;
8743let foo = «2ˇ»;"#,
8744 );
8745
8746 // noop for multiple selections with different contents
8747 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756}
8757
8758#[gpui::test]
8759async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8760 init_test(cx, |_| {});
8761
8762 let mut cx = EditorTestContext::new(cx).await;
8763 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8764
8765 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8766 .unwrap();
8767 // selection direction is preserved
8768 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8769
8770 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8771 .unwrap();
8772 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8773
8774 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8775 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8776
8777 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8778 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8779
8780 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8783
8784 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8785 .unwrap();
8786 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8787}
8788
8789#[gpui::test]
8790async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8791 init_test(cx, |_| {});
8792
8793 let language = Arc::new(Language::new(
8794 LanguageConfig::default(),
8795 Some(tree_sitter_rust::LANGUAGE.into()),
8796 ));
8797
8798 let text = r#"
8799 use mod1::mod2::{mod3, mod4};
8800
8801 fn fn_1(param1: bool, param2: &str) {
8802 let var1 = "text";
8803 }
8804 "#
8805 .unindent();
8806
8807 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8809 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8810
8811 editor
8812 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8813 .await;
8814
8815 editor.update_in(cx, |editor, window, cx| {
8816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8817 s.select_display_ranges([
8818 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8819 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8820 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8821 ]);
8822 });
8823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8824 });
8825 editor.update(cx, |editor, cx| {
8826 assert_text_with_selections(
8827 editor,
8828 indoc! {r#"
8829 use mod1::mod2::{mod3, «mod4ˇ»};
8830
8831 fn fn_1«ˇ(param1: bool, param2: &str)» {
8832 let var1 = "«ˇtext»";
8833 }
8834 "#},
8835 cx,
8836 );
8837 });
8838
8839 editor.update_in(cx, |editor, window, cx| {
8840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8841 });
8842 editor.update(cx, |editor, cx| {
8843 assert_text_with_selections(
8844 editor,
8845 indoc! {r#"
8846 use mod1::mod2::«{mod3, mod4}ˇ»;
8847
8848 «ˇfn fn_1(param1: bool, param2: &str) {
8849 let var1 = "text";
8850 }»
8851 "#},
8852 cx,
8853 );
8854 });
8855
8856 editor.update_in(cx, |editor, window, cx| {
8857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8858 });
8859 assert_eq!(
8860 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8861 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8862 );
8863
8864 // Trying to expand the selected syntax node one more time has no effect.
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 assert_eq!(
8869 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8870 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8871 );
8872
8873 editor.update_in(cx, |editor, window, cx| {
8874 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8875 });
8876 editor.update(cx, |editor, cx| {
8877 assert_text_with_selections(
8878 editor,
8879 indoc! {r#"
8880 use mod1::mod2::«{mod3, mod4}ˇ»;
8881
8882 «ˇfn fn_1(param1: bool, param2: &str) {
8883 let var1 = "text";
8884 }»
8885 "#},
8886 cx,
8887 );
8888 });
8889
8890 editor.update_in(cx, |editor, window, cx| {
8891 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8892 });
8893 editor.update(cx, |editor, cx| {
8894 assert_text_with_selections(
8895 editor,
8896 indoc! {r#"
8897 use mod1::mod2::{mod3, «mod4ˇ»};
8898
8899 fn fn_1«ˇ(param1: bool, param2: &str)» {
8900 let var1 = "«ˇtext»";
8901 }
8902 "#},
8903 cx,
8904 );
8905 });
8906
8907 editor.update_in(cx, |editor, window, cx| {
8908 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8909 });
8910 editor.update(cx, |editor, cx| {
8911 assert_text_with_selections(
8912 editor,
8913 indoc! {r#"
8914 use mod1::mod2::{mod3, moˇd4};
8915
8916 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8917 let var1 = "teˇxt";
8918 }
8919 "#},
8920 cx,
8921 );
8922 });
8923
8924 // Trying to shrink the selected syntax node one more time has no effect.
8925 editor.update_in(cx, |editor, window, cx| {
8926 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8927 });
8928 editor.update_in(cx, |editor, _, cx| {
8929 assert_text_with_selections(
8930 editor,
8931 indoc! {r#"
8932 use mod1::mod2::{mod3, moˇd4};
8933
8934 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8935 let var1 = "teˇxt";
8936 }
8937 "#},
8938 cx,
8939 );
8940 });
8941
8942 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8943 // a fold.
8944 editor.update_in(cx, |editor, window, cx| {
8945 editor.fold_creases(
8946 vec![
8947 Crease::simple(
8948 Point::new(0, 21)..Point::new(0, 24),
8949 FoldPlaceholder::test(),
8950 ),
8951 Crease::simple(
8952 Point::new(3, 20)..Point::new(3, 22),
8953 FoldPlaceholder::test(),
8954 ),
8955 ],
8956 true,
8957 window,
8958 cx,
8959 );
8960 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8961 });
8962 editor.update(cx, |editor, cx| {
8963 assert_text_with_selections(
8964 editor,
8965 indoc! {r#"
8966 use mod1::mod2::«{mod3, mod4}ˇ»;
8967
8968 fn fn_1«ˇ(param1: bool, param2: &str)» {
8969 let var1 = "«ˇtext»";
8970 }
8971 "#},
8972 cx,
8973 );
8974 });
8975}
8976
8977#[gpui::test]
8978async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8979 init_test(cx, |_| {});
8980
8981 let language = Arc::new(Language::new(
8982 LanguageConfig::default(),
8983 Some(tree_sitter_rust::LANGUAGE.into()),
8984 ));
8985
8986 let text = "let a = 2;";
8987
8988 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8989 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8990 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8991
8992 editor
8993 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8994 .await;
8995
8996 // Test case 1: Cursor at end of word
8997 editor.update_in(cx, |editor, window, cx| {
8998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8999 s.select_display_ranges([
9000 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9001 ]);
9002 });
9003 });
9004 editor.update(cx, |editor, cx| {
9005 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9006 });
9007 editor.update_in(cx, |editor, window, cx| {
9008 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9009 });
9010 editor.update(cx, |editor, cx| {
9011 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9012 });
9013 editor.update_in(cx, |editor, window, cx| {
9014 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9015 });
9016 editor.update(cx, |editor, cx| {
9017 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9018 });
9019
9020 // Test case 2: Cursor at end of statement
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9023 s.select_display_ranges([
9024 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9025 ]);
9026 });
9027 });
9028 editor.update(cx, |editor, cx| {
9029 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9030 });
9031 editor.update_in(cx, |editor, window, cx| {
9032 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9033 });
9034 editor.update(cx, |editor, cx| {
9035 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9036 });
9037}
9038
9039#[gpui::test]
9040async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9041 init_test(cx, |_| {});
9042
9043 let language = Arc::new(Language::new(
9044 LanguageConfig {
9045 name: "JavaScript".into(),
9046 ..Default::default()
9047 },
9048 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9049 ));
9050
9051 let text = r#"
9052 let a = {
9053 key: "value",
9054 };
9055 "#
9056 .unindent();
9057
9058 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9059 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9060 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9061
9062 editor
9063 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9064 .await;
9065
9066 // Test case 1: Cursor after '{'
9067 editor.update_in(cx, |editor, window, cx| {
9068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9069 s.select_display_ranges([
9070 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9071 ]);
9072 });
9073 });
9074 editor.update(cx, |editor, cx| {
9075 assert_text_with_selections(
9076 editor,
9077 indoc! {r#"
9078 let a = {ˇ
9079 key: "value",
9080 };
9081 "#},
9082 cx,
9083 );
9084 });
9085 editor.update_in(cx, |editor, window, cx| {
9086 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9087 });
9088 editor.update(cx, |editor, cx| {
9089 assert_text_with_selections(
9090 editor,
9091 indoc! {r#"
9092 let a = «ˇ{
9093 key: "value",
9094 }»;
9095 "#},
9096 cx,
9097 );
9098 });
9099
9100 // Test case 2: Cursor after ':'
9101 editor.update_in(cx, |editor, window, cx| {
9102 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9103 s.select_display_ranges([
9104 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9105 ]);
9106 });
9107 });
9108 editor.update(cx, |editor, cx| {
9109 assert_text_with_selections(
9110 editor,
9111 indoc! {r#"
9112 let a = {
9113 key:ˇ "value",
9114 };
9115 "#},
9116 cx,
9117 );
9118 });
9119 editor.update_in(cx, |editor, window, cx| {
9120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9121 });
9122 editor.update(cx, |editor, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 let a = {
9127 «ˇkey: "value"»,
9128 };
9129 "#},
9130 cx,
9131 );
9132 });
9133 editor.update_in(cx, |editor, window, cx| {
9134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9135 });
9136 editor.update(cx, |editor, cx| {
9137 assert_text_with_selections(
9138 editor,
9139 indoc! {r#"
9140 let a = «ˇ{
9141 key: "value",
9142 }»;
9143 "#},
9144 cx,
9145 );
9146 });
9147
9148 // Test case 3: Cursor after ','
9149 editor.update_in(cx, |editor, window, cx| {
9150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9151 s.select_display_ranges([
9152 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9153 ]);
9154 });
9155 });
9156 editor.update(cx, |editor, cx| {
9157 assert_text_with_selections(
9158 editor,
9159 indoc! {r#"
9160 let a = {
9161 key: "value",ˇ
9162 };
9163 "#},
9164 cx,
9165 );
9166 });
9167 editor.update_in(cx, |editor, window, cx| {
9168 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9169 });
9170 editor.update(cx, |editor, cx| {
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 let a = «ˇ{
9175 key: "value",
9176 }»;
9177 "#},
9178 cx,
9179 );
9180 });
9181
9182 // Test case 4: Cursor after ';'
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9185 s.select_display_ranges([
9186 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9187 ]);
9188 });
9189 });
9190 editor.update(cx, |editor, cx| {
9191 assert_text_with_selections(
9192 editor,
9193 indoc! {r#"
9194 let a = {
9195 key: "value",
9196 };ˇ
9197 "#},
9198 cx,
9199 );
9200 });
9201 editor.update_in(cx, |editor, window, cx| {
9202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9203 });
9204 editor.update(cx, |editor, cx| {
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 «ˇlet a = {
9209 key: "value",
9210 };
9211 »"#},
9212 cx,
9213 );
9214 });
9215}
9216
9217#[gpui::test]
9218async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9219 init_test(cx, |_| {});
9220
9221 let language = Arc::new(Language::new(
9222 LanguageConfig::default(),
9223 Some(tree_sitter_rust::LANGUAGE.into()),
9224 ));
9225
9226 let text = r#"
9227 use mod1::mod2::{mod3, mod4};
9228
9229 fn fn_1(param1: bool, param2: &str) {
9230 let var1 = "hello world";
9231 }
9232 "#
9233 .unindent();
9234
9235 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9236 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9237 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9238
9239 editor
9240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9241 .await;
9242
9243 // Test 1: Cursor on a letter of a string word
9244 editor.update_in(cx, |editor, window, cx| {
9245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9246 s.select_display_ranges([
9247 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9248 ]);
9249 });
9250 });
9251 editor.update_in(cx, |editor, window, cx| {
9252 assert_text_with_selections(
9253 editor,
9254 indoc! {r#"
9255 use mod1::mod2::{mod3, mod4};
9256
9257 fn fn_1(param1: bool, param2: &str) {
9258 let var1 = "hˇello world";
9259 }
9260 "#},
9261 cx,
9262 );
9263 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9264 assert_text_with_selections(
9265 editor,
9266 indoc! {r#"
9267 use mod1::mod2::{mod3, mod4};
9268
9269 fn fn_1(param1: bool, param2: &str) {
9270 let var1 = "«ˇhello» world";
9271 }
9272 "#},
9273 cx,
9274 );
9275 });
9276
9277 // Test 2: Partial selection within a word
9278 editor.update_in(cx, |editor, window, cx| {
9279 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9280 s.select_display_ranges([
9281 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9282 ]);
9283 });
9284 });
9285 editor.update_in(cx, |editor, window, cx| {
9286 assert_text_with_selections(
9287 editor,
9288 indoc! {r#"
9289 use mod1::mod2::{mod3, mod4};
9290
9291 fn fn_1(param1: bool, param2: &str) {
9292 let var1 = "h«elˇ»lo world";
9293 }
9294 "#},
9295 cx,
9296 );
9297 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9298 assert_text_with_selections(
9299 editor,
9300 indoc! {r#"
9301 use mod1::mod2::{mod3, mod4};
9302
9303 fn fn_1(param1: bool, param2: &str) {
9304 let var1 = "«ˇhello» world";
9305 }
9306 "#},
9307 cx,
9308 );
9309 });
9310
9311 // Test 3: Complete word already selected
9312 editor.update_in(cx, |editor, window, cx| {
9313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9314 s.select_display_ranges([
9315 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9316 ]);
9317 });
9318 });
9319 editor.update_in(cx, |editor, window, cx| {
9320 assert_text_with_selections(
9321 editor,
9322 indoc! {r#"
9323 use mod1::mod2::{mod3, mod4};
9324
9325 fn fn_1(param1: bool, param2: &str) {
9326 let var1 = "«helloˇ» world";
9327 }
9328 "#},
9329 cx,
9330 );
9331 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9332 assert_text_with_selections(
9333 editor,
9334 indoc! {r#"
9335 use mod1::mod2::{mod3, mod4};
9336
9337 fn fn_1(param1: bool, param2: &str) {
9338 let var1 = "«hello worldˇ»";
9339 }
9340 "#},
9341 cx,
9342 );
9343 });
9344
9345 // Test 4: Selection spanning across words
9346 editor.update_in(cx, |editor, window, cx| {
9347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9348 s.select_display_ranges([
9349 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9350 ]);
9351 });
9352 });
9353 editor.update_in(cx, |editor, window, cx| {
9354 assert_text_with_selections(
9355 editor,
9356 indoc! {r#"
9357 use mod1::mod2::{mod3, mod4};
9358
9359 fn fn_1(param1: bool, param2: &str) {
9360 let var1 = "hel«lo woˇ»rld";
9361 }
9362 "#},
9363 cx,
9364 );
9365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9366 assert_text_with_selections(
9367 editor,
9368 indoc! {r#"
9369 use mod1::mod2::{mod3, mod4};
9370
9371 fn fn_1(param1: bool, param2: &str) {
9372 let var1 = "«ˇhello world»";
9373 }
9374 "#},
9375 cx,
9376 );
9377 });
9378
9379 // Test 5: Expansion beyond string
9380 editor.update_in(cx, |editor, window, cx| {
9381 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9382 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9383 assert_text_with_selections(
9384 editor,
9385 indoc! {r#"
9386 use mod1::mod2::{mod3, mod4};
9387
9388 fn fn_1(param1: bool, param2: &str) {
9389 «ˇlet var1 = "hello world";»
9390 }
9391 "#},
9392 cx,
9393 );
9394 });
9395}
9396
9397#[gpui::test]
9398async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9399 init_test(cx, |_| {});
9400
9401 let mut cx = EditorTestContext::new(cx).await;
9402
9403 let language = Arc::new(Language::new(
9404 LanguageConfig::default(),
9405 Some(tree_sitter_rust::LANGUAGE.into()),
9406 ));
9407
9408 cx.update_buffer(|buffer, cx| {
9409 buffer.set_language(Some(language), cx);
9410 });
9411
9412 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9413 cx.update_editor(|editor, window, cx| {
9414 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9415 });
9416
9417 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9418
9419 cx.set_state(indoc! { r#"fn a() {
9420 // what
9421 // a
9422 // ˇlong
9423 // method
9424 // I
9425 // sure
9426 // hope
9427 // it
9428 // works
9429 }"# });
9430
9431 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9432 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9433 cx.update(|_, cx| {
9434 multi_buffer.update(cx, |multi_buffer, cx| {
9435 multi_buffer.set_excerpts_for_path(
9436 PathKey::for_buffer(&buffer, cx),
9437 buffer,
9438 [Point::new(1, 0)..Point::new(1, 0)],
9439 3,
9440 cx,
9441 );
9442 });
9443 });
9444
9445 let editor2 = cx.new_window_entity(|window, cx| {
9446 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9447 });
9448
9449 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9450 cx.update_editor(|editor, window, cx| {
9451 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9452 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9453 })
9454 });
9455
9456 cx.assert_editor_state(indoc! { "
9457 fn a() {
9458 // what
9459 // a
9460 ˇ // long
9461 // method"});
9462
9463 cx.update_editor(|editor, window, cx| {
9464 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9465 });
9466
9467 // Although we could potentially make the action work when the syntax node
9468 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9469 // did. Maybe we could also expand the excerpt to contain the range?
9470 cx.assert_editor_state(indoc! { "
9471 fn a() {
9472 // what
9473 // a
9474 ˇ // long
9475 // method"});
9476}
9477
9478#[gpui::test]
9479async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9480 init_test(cx, |_| {});
9481
9482 let base_text = r#"
9483 impl A {
9484 // this is an uncommitted comment
9485
9486 fn b() {
9487 c();
9488 }
9489
9490 // this is another uncommitted comment
9491
9492 fn d() {
9493 // e
9494 // f
9495 }
9496 }
9497
9498 fn g() {
9499 // h
9500 }
9501 "#
9502 .unindent();
9503
9504 let text = r#"
9505 ˇimpl A {
9506
9507 fn b() {
9508 c();
9509 }
9510
9511 fn d() {
9512 // e
9513 // f
9514 }
9515 }
9516
9517 fn g() {
9518 // h
9519 }
9520 "#
9521 .unindent();
9522
9523 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9524 cx.set_state(&text);
9525 cx.set_head_text(&base_text);
9526 cx.update_editor(|editor, window, cx| {
9527 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9528 });
9529
9530 cx.assert_state_with_diff(
9531 "
9532 ˇimpl A {
9533 - // this is an uncommitted comment
9534
9535 fn b() {
9536 c();
9537 }
9538
9539 - // this is another uncommitted comment
9540 -
9541 fn d() {
9542 // e
9543 // f
9544 }
9545 }
9546
9547 fn g() {
9548 // h
9549 }
9550 "
9551 .unindent(),
9552 );
9553
9554 let expected_display_text = "
9555 impl A {
9556 // this is an uncommitted comment
9557
9558 fn b() {
9559 ⋯
9560 }
9561
9562 // this is another uncommitted comment
9563
9564 fn d() {
9565 ⋯
9566 }
9567 }
9568
9569 fn g() {
9570 ⋯
9571 }
9572 "
9573 .unindent();
9574
9575 cx.update_editor(|editor, window, cx| {
9576 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9577 assert_eq!(editor.display_text(cx), expected_display_text);
9578 });
9579}
9580
9581#[gpui::test]
9582async fn test_autoindent(cx: &mut TestAppContext) {
9583 init_test(cx, |_| {});
9584
9585 let language = Arc::new(
9586 Language::new(
9587 LanguageConfig {
9588 brackets: BracketPairConfig {
9589 pairs: vec![
9590 BracketPair {
9591 start: "{".to_string(),
9592 end: "}".to_string(),
9593 close: false,
9594 surround: false,
9595 newline: true,
9596 },
9597 BracketPair {
9598 start: "(".to_string(),
9599 end: ")".to_string(),
9600 close: false,
9601 surround: false,
9602 newline: true,
9603 },
9604 ],
9605 ..Default::default()
9606 },
9607 ..Default::default()
9608 },
9609 Some(tree_sitter_rust::LANGUAGE.into()),
9610 )
9611 .with_indents_query(
9612 r#"
9613 (_ "(" ")" @end) @indent
9614 (_ "{" "}" @end) @indent
9615 "#,
9616 )
9617 .unwrap(),
9618 );
9619
9620 let text = "fn a() {}";
9621
9622 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9623 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9625 editor
9626 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9627 .await;
9628
9629 editor.update_in(cx, |editor, window, cx| {
9630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9631 s.select_ranges([5..5, 8..8, 9..9])
9632 });
9633 editor.newline(&Newline, window, cx);
9634 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9635 assert_eq!(
9636 editor.selections.ranges(&editor.display_snapshot(cx)),
9637 &[
9638 Point::new(1, 4)..Point::new(1, 4),
9639 Point::new(3, 4)..Point::new(3, 4),
9640 Point::new(5, 0)..Point::new(5, 0)
9641 ]
9642 );
9643 });
9644}
9645
9646#[gpui::test]
9647async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9648 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9649
9650 let language = Arc::new(
9651 Language::new(
9652 LanguageConfig {
9653 brackets: BracketPairConfig {
9654 pairs: vec![
9655 BracketPair {
9656 start: "{".to_string(),
9657 end: "}".to_string(),
9658 close: false,
9659 surround: false,
9660 newline: true,
9661 },
9662 BracketPair {
9663 start: "(".to_string(),
9664 end: ")".to_string(),
9665 close: false,
9666 surround: false,
9667 newline: true,
9668 },
9669 ],
9670 ..Default::default()
9671 },
9672 ..Default::default()
9673 },
9674 Some(tree_sitter_rust::LANGUAGE.into()),
9675 )
9676 .with_indents_query(
9677 r#"
9678 (_ "(" ")" @end) @indent
9679 (_ "{" "}" @end) @indent
9680 "#,
9681 )
9682 .unwrap(),
9683 );
9684
9685 let text = "fn a() {}";
9686
9687 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9688 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9689 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9690 editor
9691 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9692 .await;
9693
9694 editor.update_in(cx, |editor, window, cx| {
9695 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9696 s.select_ranges([5..5, 8..8, 9..9])
9697 });
9698 editor.newline(&Newline, window, cx);
9699 assert_eq!(
9700 editor.text(cx),
9701 indoc!(
9702 "
9703 fn a(
9704
9705 ) {
9706
9707 }
9708 "
9709 )
9710 );
9711 assert_eq!(
9712 editor.selections.ranges(&editor.display_snapshot(cx)),
9713 &[
9714 Point::new(1, 0)..Point::new(1, 0),
9715 Point::new(3, 0)..Point::new(3, 0),
9716 Point::new(5, 0)..Point::new(5, 0)
9717 ]
9718 );
9719 });
9720}
9721
9722#[gpui::test]
9723async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9724 init_test(cx, |settings| {
9725 settings.defaults.auto_indent = Some(true);
9726 settings.languages.0.insert(
9727 "python".into(),
9728 LanguageSettingsContent {
9729 auto_indent: Some(false),
9730 ..Default::default()
9731 },
9732 );
9733 });
9734
9735 let mut cx = EditorTestContext::new(cx).await;
9736
9737 let injected_language = Arc::new(
9738 Language::new(
9739 LanguageConfig {
9740 brackets: BracketPairConfig {
9741 pairs: vec![
9742 BracketPair {
9743 start: "{".to_string(),
9744 end: "}".to_string(),
9745 close: false,
9746 surround: false,
9747 newline: true,
9748 },
9749 BracketPair {
9750 start: "(".to_string(),
9751 end: ")".to_string(),
9752 close: true,
9753 surround: false,
9754 newline: true,
9755 },
9756 ],
9757 ..Default::default()
9758 },
9759 name: "python".into(),
9760 ..Default::default()
9761 },
9762 Some(tree_sitter_python::LANGUAGE.into()),
9763 )
9764 .with_indents_query(
9765 r#"
9766 (_ "(" ")" @end) @indent
9767 (_ "{" "}" @end) @indent
9768 "#,
9769 )
9770 .unwrap(),
9771 );
9772
9773 let language = Arc::new(
9774 Language::new(
9775 LanguageConfig {
9776 brackets: BracketPairConfig {
9777 pairs: vec![
9778 BracketPair {
9779 start: "{".to_string(),
9780 end: "}".to_string(),
9781 close: false,
9782 surround: false,
9783 newline: true,
9784 },
9785 BracketPair {
9786 start: "(".to_string(),
9787 end: ")".to_string(),
9788 close: true,
9789 surround: false,
9790 newline: true,
9791 },
9792 ],
9793 ..Default::default()
9794 },
9795 name: LanguageName::new("rust"),
9796 ..Default::default()
9797 },
9798 Some(tree_sitter_rust::LANGUAGE.into()),
9799 )
9800 .with_indents_query(
9801 r#"
9802 (_ "(" ")" @end) @indent
9803 (_ "{" "}" @end) @indent
9804 "#,
9805 )
9806 .unwrap()
9807 .with_injection_query(
9808 r#"
9809 (macro_invocation
9810 macro: (identifier) @_macro_name
9811 (token_tree) @injection.content
9812 (#set! injection.language "python"))
9813 "#,
9814 )
9815 .unwrap(),
9816 );
9817
9818 cx.language_registry().add(injected_language);
9819 cx.language_registry().add(language.clone());
9820
9821 cx.update_buffer(|buffer, cx| {
9822 buffer.set_language(Some(language), cx);
9823 });
9824
9825 cx.set_state(r#"struct A {ˇ}"#);
9826
9827 cx.update_editor(|editor, window, cx| {
9828 editor.newline(&Default::default(), window, cx);
9829 });
9830
9831 cx.assert_editor_state(indoc!(
9832 "struct A {
9833 ˇ
9834 }"
9835 ));
9836
9837 cx.set_state(r#"select_biased!(ˇ)"#);
9838
9839 cx.update_editor(|editor, window, cx| {
9840 editor.newline(&Default::default(), window, cx);
9841 editor.handle_input("def ", window, cx);
9842 editor.handle_input("(", window, cx);
9843 editor.newline(&Default::default(), window, cx);
9844 editor.handle_input("a", window, cx);
9845 });
9846
9847 cx.assert_editor_state(indoc!(
9848 "select_biased!(
9849 def (
9850 aˇ
9851 )
9852 )"
9853 ));
9854}
9855
9856#[gpui::test]
9857async fn test_autoindent_selections(cx: &mut TestAppContext) {
9858 init_test(cx, |_| {});
9859
9860 {
9861 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9862 cx.set_state(indoc! {"
9863 impl A {
9864
9865 fn b() {}
9866
9867 «fn c() {
9868
9869 }ˇ»
9870 }
9871 "});
9872
9873 cx.update_editor(|editor, window, cx| {
9874 editor.autoindent(&Default::default(), window, cx);
9875 });
9876
9877 cx.assert_editor_state(indoc! {"
9878 impl A {
9879
9880 fn b() {}
9881
9882 «fn c() {
9883
9884 }ˇ»
9885 }
9886 "});
9887 }
9888
9889 {
9890 let mut cx = EditorTestContext::new_multibuffer(
9891 cx,
9892 [indoc! { "
9893 impl A {
9894 «
9895 // a
9896 fn b(){}
9897 »
9898 «
9899 }
9900 fn c(){}
9901 »
9902 "}],
9903 );
9904
9905 let buffer = cx.update_editor(|editor, _, cx| {
9906 let buffer = editor.buffer().update(cx, |buffer, _| {
9907 buffer.all_buffers().iter().next().unwrap().clone()
9908 });
9909 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9910 buffer
9911 });
9912
9913 cx.run_until_parked();
9914 cx.update_editor(|editor, window, cx| {
9915 editor.select_all(&Default::default(), window, cx);
9916 editor.autoindent(&Default::default(), window, cx)
9917 });
9918 cx.run_until_parked();
9919
9920 cx.update(|_, cx| {
9921 assert_eq!(
9922 buffer.read(cx).text(),
9923 indoc! { "
9924 impl A {
9925
9926 // a
9927 fn b(){}
9928
9929
9930 }
9931 fn c(){}
9932
9933 " }
9934 )
9935 });
9936 }
9937}
9938
9939#[gpui::test]
9940async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9941 init_test(cx, |_| {});
9942
9943 let mut cx = EditorTestContext::new(cx).await;
9944
9945 let language = Arc::new(Language::new(
9946 LanguageConfig {
9947 brackets: BracketPairConfig {
9948 pairs: vec![
9949 BracketPair {
9950 start: "{".to_string(),
9951 end: "}".to_string(),
9952 close: true,
9953 surround: true,
9954 newline: true,
9955 },
9956 BracketPair {
9957 start: "(".to_string(),
9958 end: ")".to_string(),
9959 close: true,
9960 surround: true,
9961 newline: true,
9962 },
9963 BracketPair {
9964 start: "/*".to_string(),
9965 end: " */".to_string(),
9966 close: true,
9967 surround: true,
9968 newline: true,
9969 },
9970 BracketPair {
9971 start: "[".to_string(),
9972 end: "]".to_string(),
9973 close: false,
9974 surround: false,
9975 newline: true,
9976 },
9977 BracketPair {
9978 start: "\"".to_string(),
9979 end: "\"".to_string(),
9980 close: true,
9981 surround: true,
9982 newline: false,
9983 },
9984 BracketPair {
9985 start: "<".to_string(),
9986 end: ">".to_string(),
9987 close: false,
9988 surround: true,
9989 newline: true,
9990 },
9991 ],
9992 ..Default::default()
9993 },
9994 autoclose_before: "})]".to_string(),
9995 ..Default::default()
9996 },
9997 Some(tree_sitter_rust::LANGUAGE.into()),
9998 ));
9999
10000 cx.language_registry().add(language.clone());
10001 cx.update_buffer(|buffer, cx| {
10002 buffer.set_language(Some(language), cx);
10003 });
10004
10005 cx.set_state(
10006 &r#"
10007 🏀ˇ
10008 εˇ
10009 ❤️ˇ
10010 "#
10011 .unindent(),
10012 );
10013
10014 // autoclose multiple nested brackets at multiple cursors
10015 cx.update_editor(|editor, window, cx| {
10016 editor.handle_input("{", window, cx);
10017 editor.handle_input("{", window, cx);
10018 editor.handle_input("{", window, cx);
10019 });
10020 cx.assert_editor_state(
10021 &"
10022 🏀{{{ˇ}}}
10023 ε{{{ˇ}}}
10024 ❤️{{{ˇ}}}
10025 "
10026 .unindent(),
10027 );
10028
10029 // insert a different closing bracket
10030 cx.update_editor(|editor, window, cx| {
10031 editor.handle_input(")", window, cx);
10032 });
10033 cx.assert_editor_state(
10034 &"
10035 🏀{{{)ˇ}}}
10036 ε{{{)ˇ}}}
10037 ❤️{{{)ˇ}}}
10038 "
10039 .unindent(),
10040 );
10041
10042 // skip over the auto-closed brackets when typing a closing bracket
10043 cx.update_editor(|editor, window, cx| {
10044 editor.move_right(&MoveRight, window, cx);
10045 editor.handle_input("}", window, cx);
10046 editor.handle_input("}", window, cx);
10047 editor.handle_input("}", window, cx);
10048 });
10049 cx.assert_editor_state(
10050 &"
10051 🏀{{{)}}}}ˇ
10052 ε{{{)}}}}ˇ
10053 ❤️{{{)}}}}ˇ
10054 "
10055 .unindent(),
10056 );
10057
10058 // autoclose multi-character pairs
10059 cx.set_state(
10060 &"
10061 ˇ
10062 ˇ
10063 "
10064 .unindent(),
10065 );
10066 cx.update_editor(|editor, window, cx| {
10067 editor.handle_input("/", window, cx);
10068 editor.handle_input("*", window, cx);
10069 });
10070 cx.assert_editor_state(
10071 &"
10072 /*ˇ */
10073 /*ˇ */
10074 "
10075 .unindent(),
10076 );
10077
10078 // one cursor autocloses a multi-character pair, one cursor
10079 // does not autoclose.
10080 cx.set_state(
10081 &"
10082 /ˇ
10083 ˇ
10084 "
10085 .unindent(),
10086 );
10087 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10088 cx.assert_editor_state(
10089 &"
10090 /*ˇ */
10091 *ˇ
10092 "
10093 .unindent(),
10094 );
10095
10096 // Don't autoclose if the next character isn't whitespace and isn't
10097 // listed in the language's "autoclose_before" section.
10098 cx.set_state("ˇa b");
10099 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10100 cx.assert_editor_state("{ˇa b");
10101
10102 // Don't autoclose if `close` is false for the bracket pair
10103 cx.set_state("ˇ");
10104 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10105 cx.assert_editor_state("[ˇ");
10106
10107 // Surround with brackets if text is selected
10108 cx.set_state("«aˇ» b");
10109 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10110 cx.assert_editor_state("{«aˇ»} b");
10111
10112 // Autoclose when not immediately after a word character
10113 cx.set_state("a ˇ");
10114 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10115 cx.assert_editor_state("a \"ˇ\"");
10116
10117 // Autoclose pair where the start and end characters are the same
10118 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10119 cx.assert_editor_state("a \"\"ˇ");
10120
10121 // Don't autoclose when immediately after a word character
10122 cx.set_state("aˇ");
10123 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10124 cx.assert_editor_state("a\"ˇ");
10125
10126 // Do autoclose when after a non-word character
10127 cx.set_state("{ˇ");
10128 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10129 cx.assert_editor_state("{\"ˇ\"");
10130
10131 // Non identical pairs autoclose regardless of preceding character
10132 cx.set_state("aˇ");
10133 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10134 cx.assert_editor_state("a{ˇ}");
10135
10136 // Don't autoclose pair if autoclose is disabled
10137 cx.set_state("ˇ");
10138 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10139 cx.assert_editor_state("<ˇ");
10140
10141 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10142 cx.set_state("«aˇ» b");
10143 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10144 cx.assert_editor_state("<«aˇ»> b");
10145}
10146
10147#[gpui::test]
10148async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10149 init_test(cx, |settings| {
10150 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10151 });
10152
10153 let mut cx = EditorTestContext::new(cx).await;
10154
10155 let language = Arc::new(Language::new(
10156 LanguageConfig {
10157 brackets: BracketPairConfig {
10158 pairs: vec![
10159 BracketPair {
10160 start: "{".to_string(),
10161 end: "}".to_string(),
10162 close: true,
10163 surround: true,
10164 newline: true,
10165 },
10166 BracketPair {
10167 start: "(".to_string(),
10168 end: ")".to_string(),
10169 close: true,
10170 surround: true,
10171 newline: true,
10172 },
10173 BracketPair {
10174 start: "[".to_string(),
10175 end: "]".to_string(),
10176 close: false,
10177 surround: false,
10178 newline: true,
10179 },
10180 ],
10181 ..Default::default()
10182 },
10183 autoclose_before: "})]".to_string(),
10184 ..Default::default()
10185 },
10186 Some(tree_sitter_rust::LANGUAGE.into()),
10187 ));
10188
10189 cx.language_registry().add(language.clone());
10190 cx.update_buffer(|buffer, cx| {
10191 buffer.set_language(Some(language), cx);
10192 });
10193
10194 cx.set_state(
10195 &"
10196 ˇ
10197 ˇ
10198 ˇ
10199 "
10200 .unindent(),
10201 );
10202
10203 // ensure only matching closing brackets are skipped over
10204 cx.update_editor(|editor, window, cx| {
10205 editor.handle_input("}", window, cx);
10206 editor.move_left(&MoveLeft, window, cx);
10207 editor.handle_input(")", window, cx);
10208 editor.move_left(&MoveLeft, window, cx);
10209 });
10210 cx.assert_editor_state(
10211 &"
10212 ˇ)}
10213 ˇ)}
10214 ˇ)}
10215 "
10216 .unindent(),
10217 );
10218
10219 // skip-over closing brackets at multiple cursors
10220 cx.update_editor(|editor, window, cx| {
10221 editor.handle_input(")", window, cx);
10222 editor.handle_input("}", window, cx);
10223 });
10224 cx.assert_editor_state(
10225 &"
10226 )}ˇ
10227 )}ˇ
10228 )}ˇ
10229 "
10230 .unindent(),
10231 );
10232
10233 // ignore non-close brackets
10234 cx.update_editor(|editor, window, cx| {
10235 editor.handle_input("]", window, cx);
10236 editor.move_left(&MoveLeft, window, cx);
10237 editor.handle_input("]", window, cx);
10238 });
10239 cx.assert_editor_state(
10240 &"
10241 )}]ˇ]
10242 )}]ˇ]
10243 )}]ˇ]
10244 "
10245 .unindent(),
10246 );
10247}
10248
10249#[gpui::test]
10250async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10251 init_test(cx, |_| {});
10252
10253 let mut cx = EditorTestContext::new(cx).await;
10254
10255 let html_language = Arc::new(
10256 Language::new(
10257 LanguageConfig {
10258 name: "HTML".into(),
10259 brackets: BracketPairConfig {
10260 pairs: vec![
10261 BracketPair {
10262 start: "<".into(),
10263 end: ">".into(),
10264 close: true,
10265 ..Default::default()
10266 },
10267 BracketPair {
10268 start: "{".into(),
10269 end: "}".into(),
10270 close: true,
10271 ..Default::default()
10272 },
10273 BracketPair {
10274 start: "(".into(),
10275 end: ")".into(),
10276 close: true,
10277 ..Default::default()
10278 },
10279 ],
10280 ..Default::default()
10281 },
10282 autoclose_before: "})]>".into(),
10283 ..Default::default()
10284 },
10285 Some(tree_sitter_html::LANGUAGE.into()),
10286 )
10287 .with_injection_query(
10288 r#"
10289 (script_element
10290 (raw_text) @injection.content
10291 (#set! injection.language "javascript"))
10292 "#,
10293 )
10294 .unwrap(),
10295 );
10296
10297 let javascript_language = Arc::new(Language::new(
10298 LanguageConfig {
10299 name: "JavaScript".into(),
10300 brackets: BracketPairConfig {
10301 pairs: vec![
10302 BracketPair {
10303 start: "/*".into(),
10304 end: " */".into(),
10305 close: true,
10306 ..Default::default()
10307 },
10308 BracketPair {
10309 start: "{".into(),
10310 end: "}".into(),
10311 close: true,
10312 ..Default::default()
10313 },
10314 BracketPair {
10315 start: "(".into(),
10316 end: ")".into(),
10317 close: true,
10318 ..Default::default()
10319 },
10320 ],
10321 ..Default::default()
10322 },
10323 autoclose_before: "})]>".into(),
10324 ..Default::default()
10325 },
10326 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10327 ));
10328
10329 cx.language_registry().add(html_language.clone());
10330 cx.language_registry().add(javascript_language);
10331 cx.executor().run_until_parked();
10332
10333 cx.update_buffer(|buffer, cx| {
10334 buffer.set_language(Some(html_language), cx);
10335 });
10336
10337 cx.set_state(
10338 &r#"
10339 <body>ˇ
10340 <script>
10341 var x = 1;ˇ
10342 </script>
10343 </body>ˇ
10344 "#
10345 .unindent(),
10346 );
10347
10348 // Precondition: different languages are active at different locations.
10349 cx.update_editor(|editor, window, cx| {
10350 let snapshot = editor.snapshot(window, cx);
10351 let cursors = editor
10352 .selections
10353 .ranges::<usize>(&editor.display_snapshot(cx));
10354 let languages = cursors
10355 .iter()
10356 .map(|c| snapshot.language_at(c.start).unwrap().name())
10357 .collect::<Vec<_>>();
10358 assert_eq!(
10359 languages,
10360 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10361 );
10362 });
10363
10364 // Angle brackets autoclose in HTML, but not JavaScript.
10365 cx.update_editor(|editor, window, cx| {
10366 editor.handle_input("<", window, cx);
10367 editor.handle_input("a", window, cx);
10368 });
10369 cx.assert_editor_state(
10370 &r#"
10371 <body><aˇ>
10372 <script>
10373 var x = 1;<aˇ
10374 </script>
10375 </body><aˇ>
10376 "#
10377 .unindent(),
10378 );
10379
10380 // Curly braces and parens autoclose in both HTML and JavaScript.
10381 cx.update_editor(|editor, window, cx| {
10382 editor.handle_input(" b=", window, cx);
10383 editor.handle_input("{", window, cx);
10384 editor.handle_input("c", window, cx);
10385 editor.handle_input("(", window, cx);
10386 });
10387 cx.assert_editor_state(
10388 &r#"
10389 <body><a b={c(ˇ)}>
10390 <script>
10391 var x = 1;<a b={c(ˇ)}
10392 </script>
10393 </body><a b={c(ˇ)}>
10394 "#
10395 .unindent(),
10396 );
10397
10398 // Brackets that were already autoclosed are skipped.
10399 cx.update_editor(|editor, window, cx| {
10400 editor.handle_input(")", window, cx);
10401 editor.handle_input("d", window, cx);
10402 editor.handle_input("}", window, cx);
10403 });
10404 cx.assert_editor_state(
10405 &r#"
10406 <body><a b={c()d}ˇ>
10407 <script>
10408 var x = 1;<a b={c()d}ˇ
10409 </script>
10410 </body><a b={c()d}ˇ>
10411 "#
10412 .unindent(),
10413 );
10414 cx.update_editor(|editor, window, cx| {
10415 editor.handle_input(">", window, cx);
10416 });
10417 cx.assert_editor_state(
10418 &r#"
10419 <body><a b={c()d}>ˇ
10420 <script>
10421 var x = 1;<a b={c()d}>ˇ
10422 </script>
10423 </body><a b={c()d}>ˇ
10424 "#
10425 .unindent(),
10426 );
10427
10428 // Reset
10429 cx.set_state(
10430 &r#"
10431 <body>ˇ
10432 <script>
10433 var x = 1;ˇ
10434 </script>
10435 </body>ˇ
10436 "#
10437 .unindent(),
10438 );
10439
10440 cx.update_editor(|editor, window, cx| {
10441 editor.handle_input("<", window, cx);
10442 });
10443 cx.assert_editor_state(
10444 &r#"
10445 <body><ˇ>
10446 <script>
10447 var x = 1;<ˇ
10448 </script>
10449 </body><ˇ>
10450 "#
10451 .unindent(),
10452 );
10453
10454 // When backspacing, the closing angle brackets are removed.
10455 cx.update_editor(|editor, window, cx| {
10456 editor.backspace(&Backspace, window, cx);
10457 });
10458 cx.assert_editor_state(
10459 &r#"
10460 <body>ˇ
10461 <script>
10462 var x = 1;ˇ
10463 </script>
10464 </body>ˇ
10465 "#
10466 .unindent(),
10467 );
10468
10469 // Block comments autoclose in JavaScript, but not HTML.
10470 cx.update_editor(|editor, window, cx| {
10471 editor.handle_input("/", window, cx);
10472 editor.handle_input("*", window, cx);
10473 });
10474 cx.assert_editor_state(
10475 &r#"
10476 <body>/*ˇ
10477 <script>
10478 var x = 1;/*ˇ */
10479 </script>
10480 </body>/*ˇ
10481 "#
10482 .unindent(),
10483 );
10484}
10485
10486#[gpui::test]
10487async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10488 init_test(cx, |_| {});
10489
10490 let mut cx = EditorTestContext::new(cx).await;
10491
10492 let rust_language = Arc::new(
10493 Language::new(
10494 LanguageConfig {
10495 name: "Rust".into(),
10496 brackets: serde_json::from_value(json!([
10497 { "start": "{", "end": "}", "close": true, "newline": true },
10498 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10499 ]))
10500 .unwrap(),
10501 autoclose_before: "})]>".into(),
10502 ..Default::default()
10503 },
10504 Some(tree_sitter_rust::LANGUAGE.into()),
10505 )
10506 .with_override_query("(string_literal) @string")
10507 .unwrap(),
10508 );
10509
10510 cx.language_registry().add(rust_language.clone());
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(rust_language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 let x = ˇ
10518 "#
10519 .unindent(),
10520 );
10521
10522 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10523 cx.update_editor(|editor, window, cx| {
10524 editor.handle_input("\"", window, cx);
10525 });
10526 cx.assert_editor_state(
10527 &r#"
10528 let x = "ˇ"
10529 "#
10530 .unindent(),
10531 );
10532
10533 // Inserting another quotation mark. The cursor moves across the existing
10534 // automatically-inserted quotation mark.
10535 cx.update_editor(|editor, window, cx| {
10536 editor.handle_input("\"", window, cx);
10537 });
10538 cx.assert_editor_state(
10539 &r#"
10540 let x = ""ˇ
10541 "#
10542 .unindent(),
10543 );
10544
10545 // Reset
10546 cx.set_state(
10547 &r#"
10548 let x = ˇ
10549 "#
10550 .unindent(),
10551 );
10552
10553 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10554 cx.update_editor(|editor, window, cx| {
10555 editor.handle_input("\"", window, cx);
10556 editor.handle_input(" ", window, cx);
10557 editor.move_left(&Default::default(), window, cx);
10558 editor.handle_input("\\", window, cx);
10559 editor.handle_input("\"", window, cx);
10560 });
10561 cx.assert_editor_state(
10562 &r#"
10563 let x = "\"ˇ "
10564 "#
10565 .unindent(),
10566 );
10567
10568 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10569 // mark. Nothing is inserted.
10570 cx.update_editor(|editor, window, cx| {
10571 editor.move_right(&Default::default(), window, cx);
10572 editor.handle_input("\"", window, cx);
10573 });
10574 cx.assert_editor_state(
10575 &r#"
10576 let x = "\" "ˇ
10577 "#
10578 .unindent(),
10579 );
10580}
10581
10582#[gpui::test]
10583async fn test_surround_with_pair(cx: &mut TestAppContext) {
10584 init_test(cx, |_| {});
10585
10586 let language = Arc::new(Language::new(
10587 LanguageConfig {
10588 brackets: BracketPairConfig {
10589 pairs: vec![
10590 BracketPair {
10591 start: "{".to_string(),
10592 end: "}".to_string(),
10593 close: true,
10594 surround: true,
10595 newline: true,
10596 },
10597 BracketPair {
10598 start: "/* ".to_string(),
10599 end: "*/".to_string(),
10600 close: true,
10601 surround: true,
10602 ..Default::default()
10603 },
10604 ],
10605 ..Default::default()
10606 },
10607 ..Default::default()
10608 },
10609 Some(tree_sitter_rust::LANGUAGE.into()),
10610 ));
10611
10612 let text = r#"
10613 a
10614 b
10615 c
10616 "#
10617 .unindent();
10618
10619 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10620 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10621 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10622 editor
10623 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10624 .await;
10625
10626 editor.update_in(cx, |editor, window, cx| {
10627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10628 s.select_display_ranges([
10629 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10631 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10632 ])
10633 });
10634
10635 editor.handle_input("{", window, cx);
10636 editor.handle_input("{", window, cx);
10637 editor.handle_input("{", window, cx);
10638 assert_eq!(
10639 editor.text(cx),
10640 "
10641 {{{a}}}
10642 {{{b}}}
10643 {{{c}}}
10644 "
10645 .unindent()
10646 );
10647 assert_eq!(
10648 editor.selections.display_ranges(cx),
10649 [
10650 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10651 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10652 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10653 ]
10654 );
10655
10656 editor.undo(&Undo, window, cx);
10657 editor.undo(&Undo, window, cx);
10658 editor.undo(&Undo, window, cx);
10659 assert_eq!(
10660 editor.text(cx),
10661 "
10662 a
10663 b
10664 c
10665 "
10666 .unindent()
10667 );
10668 assert_eq!(
10669 editor.selections.display_ranges(cx),
10670 [
10671 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10672 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10673 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10674 ]
10675 );
10676
10677 // Ensure inserting the first character of a multi-byte bracket pair
10678 // doesn't surround the selections with the bracket.
10679 editor.handle_input("/", window, cx);
10680 assert_eq!(
10681 editor.text(cx),
10682 "
10683 /
10684 /
10685 /
10686 "
10687 .unindent()
10688 );
10689 assert_eq!(
10690 editor.selections.display_ranges(cx),
10691 [
10692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10693 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10694 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10695 ]
10696 );
10697
10698 editor.undo(&Undo, window, cx);
10699 assert_eq!(
10700 editor.text(cx),
10701 "
10702 a
10703 b
10704 c
10705 "
10706 .unindent()
10707 );
10708 assert_eq!(
10709 editor.selections.display_ranges(cx),
10710 [
10711 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10713 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10714 ]
10715 );
10716
10717 // Ensure inserting the last character of a multi-byte bracket pair
10718 // doesn't surround the selections with the bracket.
10719 editor.handle_input("*", window, cx);
10720 assert_eq!(
10721 editor.text(cx),
10722 "
10723 *
10724 *
10725 *
10726 "
10727 .unindent()
10728 );
10729 assert_eq!(
10730 editor.selections.display_ranges(cx),
10731 [
10732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10733 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10734 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10735 ]
10736 );
10737 });
10738}
10739
10740#[gpui::test]
10741async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10742 init_test(cx, |_| {});
10743
10744 let language = Arc::new(Language::new(
10745 LanguageConfig {
10746 brackets: BracketPairConfig {
10747 pairs: vec![BracketPair {
10748 start: "{".to_string(),
10749 end: "}".to_string(),
10750 close: true,
10751 surround: true,
10752 newline: true,
10753 }],
10754 ..Default::default()
10755 },
10756 autoclose_before: "}".to_string(),
10757 ..Default::default()
10758 },
10759 Some(tree_sitter_rust::LANGUAGE.into()),
10760 ));
10761
10762 let text = r#"
10763 a
10764 b
10765 c
10766 "#
10767 .unindent();
10768
10769 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10770 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10771 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10772 editor
10773 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10774 .await;
10775
10776 editor.update_in(cx, |editor, window, cx| {
10777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10778 s.select_ranges([
10779 Point::new(0, 1)..Point::new(0, 1),
10780 Point::new(1, 1)..Point::new(1, 1),
10781 Point::new(2, 1)..Point::new(2, 1),
10782 ])
10783 });
10784
10785 editor.handle_input("{", window, cx);
10786 editor.handle_input("{", window, cx);
10787 editor.handle_input("_", window, cx);
10788 assert_eq!(
10789 editor.text(cx),
10790 "
10791 a{{_}}
10792 b{{_}}
10793 c{{_}}
10794 "
10795 .unindent()
10796 );
10797 assert_eq!(
10798 editor
10799 .selections
10800 .ranges::<Point>(&editor.display_snapshot(cx)),
10801 [
10802 Point::new(0, 4)..Point::new(0, 4),
10803 Point::new(1, 4)..Point::new(1, 4),
10804 Point::new(2, 4)..Point::new(2, 4)
10805 ]
10806 );
10807
10808 editor.backspace(&Default::default(), window, cx);
10809 editor.backspace(&Default::default(), window, cx);
10810 assert_eq!(
10811 editor.text(cx),
10812 "
10813 a{}
10814 b{}
10815 c{}
10816 "
10817 .unindent()
10818 );
10819 assert_eq!(
10820 editor
10821 .selections
10822 .ranges::<Point>(&editor.display_snapshot(cx)),
10823 [
10824 Point::new(0, 2)..Point::new(0, 2),
10825 Point::new(1, 2)..Point::new(1, 2),
10826 Point::new(2, 2)..Point::new(2, 2)
10827 ]
10828 );
10829
10830 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10831 assert_eq!(
10832 editor.text(cx),
10833 "
10834 a
10835 b
10836 c
10837 "
10838 .unindent()
10839 );
10840 assert_eq!(
10841 editor
10842 .selections
10843 .ranges::<Point>(&editor.display_snapshot(cx)),
10844 [
10845 Point::new(0, 1)..Point::new(0, 1),
10846 Point::new(1, 1)..Point::new(1, 1),
10847 Point::new(2, 1)..Point::new(2, 1)
10848 ]
10849 );
10850 });
10851}
10852
10853#[gpui::test]
10854async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10855 init_test(cx, |settings| {
10856 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10857 });
10858
10859 let mut cx = EditorTestContext::new(cx).await;
10860
10861 let language = Arc::new(Language::new(
10862 LanguageConfig {
10863 brackets: BracketPairConfig {
10864 pairs: vec![
10865 BracketPair {
10866 start: "{".to_string(),
10867 end: "}".to_string(),
10868 close: true,
10869 surround: true,
10870 newline: true,
10871 },
10872 BracketPair {
10873 start: "(".to_string(),
10874 end: ")".to_string(),
10875 close: true,
10876 surround: true,
10877 newline: true,
10878 },
10879 BracketPair {
10880 start: "[".to_string(),
10881 end: "]".to_string(),
10882 close: false,
10883 surround: true,
10884 newline: true,
10885 },
10886 ],
10887 ..Default::default()
10888 },
10889 autoclose_before: "})]".to_string(),
10890 ..Default::default()
10891 },
10892 Some(tree_sitter_rust::LANGUAGE.into()),
10893 ));
10894
10895 cx.language_registry().add(language.clone());
10896 cx.update_buffer(|buffer, cx| {
10897 buffer.set_language(Some(language), cx);
10898 });
10899
10900 cx.set_state(
10901 &"
10902 {(ˇ)}
10903 [[ˇ]]
10904 {(ˇ)}
10905 "
10906 .unindent(),
10907 );
10908
10909 cx.update_editor(|editor, window, cx| {
10910 editor.backspace(&Default::default(), window, cx);
10911 editor.backspace(&Default::default(), window, cx);
10912 });
10913
10914 cx.assert_editor_state(
10915 &"
10916 ˇ
10917 ˇ]]
10918 ˇ
10919 "
10920 .unindent(),
10921 );
10922
10923 cx.update_editor(|editor, window, cx| {
10924 editor.handle_input("{", window, cx);
10925 editor.handle_input("{", window, cx);
10926 editor.move_right(&MoveRight, window, cx);
10927 editor.move_right(&MoveRight, window, cx);
10928 editor.move_left(&MoveLeft, window, cx);
10929 editor.move_left(&MoveLeft, window, cx);
10930 editor.backspace(&Default::default(), window, cx);
10931 });
10932
10933 cx.assert_editor_state(
10934 &"
10935 {ˇ}
10936 {ˇ}]]
10937 {ˇ}
10938 "
10939 .unindent(),
10940 );
10941
10942 cx.update_editor(|editor, window, cx| {
10943 editor.backspace(&Default::default(), window, cx);
10944 });
10945
10946 cx.assert_editor_state(
10947 &"
10948 ˇ
10949 ˇ]]
10950 ˇ
10951 "
10952 .unindent(),
10953 );
10954}
10955
10956#[gpui::test]
10957async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10958 init_test(cx, |_| {});
10959
10960 let language = Arc::new(Language::new(
10961 LanguageConfig::default(),
10962 Some(tree_sitter_rust::LANGUAGE.into()),
10963 ));
10964
10965 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10968 editor
10969 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10970 .await;
10971
10972 editor.update_in(cx, |editor, window, cx| {
10973 editor.set_auto_replace_emoji_shortcode(true);
10974
10975 editor.handle_input("Hello ", window, cx);
10976 editor.handle_input(":wave", window, cx);
10977 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10978
10979 editor.handle_input(":", window, cx);
10980 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10981
10982 editor.handle_input(" :smile", window, cx);
10983 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10984
10985 editor.handle_input(":", window, cx);
10986 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10987
10988 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10989 editor.handle_input(":wave", window, cx);
10990 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10991
10992 editor.handle_input(":", window, cx);
10993 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10994
10995 editor.handle_input(":1", window, cx);
10996 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10997
10998 editor.handle_input(":", window, cx);
10999 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11000
11001 // Ensure shortcode does not get replaced when it is part of a word
11002 editor.handle_input(" Test:wave", window, cx);
11003 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11004
11005 editor.handle_input(":", window, cx);
11006 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11007
11008 editor.set_auto_replace_emoji_shortcode(false);
11009
11010 // Ensure shortcode does not get replaced when auto replace is off
11011 editor.handle_input(" :wave", window, cx);
11012 assert_eq!(
11013 editor.text(cx),
11014 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11015 );
11016
11017 editor.handle_input(":", window, cx);
11018 assert_eq!(
11019 editor.text(cx),
11020 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11021 );
11022 });
11023}
11024
11025#[gpui::test]
11026async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11027 init_test(cx, |_| {});
11028
11029 let (text, insertion_ranges) = marked_text_ranges(
11030 indoc! {"
11031 ˇ
11032 "},
11033 false,
11034 );
11035
11036 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11037 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11038
11039 _ = editor.update_in(cx, |editor, window, cx| {
11040 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11041
11042 editor
11043 .insert_snippet(&insertion_ranges, snippet, window, cx)
11044 .unwrap();
11045
11046 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11047 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11048 assert_eq!(editor.text(cx), expected_text);
11049 assert_eq!(
11050 editor
11051 .selections
11052 .ranges::<usize>(&editor.display_snapshot(cx)),
11053 selection_ranges
11054 );
11055 }
11056
11057 assert(
11058 editor,
11059 cx,
11060 indoc! {"
11061 type «» =•
11062 "},
11063 );
11064
11065 assert!(editor.context_menu_visible(), "There should be a matches");
11066 });
11067}
11068
11069#[gpui::test]
11070async fn test_snippets(cx: &mut TestAppContext) {
11071 init_test(cx, |_| {});
11072
11073 let mut cx = EditorTestContext::new(cx).await;
11074
11075 cx.set_state(indoc! {"
11076 a.ˇ b
11077 a.ˇ b
11078 a.ˇ b
11079 "});
11080
11081 cx.update_editor(|editor, window, cx| {
11082 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11083 let insertion_ranges = editor
11084 .selections
11085 .all(&editor.display_snapshot(cx))
11086 .iter()
11087 .map(|s| s.range())
11088 .collect::<Vec<_>>();
11089 editor
11090 .insert_snippet(&insertion_ranges, snippet, window, cx)
11091 .unwrap();
11092 });
11093
11094 cx.assert_editor_state(indoc! {"
11095 a.f(«oneˇ», two, «threeˇ») b
11096 a.f(«oneˇ», two, «threeˇ») b
11097 a.f(«oneˇ», two, «threeˇ») b
11098 "});
11099
11100 // Can't move earlier than the first tab stop
11101 cx.update_editor(|editor, window, cx| {
11102 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11103 });
11104 cx.assert_editor_state(indoc! {"
11105 a.f(«oneˇ», two, «threeˇ») b
11106 a.f(«oneˇ», two, «threeˇ») b
11107 a.f(«oneˇ», two, «threeˇ») b
11108 "});
11109
11110 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11111 cx.assert_editor_state(indoc! {"
11112 a.f(one, «twoˇ», three) b
11113 a.f(one, «twoˇ», three) b
11114 a.f(one, «twoˇ», three) b
11115 "});
11116
11117 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11118 cx.assert_editor_state(indoc! {"
11119 a.f(«oneˇ», two, «threeˇ») b
11120 a.f(«oneˇ», two, «threeˇ») b
11121 a.f(«oneˇ», two, «threeˇ») b
11122 "});
11123
11124 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11125 cx.assert_editor_state(indoc! {"
11126 a.f(one, «twoˇ», three) b
11127 a.f(one, «twoˇ», three) b
11128 a.f(one, «twoˇ», three) b
11129 "});
11130 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11131 cx.assert_editor_state(indoc! {"
11132 a.f(one, two, three)ˇ b
11133 a.f(one, two, three)ˇ b
11134 a.f(one, two, three)ˇ b
11135 "});
11136
11137 // As soon as the last tab stop is reached, snippet state is gone
11138 cx.update_editor(|editor, window, cx| {
11139 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11140 });
11141 cx.assert_editor_state(indoc! {"
11142 a.f(one, two, three)ˇ b
11143 a.f(one, two, three)ˇ b
11144 a.f(one, two, three)ˇ b
11145 "});
11146}
11147
11148#[gpui::test]
11149async fn test_snippet_indentation(cx: &mut TestAppContext) {
11150 init_test(cx, |_| {});
11151
11152 let mut cx = EditorTestContext::new(cx).await;
11153
11154 cx.update_editor(|editor, window, cx| {
11155 let snippet = Snippet::parse(indoc! {"
11156 /*
11157 * Multiline comment with leading indentation
11158 *
11159 * $1
11160 */
11161 $0"})
11162 .unwrap();
11163 let insertion_ranges = editor
11164 .selections
11165 .all(&editor.display_snapshot(cx))
11166 .iter()
11167 .map(|s| s.range())
11168 .collect::<Vec<_>>();
11169 editor
11170 .insert_snippet(&insertion_ranges, snippet, window, cx)
11171 .unwrap();
11172 });
11173
11174 cx.assert_editor_state(indoc! {"
11175 /*
11176 * Multiline comment with leading indentation
11177 *
11178 * ˇ
11179 */
11180 "});
11181
11182 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11183 cx.assert_editor_state(indoc! {"
11184 /*
11185 * Multiline comment with leading indentation
11186 *
11187 *•
11188 */
11189 ˇ"});
11190}
11191
11192#[gpui::test]
11193async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11194 init_test(cx, |_| {});
11195
11196 let mut cx = EditorTestContext::new(cx).await;
11197 cx.update_editor(|editor, _, cx| {
11198 editor.project().unwrap().update(cx, |project, cx| {
11199 project.snippets().update(cx, |snippets, cx| {
11200 let snippet = project::snippet_provider::Snippet {
11201 prefix: vec!["multi word".to_string()],
11202 body: "this is many words".to_string(),
11203 description: Some("description".to_string()),
11204 name: "multi-word snippet test".to_string(),
11205 };
11206 snippets.add_snippet_for_test(
11207 None,
11208 PathBuf::from("test_snippets.json"),
11209 vec![Arc::new(snippet)],
11210 cx,
11211 );
11212 });
11213 })
11214 });
11215
11216 cx.set_state("ˇ");
11217 // cx.simulate_input("m");
11218 // cx.simulate_input("m ");
11219 // cx.simulate_input("m w");
11220 // cx.simulate_input("aa m w");
11221 cx.simulate_input("aa m g"); // fails correctly
11222
11223 cx.update_editor(|editor, _, _| {
11224 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11225 else {
11226 panic!("expected completion menu");
11227 };
11228 assert!(context_menu.visible());
11229 let completions = context_menu.completions.borrow();
11230
11231 assert!(
11232 completions
11233 .iter()
11234 .any(|c| c.new_text == "this is many words"),
11235 "Expected to find 'multi word' snippet in completions"
11236 );
11237 });
11238}
11239
11240#[gpui::test]
11241async fn test_document_format_during_save(cx: &mut TestAppContext) {
11242 init_test(cx, |_| {});
11243
11244 let fs = FakeFs::new(cx.executor());
11245 fs.insert_file(path!("/file.rs"), Default::default()).await;
11246
11247 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11248
11249 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11250 language_registry.add(rust_lang());
11251 let mut fake_servers = language_registry.register_fake_lsp(
11252 "Rust",
11253 FakeLspAdapter {
11254 capabilities: lsp::ServerCapabilities {
11255 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11256 ..Default::default()
11257 },
11258 ..Default::default()
11259 },
11260 );
11261
11262 let buffer = project
11263 .update(cx, |project, cx| {
11264 project.open_local_buffer(path!("/file.rs"), cx)
11265 })
11266 .await
11267 .unwrap();
11268
11269 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11270 let (editor, cx) = cx.add_window_view(|window, cx| {
11271 build_editor_with_project(project.clone(), buffer, window, cx)
11272 });
11273 editor.update_in(cx, |editor, window, cx| {
11274 editor.set_text("one\ntwo\nthree\n", window, cx)
11275 });
11276 assert!(cx.read(|cx| editor.is_dirty(cx)));
11277
11278 cx.executor().start_waiting();
11279 let fake_server = fake_servers.next().await.unwrap();
11280
11281 {
11282 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11283 move |params, _| async move {
11284 assert_eq!(
11285 params.text_document.uri,
11286 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11287 );
11288 assert_eq!(params.options.tab_size, 4);
11289 Ok(Some(vec![lsp::TextEdit::new(
11290 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11291 ", ".to_string(),
11292 )]))
11293 },
11294 );
11295 let save = editor
11296 .update_in(cx, |editor, window, cx| {
11297 editor.save(
11298 SaveOptions {
11299 format: true,
11300 autosave: false,
11301 },
11302 project.clone(),
11303 window,
11304 cx,
11305 )
11306 })
11307 .unwrap();
11308 cx.executor().start_waiting();
11309 save.await;
11310
11311 assert_eq!(
11312 editor.update(cx, |editor, cx| editor.text(cx)),
11313 "one, two\nthree\n"
11314 );
11315 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11316 }
11317
11318 {
11319 editor.update_in(cx, |editor, window, cx| {
11320 editor.set_text("one\ntwo\nthree\n", window, cx)
11321 });
11322 assert!(cx.read(|cx| editor.is_dirty(cx)));
11323
11324 // Ensure we can still save even if formatting hangs.
11325 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11326 move |params, _| async move {
11327 assert_eq!(
11328 params.text_document.uri,
11329 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11330 );
11331 futures::future::pending::<()>().await;
11332 unreachable!()
11333 },
11334 );
11335 let save = editor
11336 .update_in(cx, |editor, window, cx| {
11337 editor.save(
11338 SaveOptions {
11339 format: true,
11340 autosave: false,
11341 },
11342 project.clone(),
11343 window,
11344 cx,
11345 )
11346 })
11347 .unwrap();
11348 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11349 cx.executor().start_waiting();
11350 save.await;
11351 assert_eq!(
11352 editor.update(cx, |editor, cx| editor.text(cx)),
11353 "one\ntwo\nthree\n"
11354 );
11355 }
11356
11357 // Set rust language override and assert overridden tabsize is sent to language server
11358 update_test_language_settings(cx, |settings| {
11359 settings.languages.0.insert(
11360 "Rust".into(),
11361 LanguageSettingsContent {
11362 tab_size: NonZeroU32::new(8),
11363 ..Default::default()
11364 },
11365 );
11366 });
11367
11368 {
11369 editor.update_in(cx, |editor, window, cx| {
11370 editor.set_text("somehting_new\n", window, cx)
11371 });
11372 assert!(cx.read(|cx| editor.is_dirty(cx)));
11373 let _formatting_request_signal = fake_server
11374 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11375 assert_eq!(
11376 params.text_document.uri,
11377 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11378 );
11379 assert_eq!(params.options.tab_size, 8);
11380 Ok(Some(vec![]))
11381 });
11382 let save = editor
11383 .update_in(cx, |editor, window, cx| {
11384 editor.save(
11385 SaveOptions {
11386 format: true,
11387 autosave: false,
11388 },
11389 project.clone(),
11390 window,
11391 cx,
11392 )
11393 })
11394 .unwrap();
11395 cx.executor().start_waiting();
11396 save.await;
11397 }
11398}
11399
11400#[gpui::test]
11401async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11402 init_test(cx, |settings| {
11403 settings.defaults.ensure_final_newline_on_save = Some(false);
11404 });
11405
11406 let fs = FakeFs::new(cx.executor());
11407 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11408
11409 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11410
11411 let buffer = project
11412 .update(cx, |project, cx| {
11413 project.open_local_buffer(path!("/file.txt"), cx)
11414 })
11415 .await
11416 .unwrap();
11417
11418 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11419 let (editor, cx) = cx.add_window_view(|window, cx| {
11420 build_editor_with_project(project.clone(), buffer, window, cx)
11421 });
11422 editor.update_in(cx, |editor, window, cx| {
11423 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11424 s.select_ranges([0..0])
11425 });
11426 });
11427 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11428
11429 editor.update_in(cx, |editor, window, cx| {
11430 editor.handle_input("\n", window, cx)
11431 });
11432 cx.run_until_parked();
11433 save(&editor, &project, cx).await;
11434 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11435
11436 editor.update_in(cx, |editor, window, cx| {
11437 editor.undo(&Default::default(), window, cx);
11438 });
11439 save(&editor, &project, cx).await;
11440 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11441
11442 editor.update_in(cx, |editor, window, cx| {
11443 editor.redo(&Default::default(), window, cx);
11444 });
11445 cx.run_until_parked();
11446 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11447
11448 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11449 let save = editor
11450 .update_in(cx, |editor, window, cx| {
11451 editor.save(
11452 SaveOptions {
11453 format: true,
11454 autosave: false,
11455 },
11456 project.clone(),
11457 window,
11458 cx,
11459 )
11460 })
11461 .unwrap();
11462 cx.executor().start_waiting();
11463 save.await;
11464 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11465 }
11466}
11467
11468#[gpui::test]
11469async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11470 init_test(cx, |_| {});
11471
11472 let cols = 4;
11473 let rows = 10;
11474 let sample_text_1 = sample_text(rows, cols, 'a');
11475 assert_eq!(
11476 sample_text_1,
11477 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11478 );
11479 let sample_text_2 = sample_text(rows, cols, 'l');
11480 assert_eq!(
11481 sample_text_2,
11482 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11483 );
11484 let sample_text_3 = sample_text(rows, cols, 'v');
11485 assert_eq!(
11486 sample_text_3,
11487 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11488 );
11489
11490 let fs = FakeFs::new(cx.executor());
11491 fs.insert_tree(
11492 path!("/a"),
11493 json!({
11494 "main.rs": sample_text_1,
11495 "other.rs": sample_text_2,
11496 "lib.rs": sample_text_3,
11497 }),
11498 )
11499 .await;
11500
11501 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11502 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11503 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11504
11505 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11506 language_registry.add(rust_lang());
11507 let mut fake_servers = language_registry.register_fake_lsp(
11508 "Rust",
11509 FakeLspAdapter {
11510 capabilities: lsp::ServerCapabilities {
11511 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11512 ..Default::default()
11513 },
11514 ..Default::default()
11515 },
11516 );
11517
11518 let worktree = project.update(cx, |project, cx| {
11519 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11520 assert_eq!(worktrees.len(), 1);
11521 worktrees.pop().unwrap()
11522 });
11523 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11524
11525 let buffer_1 = project
11526 .update(cx, |project, cx| {
11527 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11528 })
11529 .await
11530 .unwrap();
11531 let buffer_2 = project
11532 .update(cx, |project, cx| {
11533 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11534 })
11535 .await
11536 .unwrap();
11537 let buffer_3 = project
11538 .update(cx, |project, cx| {
11539 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11540 })
11541 .await
11542 .unwrap();
11543
11544 let multi_buffer = cx.new(|cx| {
11545 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11546 multi_buffer.push_excerpts(
11547 buffer_1.clone(),
11548 [
11549 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11550 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11551 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11552 ],
11553 cx,
11554 );
11555 multi_buffer.push_excerpts(
11556 buffer_2.clone(),
11557 [
11558 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11559 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11560 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11561 ],
11562 cx,
11563 );
11564 multi_buffer.push_excerpts(
11565 buffer_3.clone(),
11566 [
11567 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11568 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11569 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11570 ],
11571 cx,
11572 );
11573 multi_buffer
11574 });
11575 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11576 Editor::new(
11577 EditorMode::full(),
11578 multi_buffer,
11579 Some(project.clone()),
11580 window,
11581 cx,
11582 )
11583 });
11584
11585 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11586 editor.change_selections(
11587 SelectionEffects::scroll(Autoscroll::Next),
11588 window,
11589 cx,
11590 |s| s.select_ranges(Some(1..2)),
11591 );
11592 editor.insert("|one|two|three|", window, cx);
11593 });
11594 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11595 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11596 editor.change_selections(
11597 SelectionEffects::scroll(Autoscroll::Next),
11598 window,
11599 cx,
11600 |s| s.select_ranges(Some(60..70)),
11601 );
11602 editor.insert("|four|five|six|", window, cx);
11603 });
11604 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11605
11606 // First two buffers should be edited, but not the third one.
11607 assert_eq!(
11608 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11609 "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}",
11610 );
11611 buffer_1.update(cx, |buffer, _| {
11612 assert!(buffer.is_dirty());
11613 assert_eq!(
11614 buffer.text(),
11615 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11616 )
11617 });
11618 buffer_2.update(cx, |buffer, _| {
11619 assert!(buffer.is_dirty());
11620 assert_eq!(
11621 buffer.text(),
11622 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11623 )
11624 });
11625 buffer_3.update(cx, |buffer, _| {
11626 assert!(!buffer.is_dirty());
11627 assert_eq!(buffer.text(), sample_text_3,)
11628 });
11629 cx.executor().run_until_parked();
11630
11631 cx.executor().start_waiting();
11632 let save = multi_buffer_editor
11633 .update_in(cx, |editor, window, cx| {
11634 editor.save(
11635 SaveOptions {
11636 format: true,
11637 autosave: false,
11638 },
11639 project.clone(),
11640 window,
11641 cx,
11642 )
11643 })
11644 .unwrap();
11645
11646 let fake_server = fake_servers.next().await.unwrap();
11647 fake_server
11648 .server
11649 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11650 Ok(Some(vec![lsp::TextEdit::new(
11651 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11652 format!("[{} formatted]", params.text_document.uri),
11653 )]))
11654 })
11655 .detach();
11656 save.await;
11657
11658 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11659 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11660 assert_eq!(
11661 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11662 uri!(
11663 "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}"
11664 ),
11665 );
11666 buffer_1.update(cx, |buffer, _| {
11667 assert!(!buffer.is_dirty());
11668 assert_eq!(
11669 buffer.text(),
11670 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11671 )
11672 });
11673 buffer_2.update(cx, |buffer, _| {
11674 assert!(!buffer.is_dirty());
11675 assert_eq!(
11676 buffer.text(),
11677 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11678 )
11679 });
11680 buffer_3.update(cx, |buffer, _| {
11681 assert!(!buffer.is_dirty());
11682 assert_eq!(buffer.text(), sample_text_3,)
11683 });
11684}
11685
11686#[gpui::test]
11687async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11688 init_test(cx, |_| {});
11689
11690 let fs = FakeFs::new(cx.executor());
11691 fs.insert_tree(
11692 path!("/dir"),
11693 json!({
11694 "file1.rs": "fn main() { println!(\"hello\"); }",
11695 "file2.rs": "fn test() { println!(\"test\"); }",
11696 "file3.rs": "fn other() { println!(\"other\"); }\n",
11697 }),
11698 )
11699 .await;
11700
11701 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11702 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11703 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11704
11705 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11706 language_registry.add(rust_lang());
11707
11708 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11709 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11710
11711 // Open three buffers
11712 let buffer_1 = project
11713 .update(cx, |project, cx| {
11714 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11715 })
11716 .await
11717 .unwrap();
11718 let buffer_2 = project
11719 .update(cx, |project, cx| {
11720 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11721 })
11722 .await
11723 .unwrap();
11724 let buffer_3 = project
11725 .update(cx, |project, cx| {
11726 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11727 })
11728 .await
11729 .unwrap();
11730
11731 // Create a multi-buffer with all three buffers
11732 let multi_buffer = cx.new(|cx| {
11733 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11734 multi_buffer.push_excerpts(
11735 buffer_1.clone(),
11736 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11737 cx,
11738 );
11739 multi_buffer.push_excerpts(
11740 buffer_2.clone(),
11741 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11742 cx,
11743 );
11744 multi_buffer.push_excerpts(
11745 buffer_3.clone(),
11746 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11747 cx,
11748 );
11749 multi_buffer
11750 });
11751
11752 let editor = cx.new_window_entity(|window, cx| {
11753 Editor::new(
11754 EditorMode::full(),
11755 multi_buffer,
11756 Some(project.clone()),
11757 window,
11758 cx,
11759 )
11760 });
11761
11762 // Edit only the first buffer
11763 editor.update_in(cx, |editor, window, cx| {
11764 editor.change_selections(
11765 SelectionEffects::scroll(Autoscroll::Next),
11766 window,
11767 cx,
11768 |s| s.select_ranges(Some(10..10)),
11769 );
11770 editor.insert("// edited", window, cx);
11771 });
11772
11773 // Verify that only buffer 1 is dirty
11774 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11775 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11776 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11777
11778 // Get write counts after file creation (files were created with initial content)
11779 // We expect each file to have been written once during creation
11780 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11781 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11782 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11783
11784 // Perform autosave
11785 let save_task = editor.update_in(cx, |editor, window, cx| {
11786 editor.save(
11787 SaveOptions {
11788 format: true,
11789 autosave: true,
11790 },
11791 project.clone(),
11792 window,
11793 cx,
11794 )
11795 });
11796 save_task.await.unwrap();
11797
11798 // Only the dirty buffer should have been saved
11799 assert_eq!(
11800 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11801 1,
11802 "Buffer 1 was dirty, so it should have been written once during autosave"
11803 );
11804 assert_eq!(
11805 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11806 0,
11807 "Buffer 2 was clean, so it should not have been written during autosave"
11808 );
11809 assert_eq!(
11810 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11811 0,
11812 "Buffer 3 was clean, so it should not have been written during autosave"
11813 );
11814
11815 // Verify buffer states after autosave
11816 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11817 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11818 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11819
11820 // Now perform a manual save (format = true)
11821 let save_task = editor.update_in(cx, |editor, window, cx| {
11822 editor.save(
11823 SaveOptions {
11824 format: true,
11825 autosave: false,
11826 },
11827 project.clone(),
11828 window,
11829 cx,
11830 )
11831 });
11832 save_task.await.unwrap();
11833
11834 // During manual save, clean buffers don't get written to disk
11835 // They just get did_save called for language server notifications
11836 assert_eq!(
11837 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11838 1,
11839 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11840 );
11841 assert_eq!(
11842 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11843 0,
11844 "Buffer 2 should not have been written at all"
11845 );
11846 assert_eq!(
11847 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11848 0,
11849 "Buffer 3 should not have been written at all"
11850 );
11851}
11852
11853async fn setup_range_format_test(
11854 cx: &mut TestAppContext,
11855) -> (
11856 Entity<Project>,
11857 Entity<Editor>,
11858 &mut gpui::VisualTestContext,
11859 lsp::FakeLanguageServer,
11860) {
11861 init_test(cx, |_| {});
11862
11863 let fs = FakeFs::new(cx.executor());
11864 fs.insert_file(path!("/file.rs"), Default::default()).await;
11865
11866 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11867
11868 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11869 language_registry.add(rust_lang());
11870 let mut fake_servers = language_registry.register_fake_lsp(
11871 "Rust",
11872 FakeLspAdapter {
11873 capabilities: lsp::ServerCapabilities {
11874 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11875 ..lsp::ServerCapabilities::default()
11876 },
11877 ..FakeLspAdapter::default()
11878 },
11879 );
11880
11881 let buffer = project
11882 .update(cx, |project, cx| {
11883 project.open_local_buffer(path!("/file.rs"), cx)
11884 })
11885 .await
11886 .unwrap();
11887
11888 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11889 let (editor, cx) = cx.add_window_view(|window, cx| {
11890 build_editor_with_project(project.clone(), buffer, window, cx)
11891 });
11892
11893 cx.executor().start_waiting();
11894 let fake_server = fake_servers.next().await.unwrap();
11895
11896 (project, editor, cx, fake_server)
11897}
11898
11899#[gpui::test]
11900async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11901 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11902
11903 editor.update_in(cx, |editor, window, cx| {
11904 editor.set_text("one\ntwo\nthree\n", window, cx)
11905 });
11906 assert!(cx.read(|cx| editor.is_dirty(cx)));
11907
11908 let save = editor
11909 .update_in(cx, |editor, window, cx| {
11910 editor.save(
11911 SaveOptions {
11912 format: true,
11913 autosave: false,
11914 },
11915 project.clone(),
11916 window,
11917 cx,
11918 )
11919 })
11920 .unwrap();
11921 fake_server
11922 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11923 assert_eq!(
11924 params.text_document.uri,
11925 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11926 );
11927 assert_eq!(params.options.tab_size, 4);
11928 Ok(Some(vec![lsp::TextEdit::new(
11929 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11930 ", ".to_string(),
11931 )]))
11932 })
11933 .next()
11934 .await;
11935 cx.executor().start_waiting();
11936 save.await;
11937 assert_eq!(
11938 editor.update(cx, |editor, cx| editor.text(cx)),
11939 "one, two\nthree\n"
11940 );
11941 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11942}
11943
11944#[gpui::test]
11945async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11946 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11947
11948 editor.update_in(cx, |editor, window, cx| {
11949 editor.set_text("one\ntwo\nthree\n", window, cx)
11950 });
11951 assert!(cx.read(|cx| editor.is_dirty(cx)));
11952
11953 // Test that save still works when formatting hangs
11954 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11955 move |params, _| async move {
11956 assert_eq!(
11957 params.text_document.uri,
11958 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11959 );
11960 futures::future::pending::<()>().await;
11961 unreachable!()
11962 },
11963 );
11964 let save = editor
11965 .update_in(cx, |editor, window, cx| {
11966 editor.save(
11967 SaveOptions {
11968 format: true,
11969 autosave: false,
11970 },
11971 project.clone(),
11972 window,
11973 cx,
11974 )
11975 })
11976 .unwrap();
11977 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11978 cx.executor().start_waiting();
11979 save.await;
11980 assert_eq!(
11981 editor.update(cx, |editor, cx| editor.text(cx)),
11982 "one\ntwo\nthree\n"
11983 );
11984 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11985}
11986
11987#[gpui::test]
11988async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11989 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11990
11991 // Buffer starts clean, no formatting should be requested
11992 let save = editor
11993 .update_in(cx, |editor, window, cx| {
11994 editor.save(
11995 SaveOptions {
11996 format: false,
11997 autosave: false,
11998 },
11999 project.clone(),
12000 window,
12001 cx,
12002 )
12003 })
12004 .unwrap();
12005 let _pending_format_request = fake_server
12006 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12007 panic!("Should not be invoked");
12008 })
12009 .next();
12010 cx.executor().start_waiting();
12011 save.await;
12012 cx.run_until_parked();
12013}
12014
12015#[gpui::test]
12016async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12017 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12018
12019 // Set Rust language override and assert overridden tabsize is sent to language server
12020 update_test_language_settings(cx, |settings| {
12021 settings.languages.0.insert(
12022 "Rust".into(),
12023 LanguageSettingsContent {
12024 tab_size: NonZeroU32::new(8),
12025 ..Default::default()
12026 },
12027 );
12028 });
12029
12030 editor.update_in(cx, |editor, window, cx| {
12031 editor.set_text("something_new\n", window, cx)
12032 });
12033 assert!(cx.read(|cx| editor.is_dirty(cx)));
12034 let save = editor
12035 .update_in(cx, |editor, window, cx| {
12036 editor.save(
12037 SaveOptions {
12038 format: true,
12039 autosave: false,
12040 },
12041 project.clone(),
12042 window,
12043 cx,
12044 )
12045 })
12046 .unwrap();
12047 fake_server
12048 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12049 assert_eq!(
12050 params.text_document.uri,
12051 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12052 );
12053 assert_eq!(params.options.tab_size, 8);
12054 Ok(Some(Vec::new()))
12055 })
12056 .next()
12057 .await;
12058 save.await;
12059}
12060
12061#[gpui::test]
12062async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12063 init_test(cx, |settings| {
12064 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12065 settings::LanguageServerFormatterSpecifier::Current,
12066 )))
12067 });
12068
12069 let fs = FakeFs::new(cx.executor());
12070 fs.insert_file(path!("/file.rs"), Default::default()).await;
12071
12072 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12073
12074 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12075 language_registry.add(Arc::new(Language::new(
12076 LanguageConfig {
12077 name: "Rust".into(),
12078 matcher: LanguageMatcher {
12079 path_suffixes: vec!["rs".to_string()],
12080 ..Default::default()
12081 },
12082 ..LanguageConfig::default()
12083 },
12084 Some(tree_sitter_rust::LANGUAGE.into()),
12085 )));
12086 update_test_language_settings(cx, |settings| {
12087 // Enable Prettier formatting for the same buffer, and ensure
12088 // LSP is called instead of Prettier.
12089 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12090 });
12091 let mut fake_servers = language_registry.register_fake_lsp(
12092 "Rust",
12093 FakeLspAdapter {
12094 capabilities: lsp::ServerCapabilities {
12095 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12096 ..Default::default()
12097 },
12098 ..Default::default()
12099 },
12100 );
12101
12102 let buffer = project
12103 .update(cx, |project, cx| {
12104 project.open_local_buffer(path!("/file.rs"), cx)
12105 })
12106 .await
12107 .unwrap();
12108
12109 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12110 let (editor, cx) = cx.add_window_view(|window, cx| {
12111 build_editor_with_project(project.clone(), buffer, window, cx)
12112 });
12113 editor.update_in(cx, |editor, window, cx| {
12114 editor.set_text("one\ntwo\nthree\n", window, cx)
12115 });
12116
12117 cx.executor().start_waiting();
12118 let fake_server = fake_servers.next().await.unwrap();
12119
12120 let format = editor
12121 .update_in(cx, |editor, window, cx| {
12122 editor.perform_format(
12123 project.clone(),
12124 FormatTrigger::Manual,
12125 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12126 window,
12127 cx,
12128 )
12129 })
12130 .unwrap();
12131 fake_server
12132 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12133 assert_eq!(
12134 params.text_document.uri,
12135 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12136 );
12137 assert_eq!(params.options.tab_size, 4);
12138 Ok(Some(vec![lsp::TextEdit::new(
12139 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12140 ", ".to_string(),
12141 )]))
12142 })
12143 .next()
12144 .await;
12145 cx.executor().start_waiting();
12146 format.await;
12147 assert_eq!(
12148 editor.update(cx, |editor, cx| editor.text(cx)),
12149 "one, two\nthree\n"
12150 );
12151
12152 editor.update_in(cx, |editor, window, cx| {
12153 editor.set_text("one\ntwo\nthree\n", window, cx)
12154 });
12155 // Ensure we don't lock if formatting hangs.
12156 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12157 move |params, _| async move {
12158 assert_eq!(
12159 params.text_document.uri,
12160 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12161 );
12162 futures::future::pending::<()>().await;
12163 unreachable!()
12164 },
12165 );
12166 let format = editor
12167 .update_in(cx, |editor, window, cx| {
12168 editor.perform_format(
12169 project,
12170 FormatTrigger::Manual,
12171 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12172 window,
12173 cx,
12174 )
12175 })
12176 .unwrap();
12177 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12178 cx.executor().start_waiting();
12179 format.await;
12180 assert_eq!(
12181 editor.update(cx, |editor, cx| editor.text(cx)),
12182 "one\ntwo\nthree\n"
12183 );
12184}
12185
12186#[gpui::test]
12187async fn test_multiple_formatters(cx: &mut TestAppContext) {
12188 init_test(cx, |settings| {
12189 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12190 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12191 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12192 Formatter::CodeAction("code-action-1".into()),
12193 Formatter::CodeAction("code-action-2".into()),
12194 ]))
12195 });
12196
12197 let fs = FakeFs::new(cx.executor());
12198 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12199 .await;
12200
12201 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12202 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12203 language_registry.add(rust_lang());
12204
12205 let mut fake_servers = language_registry.register_fake_lsp(
12206 "Rust",
12207 FakeLspAdapter {
12208 capabilities: lsp::ServerCapabilities {
12209 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12210 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12211 commands: vec!["the-command-for-code-action-1".into()],
12212 ..Default::default()
12213 }),
12214 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12215 ..Default::default()
12216 },
12217 ..Default::default()
12218 },
12219 );
12220
12221 let buffer = project
12222 .update(cx, |project, cx| {
12223 project.open_local_buffer(path!("/file.rs"), cx)
12224 })
12225 .await
12226 .unwrap();
12227
12228 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12229 let (editor, cx) = cx.add_window_view(|window, cx| {
12230 build_editor_with_project(project.clone(), buffer, window, cx)
12231 });
12232
12233 cx.executor().start_waiting();
12234
12235 let fake_server = fake_servers.next().await.unwrap();
12236 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12237 move |_params, _| async move {
12238 Ok(Some(vec![lsp::TextEdit::new(
12239 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12240 "applied-formatting\n".to_string(),
12241 )]))
12242 },
12243 );
12244 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12245 move |params, _| async move {
12246 let requested_code_actions = params.context.only.expect("Expected code action request");
12247 assert_eq!(requested_code_actions.len(), 1);
12248
12249 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12250 let code_action = match requested_code_actions[0].as_str() {
12251 "code-action-1" => lsp::CodeAction {
12252 kind: Some("code-action-1".into()),
12253 edit: Some(lsp::WorkspaceEdit::new(
12254 [(
12255 uri,
12256 vec![lsp::TextEdit::new(
12257 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12258 "applied-code-action-1-edit\n".to_string(),
12259 )],
12260 )]
12261 .into_iter()
12262 .collect(),
12263 )),
12264 command: Some(lsp::Command {
12265 command: "the-command-for-code-action-1".into(),
12266 ..Default::default()
12267 }),
12268 ..Default::default()
12269 },
12270 "code-action-2" => lsp::CodeAction {
12271 kind: Some("code-action-2".into()),
12272 edit: Some(lsp::WorkspaceEdit::new(
12273 [(
12274 uri,
12275 vec![lsp::TextEdit::new(
12276 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12277 "applied-code-action-2-edit\n".to_string(),
12278 )],
12279 )]
12280 .into_iter()
12281 .collect(),
12282 )),
12283 ..Default::default()
12284 },
12285 req => panic!("Unexpected code action request: {:?}", req),
12286 };
12287 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12288 code_action,
12289 )]))
12290 },
12291 );
12292
12293 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12294 move |params, _| async move { Ok(params) }
12295 });
12296
12297 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12298 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12299 let fake = fake_server.clone();
12300 let lock = command_lock.clone();
12301 move |params, _| {
12302 assert_eq!(params.command, "the-command-for-code-action-1");
12303 let fake = fake.clone();
12304 let lock = lock.clone();
12305 async move {
12306 lock.lock().await;
12307 fake.server
12308 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12309 label: None,
12310 edit: lsp::WorkspaceEdit {
12311 changes: Some(
12312 [(
12313 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12314 vec![lsp::TextEdit {
12315 range: lsp::Range::new(
12316 lsp::Position::new(0, 0),
12317 lsp::Position::new(0, 0),
12318 ),
12319 new_text: "applied-code-action-1-command\n".into(),
12320 }],
12321 )]
12322 .into_iter()
12323 .collect(),
12324 ),
12325 ..Default::default()
12326 },
12327 })
12328 .await
12329 .into_response()
12330 .unwrap();
12331 Ok(Some(json!(null)))
12332 }
12333 }
12334 });
12335
12336 cx.executor().start_waiting();
12337 editor
12338 .update_in(cx, |editor, window, cx| {
12339 editor.perform_format(
12340 project.clone(),
12341 FormatTrigger::Manual,
12342 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12343 window,
12344 cx,
12345 )
12346 })
12347 .unwrap()
12348 .await;
12349 editor.update(cx, |editor, cx| {
12350 assert_eq!(
12351 editor.text(cx),
12352 r#"
12353 applied-code-action-2-edit
12354 applied-code-action-1-command
12355 applied-code-action-1-edit
12356 applied-formatting
12357 one
12358 two
12359 three
12360 "#
12361 .unindent()
12362 );
12363 });
12364
12365 editor.update_in(cx, |editor, window, cx| {
12366 editor.undo(&Default::default(), window, cx);
12367 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12368 });
12369
12370 // Perform a manual edit while waiting for an LSP command
12371 // that's being run as part of a formatting code action.
12372 let lock_guard = command_lock.lock().await;
12373 let format = editor
12374 .update_in(cx, |editor, window, cx| {
12375 editor.perform_format(
12376 project.clone(),
12377 FormatTrigger::Manual,
12378 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12379 window,
12380 cx,
12381 )
12382 })
12383 .unwrap();
12384 cx.run_until_parked();
12385 editor.update(cx, |editor, cx| {
12386 assert_eq!(
12387 editor.text(cx),
12388 r#"
12389 applied-code-action-1-edit
12390 applied-formatting
12391 one
12392 two
12393 three
12394 "#
12395 .unindent()
12396 );
12397
12398 editor.buffer.update(cx, |buffer, cx| {
12399 let ix = buffer.len(cx);
12400 buffer.edit([(ix..ix, "edited\n")], None, cx);
12401 });
12402 });
12403
12404 // Allow the LSP command to proceed. Because the buffer was edited,
12405 // the second code action will not be run.
12406 drop(lock_guard);
12407 format.await;
12408 editor.update_in(cx, |editor, window, cx| {
12409 assert_eq!(
12410 editor.text(cx),
12411 r#"
12412 applied-code-action-1-command
12413 applied-code-action-1-edit
12414 applied-formatting
12415 one
12416 two
12417 three
12418 edited
12419 "#
12420 .unindent()
12421 );
12422
12423 // The manual edit is undone first, because it is the last thing the user did
12424 // (even though the command completed afterwards).
12425 editor.undo(&Default::default(), window, cx);
12426 assert_eq!(
12427 editor.text(cx),
12428 r#"
12429 applied-code-action-1-command
12430 applied-code-action-1-edit
12431 applied-formatting
12432 one
12433 two
12434 three
12435 "#
12436 .unindent()
12437 );
12438
12439 // All the formatting (including the command, which completed after the manual edit)
12440 // is undone together.
12441 editor.undo(&Default::default(), window, cx);
12442 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12443 });
12444}
12445
12446#[gpui::test]
12447async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12448 init_test(cx, |settings| {
12449 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12450 settings::LanguageServerFormatterSpecifier::Current,
12451 )]))
12452 });
12453
12454 let fs = FakeFs::new(cx.executor());
12455 fs.insert_file(path!("/file.ts"), Default::default()).await;
12456
12457 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12458
12459 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12460 language_registry.add(Arc::new(Language::new(
12461 LanguageConfig {
12462 name: "TypeScript".into(),
12463 matcher: LanguageMatcher {
12464 path_suffixes: vec!["ts".to_string()],
12465 ..Default::default()
12466 },
12467 ..LanguageConfig::default()
12468 },
12469 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12470 )));
12471 update_test_language_settings(cx, |settings| {
12472 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12473 });
12474 let mut fake_servers = language_registry.register_fake_lsp(
12475 "TypeScript",
12476 FakeLspAdapter {
12477 capabilities: lsp::ServerCapabilities {
12478 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12479 ..Default::default()
12480 },
12481 ..Default::default()
12482 },
12483 );
12484
12485 let buffer = project
12486 .update(cx, |project, cx| {
12487 project.open_local_buffer(path!("/file.ts"), cx)
12488 })
12489 .await
12490 .unwrap();
12491
12492 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12493 let (editor, cx) = cx.add_window_view(|window, cx| {
12494 build_editor_with_project(project.clone(), buffer, window, cx)
12495 });
12496 editor.update_in(cx, |editor, window, cx| {
12497 editor.set_text(
12498 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12499 window,
12500 cx,
12501 )
12502 });
12503
12504 cx.executor().start_waiting();
12505 let fake_server = fake_servers.next().await.unwrap();
12506
12507 let format = editor
12508 .update_in(cx, |editor, window, cx| {
12509 editor.perform_code_action_kind(
12510 project.clone(),
12511 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12512 window,
12513 cx,
12514 )
12515 })
12516 .unwrap();
12517 fake_server
12518 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12519 assert_eq!(
12520 params.text_document.uri,
12521 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12522 );
12523 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12524 lsp::CodeAction {
12525 title: "Organize Imports".to_string(),
12526 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12527 edit: Some(lsp::WorkspaceEdit {
12528 changes: Some(
12529 [(
12530 params.text_document.uri.clone(),
12531 vec![lsp::TextEdit::new(
12532 lsp::Range::new(
12533 lsp::Position::new(1, 0),
12534 lsp::Position::new(2, 0),
12535 ),
12536 "".to_string(),
12537 )],
12538 )]
12539 .into_iter()
12540 .collect(),
12541 ),
12542 ..Default::default()
12543 }),
12544 ..Default::default()
12545 },
12546 )]))
12547 })
12548 .next()
12549 .await;
12550 cx.executor().start_waiting();
12551 format.await;
12552 assert_eq!(
12553 editor.update(cx, |editor, cx| editor.text(cx)),
12554 "import { a } from 'module';\n\nconst x = a;\n"
12555 );
12556
12557 editor.update_in(cx, |editor, window, cx| {
12558 editor.set_text(
12559 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12560 window,
12561 cx,
12562 )
12563 });
12564 // Ensure we don't lock if code action hangs.
12565 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12566 move |params, _| async move {
12567 assert_eq!(
12568 params.text_document.uri,
12569 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12570 );
12571 futures::future::pending::<()>().await;
12572 unreachable!()
12573 },
12574 );
12575 let format = editor
12576 .update_in(cx, |editor, window, cx| {
12577 editor.perform_code_action_kind(
12578 project,
12579 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12580 window,
12581 cx,
12582 )
12583 })
12584 .unwrap();
12585 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12586 cx.executor().start_waiting();
12587 format.await;
12588 assert_eq!(
12589 editor.update(cx, |editor, cx| editor.text(cx)),
12590 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12591 );
12592}
12593
12594#[gpui::test]
12595async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12596 init_test(cx, |_| {});
12597
12598 let mut cx = EditorLspTestContext::new_rust(
12599 lsp::ServerCapabilities {
12600 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12601 ..Default::default()
12602 },
12603 cx,
12604 )
12605 .await;
12606
12607 cx.set_state(indoc! {"
12608 one.twoˇ
12609 "});
12610
12611 // The format request takes a long time. When it completes, it inserts
12612 // a newline and an indent before the `.`
12613 cx.lsp
12614 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12615 let executor = cx.background_executor().clone();
12616 async move {
12617 executor.timer(Duration::from_millis(100)).await;
12618 Ok(Some(vec![lsp::TextEdit {
12619 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12620 new_text: "\n ".into(),
12621 }]))
12622 }
12623 });
12624
12625 // Submit a format request.
12626 let format_1 = cx
12627 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12628 .unwrap();
12629 cx.executor().run_until_parked();
12630
12631 // Submit a second format request.
12632 let format_2 = cx
12633 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12634 .unwrap();
12635 cx.executor().run_until_parked();
12636
12637 // Wait for both format requests to complete
12638 cx.executor().advance_clock(Duration::from_millis(200));
12639 cx.executor().start_waiting();
12640 format_1.await.unwrap();
12641 cx.executor().start_waiting();
12642 format_2.await.unwrap();
12643
12644 // The formatting edits only happens once.
12645 cx.assert_editor_state(indoc! {"
12646 one
12647 .twoˇ
12648 "});
12649}
12650
12651#[gpui::test]
12652async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12653 init_test(cx, |settings| {
12654 settings.defaults.formatter = Some(FormatterList::default())
12655 });
12656
12657 let mut cx = EditorLspTestContext::new_rust(
12658 lsp::ServerCapabilities {
12659 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12660 ..Default::default()
12661 },
12662 cx,
12663 )
12664 .await;
12665
12666 // Record which buffer changes have been sent to the language server
12667 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12668 cx.lsp
12669 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12670 let buffer_changes = buffer_changes.clone();
12671 move |params, _| {
12672 buffer_changes.lock().extend(
12673 params
12674 .content_changes
12675 .into_iter()
12676 .map(|e| (e.range.unwrap(), e.text)),
12677 );
12678 }
12679 });
12680 // Handle formatting requests to the language server.
12681 cx.lsp
12682 .set_request_handler::<lsp::request::Formatting, _, _>({
12683 let buffer_changes = buffer_changes.clone();
12684 move |_, _| {
12685 let buffer_changes = buffer_changes.clone();
12686 // Insert blank lines between each line of the buffer.
12687 async move {
12688 // When formatting is requested, trailing whitespace has already been stripped,
12689 // and the trailing newline has already been added.
12690 assert_eq!(
12691 &buffer_changes.lock()[1..],
12692 &[
12693 (
12694 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12695 "".into()
12696 ),
12697 (
12698 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12699 "".into()
12700 ),
12701 (
12702 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12703 "\n".into()
12704 ),
12705 ]
12706 );
12707
12708 Ok(Some(vec![
12709 lsp::TextEdit {
12710 range: lsp::Range::new(
12711 lsp::Position::new(1, 0),
12712 lsp::Position::new(1, 0),
12713 ),
12714 new_text: "\n".into(),
12715 },
12716 lsp::TextEdit {
12717 range: lsp::Range::new(
12718 lsp::Position::new(2, 0),
12719 lsp::Position::new(2, 0),
12720 ),
12721 new_text: "\n".into(),
12722 },
12723 ]))
12724 }
12725 }
12726 });
12727
12728 // Set up a buffer white some trailing whitespace and no trailing newline.
12729 cx.set_state(
12730 &[
12731 "one ", //
12732 "twoˇ", //
12733 "three ", //
12734 "four", //
12735 ]
12736 .join("\n"),
12737 );
12738 cx.run_until_parked();
12739
12740 // Submit a format request.
12741 let format = cx
12742 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12743 .unwrap();
12744
12745 cx.run_until_parked();
12746 // After formatting the buffer, the trailing whitespace is stripped,
12747 // a newline is appended, and the edits provided by the language server
12748 // have been applied.
12749 format.await.unwrap();
12750
12751 cx.assert_editor_state(
12752 &[
12753 "one", //
12754 "", //
12755 "twoˇ", //
12756 "", //
12757 "three", //
12758 "four", //
12759 "", //
12760 ]
12761 .join("\n"),
12762 );
12763
12764 // Undoing the formatting undoes the trailing whitespace removal, the
12765 // trailing newline, and the LSP edits.
12766 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12767 cx.assert_editor_state(
12768 &[
12769 "one ", //
12770 "twoˇ", //
12771 "three ", //
12772 "four", //
12773 ]
12774 .join("\n"),
12775 );
12776}
12777
12778#[gpui::test]
12779async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12780 cx: &mut TestAppContext,
12781) {
12782 init_test(cx, |_| {});
12783
12784 cx.update(|cx| {
12785 cx.update_global::<SettingsStore, _>(|settings, cx| {
12786 settings.update_user_settings(cx, |settings| {
12787 settings.editor.auto_signature_help = Some(true);
12788 });
12789 });
12790 });
12791
12792 let mut cx = EditorLspTestContext::new_rust(
12793 lsp::ServerCapabilities {
12794 signature_help_provider: Some(lsp::SignatureHelpOptions {
12795 ..Default::default()
12796 }),
12797 ..Default::default()
12798 },
12799 cx,
12800 )
12801 .await;
12802
12803 let language = Language::new(
12804 LanguageConfig {
12805 name: "Rust".into(),
12806 brackets: BracketPairConfig {
12807 pairs: vec![
12808 BracketPair {
12809 start: "{".to_string(),
12810 end: "}".to_string(),
12811 close: true,
12812 surround: true,
12813 newline: true,
12814 },
12815 BracketPair {
12816 start: "(".to_string(),
12817 end: ")".to_string(),
12818 close: true,
12819 surround: true,
12820 newline: true,
12821 },
12822 BracketPair {
12823 start: "/*".to_string(),
12824 end: " */".to_string(),
12825 close: true,
12826 surround: true,
12827 newline: true,
12828 },
12829 BracketPair {
12830 start: "[".to_string(),
12831 end: "]".to_string(),
12832 close: false,
12833 surround: false,
12834 newline: true,
12835 },
12836 BracketPair {
12837 start: "\"".to_string(),
12838 end: "\"".to_string(),
12839 close: true,
12840 surround: true,
12841 newline: false,
12842 },
12843 BracketPair {
12844 start: "<".to_string(),
12845 end: ">".to_string(),
12846 close: false,
12847 surround: true,
12848 newline: true,
12849 },
12850 ],
12851 ..Default::default()
12852 },
12853 autoclose_before: "})]".to_string(),
12854 ..Default::default()
12855 },
12856 Some(tree_sitter_rust::LANGUAGE.into()),
12857 );
12858 let language = Arc::new(language);
12859
12860 cx.language_registry().add(language.clone());
12861 cx.update_buffer(|buffer, cx| {
12862 buffer.set_language(Some(language), cx);
12863 });
12864
12865 cx.set_state(
12866 &r#"
12867 fn main() {
12868 sampleˇ
12869 }
12870 "#
12871 .unindent(),
12872 );
12873
12874 cx.update_editor(|editor, window, cx| {
12875 editor.handle_input("(", window, cx);
12876 });
12877 cx.assert_editor_state(
12878 &"
12879 fn main() {
12880 sample(ˇ)
12881 }
12882 "
12883 .unindent(),
12884 );
12885
12886 let mocked_response = lsp::SignatureHelp {
12887 signatures: vec![lsp::SignatureInformation {
12888 label: "fn sample(param1: u8, param2: u8)".to_string(),
12889 documentation: None,
12890 parameters: Some(vec![
12891 lsp::ParameterInformation {
12892 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12893 documentation: None,
12894 },
12895 lsp::ParameterInformation {
12896 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12897 documentation: None,
12898 },
12899 ]),
12900 active_parameter: None,
12901 }],
12902 active_signature: Some(0),
12903 active_parameter: Some(0),
12904 };
12905 handle_signature_help_request(&mut cx, mocked_response).await;
12906
12907 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12908 .await;
12909
12910 cx.editor(|editor, _, _| {
12911 let signature_help_state = editor.signature_help_state.popover().cloned();
12912 let signature = signature_help_state.unwrap();
12913 assert_eq!(
12914 signature.signatures[signature.current_signature].label,
12915 "fn sample(param1: u8, param2: u8)"
12916 );
12917 });
12918}
12919
12920#[gpui::test]
12921async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12922 init_test(cx, |_| {});
12923
12924 cx.update(|cx| {
12925 cx.update_global::<SettingsStore, _>(|settings, cx| {
12926 settings.update_user_settings(cx, |settings| {
12927 settings.editor.auto_signature_help = Some(false);
12928 settings.editor.show_signature_help_after_edits = Some(false);
12929 });
12930 });
12931 });
12932
12933 let mut cx = EditorLspTestContext::new_rust(
12934 lsp::ServerCapabilities {
12935 signature_help_provider: Some(lsp::SignatureHelpOptions {
12936 ..Default::default()
12937 }),
12938 ..Default::default()
12939 },
12940 cx,
12941 )
12942 .await;
12943
12944 let language = Language::new(
12945 LanguageConfig {
12946 name: "Rust".into(),
12947 brackets: BracketPairConfig {
12948 pairs: vec![
12949 BracketPair {
12950 start: "{".to_string(),
12951 end: "}".to_string(),
12952 close: true,
12953 surround: true,
12954 newline: true,
12955 },
12956 BracketPair {
12957 start: "(".to_string(),
12958 end: ")".to_string(),
12959 close: true,
12960 surround: true,
12961 newline: true,
12962 },
12963 BracketPair {
12964 start: "/*".to_string(),
12965 end: " */".to_string(),
12966 close: true,
12967 surround: true,
12968 newline: true,
12969 },
12970 BracketPair {
12971 start: "[".to_string(),
12972 end: "]".to_string(),
12973 close: false,
12974 surround: false,
12975 newline: true,
12976 },
12977 BracketPair {
12978 start: "\"".to_string(),
12979 end: "\"".to_string(),
12980 close: true,
12981 surround: true,
12982 newline: false,
12983 },
12984 BracketPair {
12985 start: "<".to_string(),
12986 end: ">".to_string(),
12987 close: false,
12988 surround: true,
12989 newline: true,
12990 },
12991 ],
12992 ..Default::default()
12993 },
12994 autoclose_before: "})]".to_string(),
12995 ..Default::default()
12996 },
12997 Some(tree_sitter_rust::LANGUAGE.into()),
12998 );
12999 let language = Arc::new(language);
13000
13001 cx.language_registry().add(language.clone());
13002 cx.update_buffer(|buffer, cx| {
13003 buffer.set_language(Some(language), cx);
13004 });
13005
13006 // Ensure that signature_help is not called when no signature help is enabled.
13007 cx.set_state(
13008 &r#"
13009 fn main() {
13010 sampleˇ
13011 }
13012 "#
13013 .unindent(),
13014 );
13015 cx.update_editor(|editor, window, cx| {
13016 editor.handle_input("(", window, cx);
13017 });
13018 cx.assert_editor_state(
13019 &"
13020 fn main() {
13021 sample(ˇ)
13022 }
13023 "
13024 .unindent(),
13025 );
13026 cx.editor(|editor, _, _| {
13027 assert!(editor.signature_help_state.task().is_none());
13028 });
13029
13030 let mocked_response = lsp::SignatureHelp {
13031 signatures: vec![lsp::SignatureInformation {
13032 label: "fn sample(param1: u8, param2: u8)".to_string(),
13033 documentation: None,
13034 parameters: Some(vec![
13035 lsp::ParameterInformation {
13036 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13037 documentation: None,
13038 },
13039 lsp::ParameterInformation {
13040 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13041 documentation: None,
13042 },
13043 ]),
13044 active_parameter: None,
13045 }],
13046 active_signature: Some(0),
13047 active_parameter: Some(0),
13048 };
13049
13050 // Ensure that signature_help is called when enabled afte edits
13051 cx.update(|_, cx| {
13052 cx.update_global::<SettingsStore, _>(|settings, cx| {
13053 settings.update_user_settings(cx, |settings| {
13054 settings.editor.auto_signature_help = Some(false);
13055 settings.editor.show_signature_help_after_edits = Some(true);
13056 });
13057 });
13058 });
13059 cx.set_state(
13060 &r#"
13061 fn main() {
13062 sampleˇ
13063 }
13064 "#
13065 .unindent(),
13066 );
13067 cx.update_editor(|editor, window, cx| {
13068 editor.handle_input("(", window, cx);
13069 });
13070 cx.assert_editor_state(
13071 &"
13072 fn main() {
13073 sample(ˇ)
13074 }
13075 "
13076 .unindent(),
13077 );
13078 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13079 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13080 .await;
13081 cx.update_editor(|editor, _, _| {
13082 let signature_help_state = editor.signature_help_state.popover().cloned();
13083 assert!(signature_help_state.is_some());
13084 let signature = signature_help_state.unwrap();
13085 assert_eq!(
13086 signature.signatures[signature.current_signature].label,
13087 "fn sample(param1: u8, param2: u8)"
13088 );
13089 editor.signature_help_state = SignatureHelpState::default();
13090 });
13091
13092 // Ensure that signature_help is called when auto signature help override is enabled
13093 cx.update(|_, cx| {
13094 cx.update_global::<SettingsStore, _>(|settings, cx| {
13095 settings.update_user_settings(cx, |settings| {
13096 settings.editor.auto_signature_help = Some(true);
13097 settings.editor.show_signature_help_after_edits = Some(false);
13098 });
13099 });
13100 });
13101 cx.set_state(
13102 &r#"
13103 fn main() {
13104 sampleˇ
13105 }
13106 "#
13107 .unindent(),
13108 );
13109 cx.update_editor(|editor, window, cx| {
13110 editor.handle_input("(", window, cx);
13111 });
13112 cx.assert_editor_state(
13113 &"
13114 fn main() {
13115 sample(ˇ)
13116 }
13117 "
13118 .unindent(),
13119 );
13120 handle_signature_help_request(&mut cx, mocked_response).await;
13121 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13122 .await;
13123 cx.editor(|editor, _, _| {
13124 let signature_help_state = editor.signature_help_state.popover().cloned();
13125 assert!(signature_help_state.is_some());
13126 let signature = signature_help_state.unwrap();
13127 assert_eq!(
13128 signature.signatures[signature.current_signature].label,
13129 "fn sample(param1: u8, param2: u8)"
13130 );
13131 });
13132}
13133
13134#[gpui::test]
13135async fn test_signature_help(cx: &mut TestAppContext) {
13136 init_test(cx, |_| {});
13137 cx.update(|cx| {
13138 cx.update_global::<SettingsStore, _>(|settings, cx| {
13139 settings.update_user_settings(cx, |settings| {
13140 settings.editor.auto_signature_help = Some(true);
13141 });
13142 });
13143 });
13144
13145 let mut cx = EditorLspTestContext::new_rust(
13146 lsp::ServerCapabilities {
13147 signature_help_provider: Some(lsp::SignatureHelpOptions {
13148 ..Default::default()
13149 }),
13150 ..Default::default()
13151 },
13152 cx,
13153 )
13154 .await;
13155
13156 // A test that directly calls `show_signature_help`
13157 cx.update_editor(|editor, window, cx| {
13158 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13159 });
13160
13161 let mocked_response = lsp::SignatureHelp {
13162 signatures: vec![lsp::SignatureInformation {
13163 label: "fn sample(param1: u8, param2: u8)".to_string(),
13164 documentation: None,
13165 parameters: Some(vec![
13166 lsp::ParameterInformation {
13167 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13168 documentation: None,
13169 },
13170 lsp::ParameterInformation {
13171 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13172 documentation: None,
13173 },
13174 ]),
13175 active_parameter: None,
13176 }],
13177 active_signature: Some(0),
13178 active_parameter: Some(0),
13179 };
13180 handle_signature_help_request(&mut cx, mocked_response).await;
13181
13182 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13183 .await;
13184
13185 cx.editor(|editor, _, _| {
13186 let signature_help_state = editor.signature_help_state.popover().cloned();
13187 assert!(signature_help_state.is_some());
13188 let signature = signature_help_state.unwrap();
13189 assert_eq!(
13190 signature.signatures[signature.current_signature].label,
13191 "fn sample(param1: u8, param2: u8)"
13192 );
13193 });
13194
13195 // When exiting outside from inside the brackets, `signature_help` is closed.
13196 cx.set_state(indoc! {"
13197 fn main() {
13198 sample(ˇ);
13199 }
13200
13201 fn sample(param1: u8, param2: u8) {}
13202 "});
13203
13204 cx.update_editor(|editor, window, cx| {
13205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13206 s.select_ranges([0..0])
13207 });
13208 });
13209
13210 let mocked_response = lsp::SignatureHelp {
13211 signatures: Vec::new(),
13212 active_signature: None,
13213 active_parameter: None,
13214 };
13215 handle_signature_help_request(&mut cx, mocked_response).await;
13216
13217 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13218 .await;
13219
13220 cx.editor(|editor, _, _| {
13221 assert!(!editor.signature_help_state.is_shown());
13222 });
13223
13224 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13225 cx.set_state(indoc! {"
13226 fn main() {
13227 sample(ˇ);
13228 }
13229
13230 fn sample(param1: u8, param2: u8) {}
13231 "});
13232
13233 let mocked_response = lsp::SignatureHelp {
13234 signatures: vec![lsp::SignatureInformation {
13235 label: "fn sample(param1: u8, param2: u8)".to_string(),
13236 documentation: None,
13237 parameters: Some(vec![
13238 lsp::ParameterInformation {
13239 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13240 documentation: None,
13241 },
13242 lsp::ParameterInformation {
13243 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13244 documentation: None,
13245 },
13246 ]),
13247 active_parameter: None,
13248 }],
13249 active_signature: Some(0),
13250 active_parameter: Some(0),
13251 };
13252 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13253 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13254 .await;
13255 cx.editor(|editor, _, _| {
13256 assert!(editor.signature_help_state.is_shown());
13257 });
13258
13259 // Restore the popover with more parameter input
13260 cx.set_state(indoc! {"
13261 fn main() {
13262 sample(param1, param2ˇ);
13263 }
13264
13265 fn sample(param1: u8, param2: u8) {}
13266 "});
13267
13268 let mocked_response = lsp::SignatureHelp {
13269 signatures: vec![lsp::SignatureInformation {
13270 label: "fn sample(param1: u8, param2: u8)".to_string(),
13271 documentation: None,
13272 parameters: Some(vec![
13273 lsp::ParameterInformation {
13274 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13275 documentation: None,
13276 },
13277 lsp::ParameterInformation {
13278 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13279 documentation: None,
13280 },
13281 ]),
13282 active_parameter: None,
13283 }],
13284 active_signature: Some(0),
13285 active_parameter: Some(1),
13286 };
13287 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13288 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13289 .await;
13290
13291 // When selecting a range, the popover is gone.
13292 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13293 cx.update_editor(|editor, window, cx| {
13294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13295 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13296 })
13297 });
13298 cx.assert_editor_state(indoc! {"
13299 fn main() {
13300 sample(param1, «ˇparam2»);
13301 }
13302
13303 fn sample(param1: u8, param2: u8) {}
13304 "});
13305 cx.editor(|editor, _, _| {
13306 assert!(!editor.signature_help_state.is_shown());
13307 });
13308
13309 // When unselecting again, the popover is back if within the brackets.
13310 cx.update_editor(|editor, window, cx| {
13311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13312 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13313 })
13314 });
13315 cx.assert_editor_state(indoc! {"
13316 fn main() {
13317 sample(param1, ˇparam2);
13318 }
13319
13320 fn sample(param1: u8, param2: u8) {}
13321 "});
13322 handle_signature_help_request(&mut cx, mocked_response).await;
13323 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13324 .await;
13325 cx.editor(|editor, _, _| {
13326 assert!(editor.signature_help_state.is_shown());
13327 });
13328
13329 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13330 cx.update_editor(|editor, window, cx| {
13331 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13332 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13333 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13334 })
13335 });
13336 cx.assert_editor_state(indoc! {"
13337 fn main() {
13338 sample(param1, ˇparam2);
13339 }
13340
13341 fn sample(param1: u8, param2: u8) {}
13342 "});
13343
13344 let mocked_response = lsp::SignatureHelp {
13345 signatures: vec![lsp::SignatureInformation {
13346 label: "fn sample(param1: u8, param2: u8)".to_string(),
13347 documentation: None,
13348 parameters: Some(vec![
13349 lsp::ParameterInformation {
13350 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13351 documentation: None,
13352 },
13353 lsp::ParameterInformation {
13354 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13355 documentation: None,
13356 },
13357 ]),
13358 active_parameter: None,
13359 }],
13360 active_signature: Some(0),
13361 active_parameter: Some(1),
13362 };
13363 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13364 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13365 .await;
13366 cx.update_editor(|editor, _, cx| {
13367 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13368 });
13369 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13370 .await;
13371 cx.update_editor(|editor, window, cx| {
13372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13373 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13374 })
13375 });
13376 cx.assert_editor_state(indoc! {"
13377 fn main() {
13378 sample(param1, «ˇparam2»);
13379 }
13380
13381 fn sample(param1: u8, param2: u8) {}
13382 "});
13383 cx.update_editor(|editor, window, cx| {
13384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13385 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13386 })
13387 });
13388 cx.assert_editor_state(indoc! {"
13389 fn main() {
13390 sample(param1, ˇparam2);
13391 }
13392
13393 fn sample(param1: u8, param2: u8) {}
13394 "});
13395 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13396 .await;
13397}
13398
13399#[gpui::test]
13400async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13401 init_test(cx, |_| {});
13402
13403 let mut cx = EditorLspTestContext::new_rust(
13404 lsp::ServerCapabilities {
13405 signature_help_provider: Some(lsp::SignatureHelpOptions {
13406 ..Default::default()
13407 }),
13408 ..Default::default()
13409 },
13410 cx,
13411 )
13412 .await;
13413
13414 cx.set_state(indoc! {"
13415 fn main() {
13416 overloadedˇ
13417 }
13418 "});
13419
13420 cx.update_editor(|editor, window, cx| {
13421 editor.handle_input("(", window, cx);
13422 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13423 });
13424
13425 // Mock response with 3 signatures
13426 let mocked_response = lsp::SignatureHelp {
13427 signatures: vec![
13428 lsp::SignatureInformation {
13429 label: "fn overloaded(x: i32)".to_string(),
13430 documentation: None,
13431 parameters: Some(vec![lsp::ParameterInformation {
13432 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13433 documentation: None,
13434 }]),
13435 active_parameter: None,
13436 },
13437 lsp::SignatureInformation {
13438 label: "fn overloaded(x: i32, y: i32)".to_string(),
13439 documentation: None,
13440 parameters: Some(vec![
13441 lsp::ParameterInformation {
13442 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13443 documentation: None,
13444 },
13445 lsp::ParameterInformation {
13446 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13447 documentation: None,
13448 },
13449 ]),
13450 active_parameter: None,
13451 },
13452 lsp::SignatureInformation {
13453 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13454 documentation: None,
13455 parameters: Some(vec![
13456 lsp::ParameterInformation {
13457 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13458 documentation: None,
13459 },
13460 lsp::ParameterInformation {
13461 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13462 documentation: None,
13463 },
13464 lsp::ParameterInformation {
13465 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13466 documentation: None,
13467 },
13468 ]),
13469 active_parameter: None,
13470 },
13471 ],
13472 active_signature: Some(1),
13473 active_parameter: Some(0),
13474 };
13475 handle_signature_help_request(&mut cx, mocked_response).await;
13476
13477 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13478 .await;
13479
13480 // Verify we have multiple signatures and the right one is selected
13481 cx.editor(|editor, _, _| {
13482 let popover = editor.signature_help_state.popover().cloned().unwrap();
13483 assert_eq!(popover.signatures.len(), 3);
13484 // active_signature was 1, so that should be the current
13485 assert_eq!(popover.current_signature, 1);
13486 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13487 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13488 assert_eq!(
13489 popover.signatures[2].label,
13490 "fn overloaded(x: i32, y: i32, z: i32)"
13491 );
13492 });
13493
13494 // Test navigation functionality
13495 cx.update_editor(|editor, window, cx| {
13496 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13497 });
13498
13499 cx.editor(|editor, _, _| {
13500 let popover = editor.signature_help_state.popover().cloned().unwrap();
13501 assert_eq!(popover.current_signature, 2);
13502 });
13503
13504 // Test wrap around
13505 cx.update_editor(|editor, window, cx| {
13506 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13507 });
13508
13509 cx.editor(|editor, _, _| {
13510 let popover = editor.signature_help_state.popover().cloned().unwrap();
13511 assert_eq!(popover.current_signature, 0);
13512 });
13513
13514 // Test previous navigation
13515 cx.update_editor(|editor, window, cx| {
13516 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13517 });
13518
13519 cx.editor(|editor, _, _| {
13520 let popover = editor.signature_help_state.popover().cloned().unwrap();
13521 assert_eq!(popover.current_signature, 2);
13522 });
13523}
13524
13525#[gpui::test]
13526async fn test_completion_mode(cx: &mut TestAppContext) {
13527 init_test(cx, |_| {});
13528 let mut cx = EditorLspTestContext::new_rust(
13529 lsp::ServerCapabilities {
13530 completion_provider: Some(lsp::CompletionOptions {
13531 resolve_provider: Some(true),
13532 ..Default::default()
13533 }),
13534 ..Default::default()
13535 },
13536 cx,
13537 )
13538 .await;
13539
13540 struct Run {
13541 run_description: &'static str,
13542 initial_state: String,
13543 buffer_marked_text: String,
13544 completion_label: &'static str,
13545 completion_text: &'static str,
13546 expected_with_insert_mode: String,
13547 expected_with_replace_mode: String,
13548 expected_with_replace_subsequence_mode: String,
13549 expected_with_replace_suffix_mode: String,
13550 }
13551
13552 let runs = [
13553 Run {
13554 run_description: "Start of word matches completion text",
13555 initial_state: "before ediˇ after".into(),
13556 buffer_marked_text: "before <edi|> after".into(),
13557 completion_label: "editor",
13558 completion_text: "editor",
13559 expected_with_insert_mode: "before editorˇ after".into(),
13560 expected_with_replace_mode: "before editorˇ after".into(),
13561 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13562 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13563 },
13564 Run {
13565 run_description: "Accept same text at the middle of the word",
13566 initial_state: "before ediˇtor after".into(),
13567 buffer_marked_text: "before <edi|tor> after".into(),
13568 completion_label: "editor",
13569 completion_text: "editor",
13570 expected_with_insert_mode: "before editorˇtor after".into(),
13571 expected_with_replace_mode: "before editorˇ after".into(),
13572 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13573 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13574 },
13575 Run {
13576 run_description: "End of word matches completion text -- cursor at end",
13577 initial_state: "before torˇ after".into(),
13578 buffer_marked_text: "before <tor|> after".into(),
13579 completion_label: "editor",
13580 completion_text: "editor",
13581 expected_with_insert_mode: "before editorˇ after".into(),
13582 expected_with_replace_mode: "before editorˇ after".into(),
13583 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13584 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13585 },
13586 Run {
13587 run_description: "End of word matches completion text -- cursor at start",
13588 initial_state: "before ˇtor after".into(),
13589 buffer_marked_text: "before <|tor> after".into(),
13590 completion_label: "editor",
13591 completion_text: "editor",
13592 expected_with_insert_mode: "before editorˇtor after".into(),
13593 expected_with_replace_mode: "before editorˇ after".into(),
13594 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13595 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13596 },
13597 Run {
13598 run_description: "Prepend text containing whitespace",
13599 initial_state: "pˇfield: bool".into(),
13600 buffer_marked_text: "<p|field>: bool".into(),
13601 completion_label: "pub ",
13602 completion_text: "pub ",
13603 expected_with_insert_mode: "pub ˇfield: bool".into(),
13604 expected_with_replace_mode: "pub ˇ: bool".into(),
13605 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13606 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13607 },
13608 Run {
13609 run_description: "Add element to start of list",
13610 initial_state: "[element_ˇelement_2]".into(),
13611 buffer_marked_text: "[<element_|element_2>]".into(),
13612 completion_label: "element_1",
13613 completion_text: "element_1",
13614 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13615 expected_with_replace_mode: "[element_1ˇ]".into(),
13616 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13617 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13618 },
13619 Run {
13620 run_description: "Add element to start of list -- first and second elements are equal",
13621 initial_state: "[elˇelement]".into(),
13622 buffer_marked_text: "[<el|element>]".into(),
13623 completion_label: "element",
13624 completion_text: "element",
13625 expected_with_insert_mode: "[elementˇelement]".into(),
13626 expected_with_replace_mode: "[elementˇ]".into(),
13627 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13628 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13629 },
13630 Run {
13631 run_description: "Ends with matching suffix",
13632 initial_state: "SubˇError".into(),
13633 buffer_marked_text: "<Sub|Error>".into(),
13634 completion_label: "SubscriptionError",
13635 completion_text: "SubscriptionError",
13636 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13637 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13638 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13639 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13640 },
13641 Run {
13642 run_description: "Suffix is a subsequence -- contiguous",
13643 initial_state: "SubˇErr".into(),
13644 buffer_marked_text: "<Sub|Err>".into(),
13645 completion_label: "SubscriptionError",
13646 completion_text: "SubscriptionError",
13647 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13648 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13649 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13650 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13651 },
13652 Run {
13653 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13654 initial_state: "Suˇscrirr".into(),
13655 buffer_marked_text: "<Su|scrirr>".into(),
13656 completion_label: "SubscriptionError",
13657 completion_text: "SubscriptionError",
13658 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13659 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13660 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13661 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13662 },
13663 Run {
13664 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13665 initial_state: "foo(indˇix)".into(),
13666 buffer_marked_text: "foo(<ind|ix>)".into(),
13667 completion_label: "node_index",
13668 completion_text: "node_index",
13669 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13670 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13671 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13672 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13673 },
13674 Run {
13675 run_description: "Replace range ends before cursor - should extend to cursor",
13676 initial_state: "before editˇo after".into(),
13677 buffer_marked_text: "before <{ed}>it|o after".into(),
13678 completion_label: "editor",
13679 completion_text: "editor",
13680 expected_with_insert_mode: "before editorˇo after".into(),
13681 expected_with_replace_mode: "before editorˇo after".into(),
13682 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13683 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13684 },
13685 Run {
13686 run_description: "Uses label for suffix matching",
13687 initial_state: "before ediˇtor after".into(),
13688 buffer_marked_text: "before <edi|tor> after".into(),
13689 completion_label: "editor",
13690 completion_text: "editor()",
13691 expected_with_insert_mode: "before editor()ˇtor after".into(),
13692 expected_with_replace_mode: "before editor()ˇ after".into(),
13693 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13694 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13695 },
13696 Run {
13697 run_description: "Case insensitive subsequence and suffix matching",
13698 initial_state: "before EDiˇtoR after".into(),
13699 buffer_marked_text: "before <EDi|toR> after".into(),
13700 completion_label: "editor",
13701 completion_text: "editor",
13702 expected_with_insert_mode: "before editorˇtoR after".into(),
13703 expected_with_replace_mode: "before editorˇ after".into(),
13704 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13705 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13706 },
13707 ];
13708
13709 for run in runs {
13710 let run_variations = [
13711 (LspInsertMode::Insert, run.expected_with_insert_mode),
13712 (LspInsertMode::Replace, run.expected_with_replace_mode),
13713 (
13714 LspInsertMode::ReplaceSubsequence,
13715 run.expected_with_replace_subsequence_mode,
13716 ),
13717 (
13718 LspInsertMode::ReplaceSuffix,
13719 run.expected_with_replace_suffix_mode,
13720 ),
13721 ];
13722
13723 for (lsp_insert_mode, expected_text) in run_variations {
13724 eprintln!(
13725 "run = {:?}, mode = {lsp_insert_mode:.?}",
13726 run.run_description,
13727 );
13728
13729 update_test_language_settings(&mut cx, |settings| {
13730 settings.defaults.completions = Some(CompletionSettingsContent {
13731 lsp_insert_mode: Some(lsp_insert_mode),
13732 words: Some(WordsCompletionMode::Disabled),
13733 words_min_length: Some(0),
13734 ..Default::default()
13735 });
13736 });
13737
13738 cx.set_state(&run.initial_state);
13739 cx.update_editor(|editor, window, cx| {
13740 editor.show_completions(
13741 &ShowCompletions {
13742 trigger: None,
13743 snippets_only: false,
13744 },
13745 window,
13746 cx,
13747 );
13748 });
13749
13750 let counter = Arc::new(AtomicUsize::new(0));
13751 handle_completion_request_with_insert_and_replace(
13752 &mut cx,
13753 &run.buffer_marked_text,
13754 vec![(run.completion_label, run.completion_text)],
13755 counter.clone(),
13756 )
13757 .await;
13758 cx.condition(|editor, _| editor.context_menu_visible())
13759 .await;
13760 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13761
13762 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13763 editor
13764 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13765 .unwrap()
13766 });
13767 cx.assert_editor_state(&expected_text);
13768 handle_resolve_completion_request(&mut cx, None).await;
13769 apply_additional_edits.await.unwrap();
13770 }
13771 }
13772}
13773
13774#[gpui::test]
13775async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13776 init_test(cx, |_| {});
13777 let mut cx = EditorLspTestContext::new_rust(
13778 lsp::ServerCapabilities {
13779 completion_provider: Some(lsp::CompletionOptions {
13780 resolve_provider: Some(true),
13781 ..Default::default()
13782 }),
13783 ..Default::default()
13784 },
13785 cx,
13786 )
13787 .await;
13788
13789 let initial_state = "SubˇError";
13790 let buffer_marked_text = "<Sub|Error>";
13791 let completion_text = "SubscriptionError";
13792 let expected_with_insert_mode = "SubscriptionErrorˇError";
13793 let expected_with_replace_mode = "SubscriptionErrorˇ";
13794
13795 update_test_language_settings(&mut cx, |settings| {
13796 settings.defaults.completions = Some(CompletionSettingsContent {
13797 words: Some(WordsCompletionMode::Disabled),
13798 words_min_length: Some(0),
13799 // set the opposite here to ensure that the action is overriding the default behavior
13800 lsp_insert_mode: Some(LspInsertMode::Insert),
13801 ..Default::default()
13802 });
13803 });
13804
13805 cx.set_state(initial_state);
13806 cx.update_editor(|editor, window, cx| {
13807 editor.show_completions(
13808 &ShowCompletions {
13809 trigger: None,
13810 snippets_only: false,
13811 },
13812 window,
13813 cx,
13814 );
13815 });
13816
13817 let counter = Arc::new(AtomicUsize::new(0));
13818 handle_completion_request_with_insert_and_replace(
13819 &mut cx,
13820 buffer_marked_text,
13821 vec![(completion_text, completion_text)],
13822 counter.clone(),
13823 )
13824 .await;
13825 cx.condition(|editor, _| editor.context_menu_visible())
13826 .await;
13827 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13828
13829 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13830 editor
13831 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13832 .unwrap()
13833 });
13834 cx.assert_editor_state(expected_with_replace_mode);
13835 handle_resolve_completion_request(&mut cx, None).await;
13836 apply_additional_edits.await.unwrap();
13837
13838 update_test_language_settings(&mut cx, |settings| {
13839 settings.defaults.completions = Some(CompletionSettingsContent {
13840 words: Some(WordsCompletionMode::Disabled),
13841 words_min_length: Some(0),
13842 // set the opposite here to ensure that the action is overriding the default behavior
13843 lsp_insert_mode: Some(LspInsertMode::Replace),
13844 ..Default::default()
13845 });
13846 });
13847
13848 cx.set_state(initial_state);
13849 cx.update_editor(|editor, window, cx| {
13850 editor.show_completions(
13851 &ShowCompletions {
13852 trigger: None,
13853 snippets_only: false,
13854 },
13855 window,
13856 cx,
13857 );
13858 });
13859 handle_completion_request_with_insert_and_replace(
13860 &mut cx,
13861 buffer_marked_text,
13862 vec![(completion_text, completion_text)],
13863 counter.clone(),
13864 )
13865 .await;
13866 cx.condition(|editor, _| editor.context_menu_visible())
13867 .await;
13868 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13869
13870 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13871 editor
13872 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13873 .unwrap()
13874 });
13875 cx.assert_editor_state(expected_with_insert_mode);
13876 handle_resolve_completion_request(&mut cx, None).await;
13877 apply_additional_edits.await.unwrap();
13878}
13879
13880#[gpui::test]
13881async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13882 init_test(cx, |_| {});
13883 let mut cx = EditorLspTestContext::new_rust(
13884 lsp::ServerCapabilities {
13885 completion_provider: Some(lsp::CompletionOptions {
13886 resolve_provider: Some(true),
13887 ..Default::default()
13888 }),
13889 ..Default::default()
13890 },
13891 cx,
13892 )
13893 .await;
13894
13895 // scenario: surrounding text matches completion text
13896 let completion_text = "to_offset";
13897 let initial_state = indoc! {"
13898 1. buf.to_offˇsuffix
13899 2. buf.to_offˇsuf
13900 3. buf.to_offˇfix
13901 4. buf.to_offˇ
13902 5. into_offˇensive
13903 6. ˇsuffix
13904 7. let ˇ //
13905 8. aaˇzz
13906 9. buf.to_off«zzzzzˇ»suffix
13907 10. buf.«ˇzzzzz»suffix
13908 11. to_off«ˇzzzzz»
13909
13910 buf.to_offˇsuffix // newest cursor
13911 "};
13912 let completion_marked_buffer = indoc! {"
13913 1. buf.to_offsuffix
13914 2. buf.to_offsuf
13915 3. buf.to_offfix
13916 4. buf.to_off
13917 5. into_offensive
13918 6. suffix
13919 7. let //
13920 8. aazz
13921 9. buf.to_offzzzzzsuffix
13922 10. buf.zzzzzsuffix
13923 11. to_offzzzzz
13924
13925 buf.<to_off|suffix> // newest cursor
13926 "};
13927 let expected = indoc! {"
13928 1. buf.to_offsetˇ
13929 2. buf.to_offsetˇsuf
13930 3. buf.to_offsetˇfix
13931 4. buf.to_offsetˇ
13932 5. into_offsetˇensive
13933 6. to_offsetˇsuffix
13934 7. let to_offsetˇ //
13935 8. aato_offsetˇzz
13936 9. buf.to_offsetˇ
13937 10. buf.to_offsetˇsuffix
13938 11. to_offsetˇ
13939
13940 buf.to_offsetˇ // newest cursor
13941 "};
13942 cx.set_state(initial_state);
13943 cx.update_editor(|editor, window, cx| {
13944 editor.show_completions(
13945 &ShowCompletions {
13946 trigger: None,
13947 snippets_only: false,
13948 },
13949 window,
13950 cx,
13951 );
13952 });
13953 handle_completion_request_with_insert_and_replace(
13954 &mut cx,
13955 completion_marked_buffer,
13956 vec![(completion_text, completion_text)],
13957 Arc::new(AtomicUsize::new(0)),
13958 )
13959 .await;
13960 cx.condition(|editor, _| editor.context_menu_visible())
13961 .await;
13962 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13963 editor
13964 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13965 .unwrap()
13966 });
13967 cx.assert_editor_state(expected);
13968 handle_resolve_completion_request(&mut cx, None).await;
13969 apply_additional_edits.await.unwrap();
13970
13971 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13972 let completion_text = "foo_and_bar";
13973 let initial_state = indoc! {"
13974 1. ooanbˇ
13975 2. zooanbˇ
13976 3. ooanbˇz
13977 4. zooanbˇz
13978 5. ooanˇ
13979 6. oanbˇ
13980
13981 ooanbˇ
13982 "};
13983 let completion_marked_buffer = indoc! {"
13984 1. ooanb
13985 2. zooanb
13986 3. ooanbz
13987 4. zooanbz
13988 5. ooan
13989 6. oanb
13990
13991 <ooanb|>
13992 "};
13993 let expected = indoc! {"
13994 1. foo_and_barˇ
13995 2. zfoo_and_barˇ
13996 3. foo_and_barˇz
13997 4. zfoo_and_barˇz
13998 5. ooanfoo_and_barˇ
13999 6. oanbfoo_and_barˇ
14000
14001 foo_and_barˇ
14002 "};
14003 cx.set_state(initial_state);
14004 cx.update_editor(|editor, window, cx| {
14005 editor.show_completions(
14006 &ShowCompletions {
14007 trigger: None,
14008 snippets_only: false,
14009 },
14010 window,
14011 cx,
14012 );
14013 });
14014 handle_completion_request_with_insert_and_replace(
14015 &mut cx,
14016 completion_marked_buffer,
14017 vec![(completion_text, completion_text)],
14018 Arc::new(AtomicUsize::new(0)),
14019 )
14020 .await;
14021 cx.condition(|editor, _| editor.context_menu_visible())
14022 .await;
14023 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14024 editor
14025 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14026 .unwrap()
14027 });
14028 cx.assert_editor_state(expected);
14029 handle_resolve_completion_request(&mut cx, None).await;
14030 apply_additional_edits.await.unwrap();
14031
14032 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14033 // (expects the same as if it was inserted at the end)
14034 let completion_text = "foo_and_bar";
14035 let initial_state = indoc! {"
14036 1. ooˇanb
14037 2. zooˇanb
14038 3. ooˇanbz
14039 4. zooˇanbz
14040
14041 ooˇanb
14042 "};
14043 let completion_marked_buffer = indoc! {"
14044 1. ooanb
14045 2. zooanb
14046 3. ooanbz
14047 4. zooanbz
14048
14049 <oo|anb>
14050 "};
14051 let expected = indoc! {"
14052 1. foo_and_barˇ
14053 2. zfoo_and_barˇ
14054 3. foo_and_barˇz
14055 4. zfoo_and_barˇz
14056
14057 foo_and_barˇ
14058 "};
14059 cx.set_state(initial_state);
14060 cx.update_editor(|editor, window, cx| {
14061 editor.show_completions(
14062 &ShowCompletions {
14063 trigger: None,
14064 snippets_only: false,
14065 },
14066 window,
14067 cx,
14068 );
14069 });
14070 handle_completion_request_with_insert_and_replace(
14071 &mut cx,
14072 completion_marked_buffer,
14073 vec![(completion_text, completion_text)],
14074 Arc::new(AtomicUsize::new(0)),
14075 )
14076 .await;
14077 cx.condition(|editor, _| editor.context_menu_visible())
14078 .await;
14079 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14080 editor
14081 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14082 .unwrap()
14083 });
14084 cx.assert_editor_state(expected);
14085 handle_resolve_completion_request(&mut cx, None).await;
14086 apply_additional_edits.await.unwrap();
14087}
14088
14089// This used to crash
14090#[gpui::test]
14091async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14092 init_test(cx, |_| {});
14093
14094 let buffer_text = indoc! {"
14095 fn main() {
14096 10.satu;
14097
14098 //
14099 // separate cursors so they open in different excerpts (manually reproducible)
14100 //
14101
14102 10.satu20;
14103 }
14104 "};
14105 let multibuffer_text_with_selections = indoc! {"
14106 fn main() {
14107 10.satuˇ;
14108
14109 //
14110
14111 //
14112
14113 10.satuˇ20;
14114 }
14115 "};
14116 let expected_multibuffer = indoc! {"
14117 fn main() {
14118 10.saturating_sub()ˇ;
14119
14120 //
14121
14122 //
14123
14124 10.saturating_sub()ˇ;
14125 }
14126 "};
14127
14128 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14129 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14130
14131 let fs = FakeFs::new(cx.executor());
14132 fs.insert_tree(
14133 path!("/a"),
14134 json!({
14135 "main.rs": buffer_text,
14136 }),
14137 )
14138 .await;
14139
14140 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14141 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14142 language_registry.add(rust_lang());
14143 let mut fake_servers = language_registry.register_fake_lsp(
14144 "Rust",
14145 FakeLspAdapter {
14146 capabilities: lsp::ServerCapabilities {
14147 completion_provider: Some(lsp::CompletionOptions {
14148 resolve_provider: None,
14149 ..lsp::CompletionOptions::default()
14150 }),
14151 ..lsp::ServerCapabilities::default()
14152 },
14153 ..FakeLspAdapter::default()
14154 },
14155 );
14156 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14157 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14158 let buffer = project
14159 .update(cx, |project, cx| {
14160 project.open_local_buffer(path!("/a/main.rs"), cx)
14161 })
14162 .await
14163 .unwrap();
14164
14165 let multi_buffer = cx.new(|cx| {
14166 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14167 multi_buffer.push_excerpts(
14168 buffer.clone(),
14169 [ExcerptRange::new(0..first_excerpt_end)],
14170 cx,
14171 );
14172 multi_buffer.push_excerpts(
14173 buffer.clone(),
14174 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14175 cx,
14176 );
14177 multi_buffer
14178 });
14179
14180 let editor = workspace
14181 .update(cx, |_, window, cx| {
14182 cx.new(|cx| {
14183 Editor::new(
14184 EditorMode::Full {
14185 scale_ui_elements_with_buffer_font_size: false,
14186 show_active_line_background: false,
14187 sized_by_content: false,
14188 },
14189 multi_buffer.clone(),
14190 Some(project.clone()),
14191 window,
14192 cx,
14193 )
14194 })
14195 })
14196 .unwrap();
14197
14198 let pane = workspace
14199 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14200 .unwrap();
14201 pane.update_in(cx, |pane, window, cx| {
14202 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14203 });
14204
14205 let fake_server = fake_servers.next().await.unwrap();
14206
14207 editor.update_in(cx, |editor, window, cx| {
14208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14209 s.select_ranges([
14210 Point::new(1, 11)..Point::new(1, 11),
14211 Point::new(7, 11)..Point::new(7, 11),
14212 ])
14213 });
14214
14215 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14216 });
14217
14218 editor.update_in(cx, |editor, window, cx| {
14219 editor.show_completions(
14220 &ShowCompletions {
14221 trigger: None,
14222 snippets_only: false,
14223 },
14224 window,
14225 cx,
14226 );
14227 });
14228
14229 fake_server
14230 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14231 let completion_item = lsp::CompletionItem {
14232 label: "saturating_sub()".into(),
14233 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14234 lsp::InsertReplaceEdit {
14235 new_text: "saturating_sub()".to_owned(),
14236 insert: lsp::Range::new(
14237 lsp::Position::new(7, 7),
14238 lsp::Position::new(7, 11),
14239 ),
14240 replace: lsp::Range::new(
14241 lsp::Position::new(7, 7),
14242 lsp::Position::new(7, 13),
14243 ),
14244 },
14245 )),
14246 ..lsp::CompletionItem::default()
14247 };
14248
14249 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14250 })
14251 .next()
14252 .await
14253 .unwrap();
14254
14255 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14256 .await;
14257
14258 editor
14259 .update_in(cx, |editor, window, cx| {
14260 editor
14261 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14262 .unwrap()
14263 })
14264 .await
14265 .unwrap();
14266
14267 editor.update(cx, |editor, cx| {
14268 assert_text_with_selections(editor, expected_multibuffer, cx);
14269 })
14270}
14271
14272#[gpui::test]
14273async fn test_completion(cx: &mut TestAppContext) {
14274 init_test(cx, |_| {});
14275
14276 let mut cx = EditorLspTestContext::new_rust(
14277 lsp::ServerCapabilities {
14278 completion_provider: Some(lsp::CompletionOptions {
14279 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14280 resolve_provider: Some(true),
14281 ..Default::default()
14282 }),
14283 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14284 ..Default::default()
14285 },
14286 cx,
14287 )
14288 .await;
14289 let counter = Arc::new(AtomicUsize::new(0));
14290
14291 cx.set_state(indoc! {"
14292 oneˇ
14293 two
14294 three
14295 "});
14296 cx.simulate_keystroke(".");
14297 handle_completion_request(
14298 indoc! {"
14299 one.|<>
14300 two
14301 three
14302 "},
14303 vec!["first_completion", "second_completion"],
14304 true,
14305 counter.clone(),
14306 &mut cx,
14307 )
14308 .await;
14309 cx.condition(|editor, _| editor.context_menu_visible())
14310 .await;
14311 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14312
14313 let _handler = handle_signature_help_request(
14314 &mut cx,
14315 lsp::SignatureHelp {
14316 signatures: vec![lsp::SignatureInformation {
14317 label: "test signature".to_string(),
14318 documentation: None,
14319 parameters: Some(vec![lsp::ParameterInformation {
14320 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14321 documentation: None,
14322 }]),
14323 active_parameter: None,
14324 }],
14325 active_signature: None,
14326 active_parameter: None,
14327 },
14328 );
14329 cx.update_editor(|editor, window, cx| {
14330 assert!(
14331 !editor.signature_help_state.is_shown(),
14332 "No signature help was called for"
14333 );
14334 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14335 });
14336 cx.run_until_parked();
14337 cx.update_editor(|editor, _, _| {
14338 assert!(
14339 !editor.signature_help_state.is_shown(),
14340 "No signature help should be shown when completions menu is open"
14341 );
14342 });
14343
14344 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14345 editor.context_menu_next(&Default::default(), window, cx);
14346 editor
14347 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14348 .unwrap()
14349 });
14350 cx.assert_editor_state(indoc! {"
14351 one.second_completionˇ
14352 two
14353 three
14354 "});
14355
14356 handle_resolve_completion_request(
14357 &mut cx,
14358 Some(vec![
14359 (
14360 //This overlaps with the primary completion edit which is
14361 //misbehavior from the LSP spec, test that we filter it out
14362 indoc! {"
14363 one.second_ˇcompletion
14364 two
14365 threeˇ
14366 "},
14367 "overlapping additional edit",
14368 ),
14369 (
14370 indoc! {"
14371 one.second_completion
14372 two
14373 threeˇ
14374 "},
14375 "\nadditional edit",
14376 ),
14377 ]),
14378 )
14379 .await;
14380 apply_additional_edits.await.unwrap();
14381 cx.assert_editor_state(indoc! {"
14382 one.second_completionˇ
14383 two
14384 three
14385 additional edit
14386 "});
14387
14388 cx.set_state(indoc! {"
14389 one.second_completion
14390 twoˇ
14391 threeˇ
14392 additional edit
14393 "});
14394 cx.simulate_keystroke(" ");
14395 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14396 cx.simulate_keystroke("s");
14397 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14398
14399 cx.assert_editor_state(indoc! {"
14400 one.second_completion
14401 two sˇ
14402 three sˇ
14403 additional edit
14404 "});
14405 handle_completion_request(
14406 indoc! {"
14407 one.second_completion
14408 two s
14409 three <s|>
14410 additional edit
14411 "},
14412 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14413 true,
14414 counter.clone(),
14415 &mut cx,
14416 )
14417 .await;
14418 cx.condition(|editor, _| editor.context_menu_visible())
14419 .await;
14420 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14421
14422 cx.simulate_keystroke("i");
14423
14424 handle_completion_request(
14425 indoc! {"
14426 one.second_completion
14427 two si
14428 three <si|>
14429 additional edit
14430 "},
14431 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14432 true,
14433 counter.clone(),
14434 &mut cx,
14435 )
14436 .await;
14437 cx.condition(|editor, _| editor.context_menu_visible())
14438 .await;
14439 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14440
14441 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14442 editor
14443 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14444 .unwrap()
14445 });
14446 cx.assert_editor_state(indoc! {"
14447 one.second_completion
14448 two sixth_completionˇ
14449 three sixth_completionˇ
14450 additional edit
14451 "});
14452
14453 apply_additional_edits.await.unwrap();
14454
14455 update_test_language_settings(&mut cx, |settings| {
14456 settings.defaults.show_completions_on_input = Some(false);
14457 });
14458 cx.set_state("editorˇ");
14459 cx.simulate_keystroke(".");
14460 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14461 cx.simulate_keystrokes("c l o");
14462 cx.assert_editor_state("editor.cloˇ");
14463 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14464 cx.update_editor(|editor, window, cx| {
14465 editor.show_completions(
14466 &ShowCompletions {
14467 trigger: None,
14468 snippets_only: false,
14469 },
14470 window,
14471 cx,
14472 );
14473 });
14474 handle_completion_request(
14475 "editor.<clo|>",
14476 vec!["close", "clobber"],
14477 true,
14478 counter.clone(),
14479 &mut cx,
14480 )
14481 .await;
14482 cx.condition(|editor, _| editor.context_menu_visible())
14483 .await;
14484 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14485
14486 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14487 editor
14488 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14489 .unwrap()
14490 });
14491 cx.assert_editor_state("editor.clobberˇ");
14492 handle_resolve_completion_request(&mut cx, None).await;
14493 apply_additional_edits.await.unwrap();
14494}
14495
14496#[gpui::test]
14497async fn test_completion_reuse(cx: &mut TestAppContext) {
14498 init_test(cx, |_| {});
14499
14500 let mut cx = EditorLspTestContext::new_rust(
14501 lsp::ServerCapabilities {
14502 completion_provider: Some(lsp::CompletionOptions {
14503 trigger_characters: Some(vec![".".to_string()]),
14504 ..Default::default()
14505 }),
14506 ..Default::default()
14507 },
14508 cx,
14509 )
14510 .await;
14511
14512 let counter = Arc::new(AtomicUsize::new(0));
14513 cx.set_state("objˇ");
14514 cx.simulate_keystroke(".");
14515
14516 // Initial completion request returns complete results
14517 let is_incomplete = false;
14518 handle_completion_request(
14519 "obj.|<>",
14520 vec!["a", "ab", "abc"],
14521 is_incomplete,
14522 counter.clone(),
14523 &mut cx,
14524 )
14525 .await;
14526 cx.run_until_parked();
14527 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14528 cx.assert_editor_state("obj.ˇ");
14529 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14530
14531 // Type "a" - filters existing completions
14532 cx.simulate_keystroke("a");
14533 cx.run_until_parked();
14534 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14535 cx.assert_editor_state("obj.aˇ");
14536 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14537
14538 // Type "b" - filters existing completions
14539 cx.simulate_keystroke("b");
14540 cx.run_until_parked();
14541 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14542 cx.assert_editor_state("obj.abˇ");
14543 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14544
14545 // Type "c" - filters existing completions
14546 cx.simulate_keystroke("c");
14547 cx.run_until_parked();
14548 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14549 cx.assert_editor_state("obj.abcˇ");
14550 check_displayed_completions(vec!["abc"], &mut cx);
14551
14552 // Backspace to delete "c" - filters existing completions
14553 cx.update_editor(|editor, window, cx| {
14554 editor.backspace(&Backspace, window, cx);
14555 });
14556 cx.run_until_parked();
14557 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14558 cx.assert_editor_state("obj.abˇ");
14559 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14560
14561 // Moving cursor to the left dismisses menu.
14562 cx.update_editor(|editor, window, cx| {
14563 editor.move_left(&MoveLeft, window, cx);
14564 });
14565 cx.run_until_parked();
14566 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14567 cx.assert_editor_state("obj.aˇb");
14568 cx.update_editor(|editor, _, _| {
14569 assert_eq!(editor.context_menu_visible(), false);
14570 });
14571
14572 // Type "b" - new request
14573 cx.simulate_keystroke("b");
14574 let is_incomplete = false;
14575 handle_completion_request(
14576 "obj.<ab|>a",
14577 vec!["ab", "abc"],
14578 is_incomplete,
14579 counter.clone(),
14580 &mut cx,
14581 )
14582 .await;
14583 cx.run_until_parked();
14584 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14585 cx.assert_editor_state("obj.abˇb");
14586 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14587
14588 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14589 cx.update_editor(|editor, window, cx| {
14590 editor.backspace(&Backspace, window, cx);
14591 });
14592 let is_incomplete = false;
14593 handle_completion_request(
14594 "obj.<a|>b",
14595 vec!["a", "ab", "abc"],
14596 is_incomplete,
14597 counter.clone(),
14598 &mut cx,
14599 )
14600 .await;
14601 cx.run_until_parked();
14602 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14603 cx.assert_editor_state("obj.aˇb");
14604 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14605
14606 // Backspace to delete "a" - dismisses menu.
14607 cx.update_editor(|editor, window, cx| {
14608 editor.backspace(&Backspace, window, cx);
14609 });
14610 cx.run_until_parked();
14611 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14612 cx.assert_editor_state("obj.ˇb");
14613 cx.update_editor(|editor, _, _| {
14614 assert_eq!(editor.context_menu_visible(), false);
14615 });
14616}
14617
14618#[gpui::test]
14619async fn test_word_completion(cx: &mut TestAppContext) {
14620 let lsp_fetch_timeout_ms = 10;
14621 init_test(cx, |language_settings| {
14622 language_settings.defaults.completions = Some(CompletionSettingsContent {
14623 words_min_length: Some(0),
14624 lsp_fetch_timeout_ms: Some(10),
14625 lsp_insert_mode: Some(LspInsertMode::Insert),
14626 ..Default::default()
14627 });
14628 });
14629
14630 let mut cx = EditorLspTestContext::new_rust(
14631 lsp::ServerCapabilities {
14632 completion_provider: Some(lsp::CompletionOptions {
14633 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14634 ..lsp::CompletionOptions::default()
14635 }),
14636 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14637 ..lsp::ServerCapabilities::default()
14638 },
14639 cx,
14640 )
14641 .await;
14642
14643 let throttle_completions = Arc::new(AtomicBool::new(false));
14644
14645 let lsp_throttle_completions = throttle_completions.clone();
14646 let _completion_requests_handler =
14647 cx.lsp
14648 .server
14649 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14650 let lsp_throttle_completions = lsp_throttle_completions.clone();
14651 let cx = cx.clone();
14652 async move {
14653 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14654 cx.background_executor()
14655 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14656 .await;
14657 }
14658 Ok(Some(lsp::CompletionResponse::Array(vec![
14659 lsp::CompletionItem {
14660 label: "first".into(),
14661 ..lsp::CompletionItem::default()
14662 },
14663 lsp::CompletionItem {
14664 label: "last".into(),
14665 ..lsp::CompletionItem::default()
14666 },
14667 ])))
14668 }
14669 });
14670
14671 cx.set_state(indoc! {"
14672 oneˇ
14673 two
14674 three
14675 "});
14676 cx.simulate_keystroke(".");
14677 cx.executor().run_until_parked();
14678 cx.condition(|editor, _| editor.context_menu_visible())
14679 .await;
14680 cx.update_editor(|editor, window, cx| {
14681 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14682 {
14683 assert_eq!(
14684 completion_menu_entries(menu),
14685 &["first", "last"],
14686 "When LSP server is fast to reply, no fallback word completions are used"
14687 );
14688 } else {
14689 panic!("expected completion menu to be open");
14690 }
14691 editor.cancel(&Cancel, window, cx);
14692 });
14693 cx.executor().run_until_parked();
14694 cx.condition(|editor, _| !editor.context_menu_visible())
14695 .await;
14696
14697 throttle_completions.store(true, atomic::Ordering::Release);
14698 cx.simulate_keystroke(".");
14699 cx.executor()
14700 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14701 cx.executor().run_until_parked();
14702 cx.condition(|editor, _| editor.context_menu_visible())
14703 .await;
14704 cx.update_editor(|editor, _, _| {
14705 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14706 {
14707 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14708 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14709 } else {
14710 panic!("expected completion menu to be open");
14711 }
14712 });
14713}
14714
14715#[gpui::test]
14716async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14717 init_test(cx, |language_settings| {
14718 language_settings.defaults.completions = Some(CompletionSettingsContent {
14719 words: Some(WordsCompletionMode::Enabled),
14720 words_min_length: Some(0),
14721 lsp_insert_mode: Some(LspInsertMode::Insert),
14722 ..Default::default()
14723 });
14724 });
14725
14726 let mut cx = EditorLspTestContext::new_rust(
14727 lsp::ServerCapabilities {
14728 completion_provider: Some(lsp::CompletionOptions {
14729 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14730 ..lsp::CompletionOptions::default()
14731 }),
14732 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14733 ..lsp::ServerCapabilities::default()
14734 },
14735 cx,
14736 )
14737 .await;
14738
14739 let _completion_requests_handler =
14740 cx.lsp
14741 .server
14742 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14743 Ok(Some(lsp::CompletionResponse::Array(vec![
14744 lsp::CompletionItem {
14745 label: "first".into(),
14746 ..lsp::CompletionItem::default()
14747 },
14748 lsp::CompletionItem {
14749 label: "last".into(),
14750 ..lsp::CompletionItem::default()
14751 },
14752 ])))
14753 });
14754
14755 cx.set_state(indoc! {"ˇ
14756 first
14757 last
14758 second
14759 "});
14760 cx.simulate_keystroke(".");
14761 cx.executor().run_until_parked();
14762 cx.condition(|editor, _| editor.context_menu_visible())
14763 .await;
14764 cx.update_editor(|editor, _, _| {
14765 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14766 {
14767 assert_eq!(
14768 completion_menu_entries(menu),
14769 &["first", "last", "second"],
14770 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14771 );
14772 } else {
14773 panic!("expected completion menu to be open");
14774 }
14775 });
14776}
14777
14778#[gpui::test]
14779async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14780 init_test(cx, |language_settings| {
14781 language_settings.defaults.completions = Some(CompletionSettingsContent {
14782 words: Some(WordsCompletionMode::Disabled),
14783 words_min_length: Some(0),
14784 lsp_insert_mode: Some(LspInsertMode::Insert),
14785 ..Default::default()
14786 });
14787 });
14788
14789 let mut cx = EditorLspTestContext::new_rust(
14790 lsp::ServerCapabilities {
14791 completion_provider: Some(lsp::CompletionOptions {
14792 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14793 ..lsp::CompletionOptions::default()
14794 }),
14795 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14796 ..lsp::ServerCapabilities::default()
14797 },
14798 cx,
14799 )
14800 .await;
14801
14802 let _completion_requests_handler =
14803 cx.lsp
14804 .server
14805 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14806 panic!("LSP completions should not be queried when dealing with word completions")
14807 });
14808
14809 cx.set_state(indoc! {"ˇ
14810 first
14811 last
14812 second
14813 "});
14814 cx.update_editor(|editor, window, cx| {
14815 editor.show_word_completions(&ShowWordCompletions, window, cx);
14816 });
14817 cx.executor().run_until_parked();
14818 cx.condition(|editor, _| editor.context_menu_visible())
14819 .await;
14820 cx.update_editor(|editor, _, _| {
14821 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14822 {
14823 assert_eq!(
14824 completion_menu_entries(menu),
14825 &["first", "last", "second"],
14826 "`ShowWordCompletions` action should show word completions"
14827 );
14828 } else {
14829 panic!("expected completion menu to be open");
14830 }
14831 });
14832
14833 cx.simulate_keystroke("l");
14834 cx.executor().run_until_parked();
14835 cx.condition(|editor, _| editor.context_menu_visible())
14836 .await;
14837 cx.update_editor(|editor, _, _| {
14838 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14839 {
14840 assert_eq!(
14841 completion_menu_entries(menu),
14842 &["last"],
14843 "After showing word completions, further editing should filter them and not query the LSP"
14844 );
14845 } else {
14846 panic!("expected completion menu to be open");
14847 }
14848 });
14849}
14850
14851#[gpui::test]
14852async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14853 init_test(cx, |language_settings| {
14854 language_settings.defaults.completions = Some(CompletionSettingsContent {
14855 words_min_length: Some(0),
14856 lsp: Some(false),
14857 lsp_insert_mode: Some(LspInsertMode::Insert),
14858 ..Default::default()
14859 });
14860 });
14861
14862 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14863
14864 cx.set_state(indoc! {"ˇ
14865 0_usize
14866 let
14867 33
14868 4.5f32
14869 "});
14870 cx.update_editor(|editor, window, cx| {
14871 editor.show_completions(&ShowCompletions::default(), window, cx);
14872 });
14873 cx.executor().run_until_parked();
14874 cx.condition(|editor, _| editor.context_menu_visible())
14875 .await;
14876 cx.update_editor(|editor, window, cx| {
14877 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14878 {
14879 assert_eq!(
14880 completion_menu_entries(menu),
14881 &["let"],
14882 "With no digits in the completion query, no digits should be in the word completions"
14883 );
14884 } else {
14885 panic!("expected completion menu to be open");
14886 }
14887 editor.cancel(&Cancel, window, cx);
14888 });
14889
14890 cx.set_state(indoc! {"3ˇ
14891 0_usize
14892 let
14893 3
14894 33.35f32
14895 "});
14896 cx.update_editor(|editor, window, cx| {
14897 editor.show_completions(&ShowCompletions::default(), window, cx);
14898 });
14899 cx.executor().run_until_parked();
14900 cx.condition(|editor, _| editor.context_menu_visible())
14901 .await;
14902 cx.update_editor(|editor, _, _| {
14903 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14904 {
14905 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14906 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14907 } else {
14908 panic!("expected completion menu to be open");
14909 }
14910 });
14911}
14912
14913#[gpui::test]
14914async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14915 init_test(cx, |language_settings| {
14916 language_settings.defaults.completions = Some(CompletionSettingsContent {
14917 words: Some(WordsCompletionMode::Enabled),
14918 words_min_length: Some(3),
14919 lsp_insert_mode: Some(LspInsertMode::Insert),
14920 ..Default::default()
14921 });
14922 });
14923
14924 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14925 cx.set_state(indoc! {"ˇ
14926 wow
14927 wowen
14928 wowser
14929 "});
14930 cx.simulate_keystroke("w");
14931 cx.executor().run_until_parked();
14932 cx.update_editor(|editor, _, _| {
14933 if editor.context_menu.borrow_mut().is_some() {
14934 panic!(
14935 "expected completion menu to be hidden, as words completion threshold is not met"
14936 );
14937 }
14938 });
14939
14940 cx.update_editor(|editor, window, cx| {
14941 editor.show_word_completions(&ShowWordCompletions, window, cx);
14942 });
14943 cx.executor().run_until_parked();
14944 cx.update_editor(|editor, window, cx| {
14945 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14946 {
14947 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");
14948 } else {
14949 panic!("expected completion menu to be open after the word completions are called with an action");
14950 }
14951
14952 editor.cancel(&Cancel, window, cx);
14953 });
14954 cx.update_editor(|editor, _, _| {
14955 if editor.context_menu.borrow_mut().is_some() {
14956 panic!("expected completion menu to be hidden after canceling");
14957 }
14958 });
14959
14960 cx.simulate_keystroke("o");
14961 cx.executor().run_until_parked();
14962 cx.update_editor(|editor, _, _| {
14963 if editor.context_menu.borrow_mut().is_some() {
14964 panic!(
14965 "expected completion menu to be hidden, as words completion threshold is not met still"
14966 );
14967 }
14968 });
14969
14970 cx.simulate_keystroke("w");
14971 cx.executor().run_until_parked();
14972 cx.update_editor(|editor, _, _| {
14973 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14974 {
14975 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14976 } else {
14977 panic!("expected completion menu to be open after the word completions threshold is met");
14978 }
14979 });
14980}
14981
14982#[gpui::test]
14983async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14984 init_test(cx, |language_settings| {
14985 language_settings.defaults.completions = Some(CompletionSettingsContent {
14986 words: Some(WordsCompletionMode::Enabled),
14987 words_min_length: Some(0),
14988 lsp_insert_mode: Some(LspInsertMode::Insert),
14989 ..Default::default()
14990 });
14991 });
14992
14993 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14994 cx.update_editor(|editor, _, _| {
14995 editor.disable_word_completions();
14996 });
14997 cx.set_state(indoc! {"ˇ
14998 wow
14999 wowen
15000 wowser
15001 "});
15002 cx.simulate_keystroke("w");
15003 cx.executor().run_until_parked();
15004 cx.update_editor(|editor, _, _| {
15005 if editor.context_menu.borrow_mut().is_some() {
15006 panic!(
15007 "expected completion menu to be hidden, as words completion are disabled for this editor"
15008 );
15009 }
15010 });
15011
15012 cx.update_editor(|editor, window, cx| {
15013 editor.show_word_completions(&ShowWordCompletions, window, cx);
15014 });
15015 cx.executor().run_until_parked();
15016 cx.update_editor(|editor, _, _| {
15017 if editor.context_menu.borrow_mut().is_some() {
15018 panic!(
15019 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15020 );
15021 }
15022 });
15023}
15024
15025fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15026 let position = || lsp::Position {
15027 line: params.text_document_position.position.line,
15028 character: params.text_document_position.position.character,
15029 };
15030 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15031 range: lsp::Range {
15032 start: position(),
15033 end: position(),
15034 },
15035 new_text: text.to_string(),
15036 }))
15037}
15038
15039#[gpui::test]
15040async fn test_multiline_completion(cx: &mut TestAppContext) {
15041 init_test(cx, |_| {});
15042
15043 let fs = FakeFs::new(cx.executor());
15044 fs.insert_tree(
15045 path!("/a"),
15046 json!({
15047 "main.ts": "a",
15048 }),
15049 )
15050 .await;
15051
15052 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15053 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15054 let typescript_language = Arc::new(Language::new(
15055 LanguageConfig {
15056 name: "TypeScript".into(),
15057 matcher: LanguageMatcher {
15058 path_suffixes: vec!["ts".to_string()],
15059 ..LanguageMatcher::default()
15060 },
15061 line_comments: vec!["// ".into()],
15062 ..LanguageConfig::default()
15063 },
15064 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15065 ));
15066 language_registry.add(typescript_language.clone());
15067 let mut fake_servers = language_registry.register_fake_lsp(
15068 "TypeScript",
15069 FakeLspAdapter {
15070 capabilities: lsp::ServerCapabilities {
15071 completion_provider: Some(lsp::CompletionOptions {
15072 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15073 ..lsp::CompletionOptions::default()
15074 }),
15075 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15076 ..lsp::ServerCapabilities::default()
15077 },
15078 // Emulate vtsls label generation
15079 label_for_completion: Some(Box::new(|item, _| {
15080 let text = if let Some(description) = item
15081 .label_details
15082 .as_ref()
15083 .and_then(|label_details| label_details.description.as_ref())
15084 {
15085 format!("{} {}", item.label, description)
15086 } else if let Some(detail) = &item.detail {
15087 format!("{} {}", item.label, detail)
15088 } else {
15089 item.label.clone()
15090 };
15091 Some(language::CodeLabel::plain(text, None))
15092 })),
15093 ..FakeLspAdapter::default()
15094 },
15095 );
15096 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15097 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15098 let worktree_id = workspace
15099 .update(cx, |workspace, _window, cx| {
15100 workspace.project().update(cx, |project, cx| {
15101 project.worktrees(cx).next().unwrap().read(cx).id()
15102 })
15103 })
15104 .unwrap();
15105 let _buffer = project
15106 .update(cx, |project, cx| {
15107 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15108 })
15109 .await
15110 .unwrap();
15111 let editor = workspace
15112 .update(cx, |workspace, window, cx| {
15113 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15114 })
15115 .unwrap()
15116 .await
15117 .unwrap()
15118 .downcast::<Editor>()
15119 .unwrap();
15120 let fake_server = fake_servers.next().await.unwrap();
15121
15122 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15123 let multiline_label_2 = "a\nb\nc\n";
15124 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15125 let multiline_description = "d\ne\nf\n";
15126 let multiline_detail_2 = "g\nh\ni\n";
15127
15128 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15129 move |params, _| async move {
15130 Ok(Some(lsp::CompletionResponse::Array(vec![
15131 lsp::CompletionItem {
15132 label: multiline_label.to_string(),
15133 text_edit: gen_text_edit(¶ms, "new_text_1"),
15134 ..lsp::CompletionItem::default()
15135 },
15136 lsp::CompletionItem {
15137 label: "single line label 1".to_string(),
15138 detail: Some(multiline_detail.to_string()),
15139 text_edit: gen_text_edit(¶ms, "new_text_2"),
15140 ..lsp::CompletionItem::default()
15141 },
15142 lsp::CompletionItem {
15143 label: "single line label 2".to_string(),
15144 label_details: Some(lsp::CompletionItemLabelDetails {
15145 description: Some(multiline_description.to_string()),
15146 detail: None,
15147 }),
15148 text_edit: gen_text_edit(¶ms, "new_text_2"),
15149 ..lsp::CompletionItem::default()
15150 },
15151 lsp::CompletionItem {
15152 label: multiline_label_2.to_string(),
15153 detail: Some(multiline_detail_2.to_string()),
15154 text_edit: gen_text_edit(¶ms, "new_text_3"),
15155 ..lsp::CompletionItem::default()
15156 },
15157 lsp::CompletionItem {
15158 label: "Label with many spaces and \t but without newlines".to_string(),
15159 detail: Some(
15160 "Details with many spaces and \t but without newlines".to_string(),
15161 ),
15162 text_edit: gen_text_edit(¶ms, "new_text_4"),
15163 ..lsp::CompletionItem::default()
15164 },
15165 ])))
15166 },
15167 );
15168
15169 editor.update_in(cx, |editor, window, cx| {
15170 cx.focus_self(window);
15171 editor.move_to_end(&MoveToEnd, window, cx);
15172 editor.handle_input(".", window, cx);
15173 });
15174 cx.run_until_parked();
15175 completion_handle.next().await.unwrap();
15176
15177 editor.update(cx, |editor, _| {
15178 assert!(editor.context_menu_visible());
15179 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15180 {
15181 let completion_labels = menu
15182 .completions
15183 .borrow()
15184 .iter()
15185 .map(|c| c.label.text.clone())
15186 .collect::<Vec<_>>();
15187 assert_eq!(
15188 completion_labels,
15189 &[
15190 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15191 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15192 "single line label 2 d e f ",
15193 "a b c g h i ",
15194 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15195 ],
15196 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15197 );
15198
15199 for completion in menu
15200 .completions
15201 .borrow()
15202 .iter() {
15203 assert_eq!(
15204 completion.label.filter_range,
15205 0..completion.label.text.len(),
15206 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15207 );
15208 }
15209 } else {
15210 panic!("expected completion menu to be open");
15211 }
15212 });
15213}
15214
15215#[gpui::test]
15216async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15217 init_test(cx, |_| {});
15218 let mut cx = EditorLspTestContext::new_rust(
15219 lsp::ServerCapabilities {
15220 completion_provider: Some(lsp::CompletionOptions {
15221 trigger_characters: Some(vec![".".to_string()]),
15222 ..Default::default()
15223 }),
15224 ..Default::default()
15225 },
15226 cx,
15227 )
15228 .await;
15229 cx.lsp
15230 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15231 Ok(Some(lsp::CompletionResponse::Array(vec![
15232 lsp::CompletionItem {
15233 label: "first".into(),
15234 ..Default::default()
15235 },
15236 lsp::CompletionItem {
15237 label: "last".into(),
15238 ..Default::default()
15239 },
15240 ])))
15241 });
15242 cx.set_state("variableˇ");
15243 cx.simulate_keystroke(".");
15244 cx.executor().run_until_parked();
15245
15246 cx.update_editor(|editor, _, _| {
15247 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15248 {
15249 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15250 } else {
15251 panic!("expected completion menu to be open");
15252 }
15253 });
15254
15255 cx.update_editor(|editor, window, cx| {
15256 editor.move_page_down(&MovePageDown::default(), window, cx);
15257 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15258 {
15259 assert!(
15260 menu.selected_item == 1,
15261 "expected PageDown to select the last item from the context menu"
15262 );
15263 } else {
15264 panic!("expected completion menu to stay open after PageDown");
15265 }
15266 });
15267
15268 cx.update_editor(|editor, window, cx| {
15269 editor.move_page_up(&MovePageUp::default(), window, cx);
15270 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15271 {
15272 assert!(
15273 menu.selected_item == 0,
15274 "expected PageUp to select the first item from the context menu"
15275 );
15276 } else {
15277 panic!("expected completion menu to stay open after PageUp");
15278 }
15279 });
15280}
15281
15282#[gpui::test]
15283async fn test_as_is_completions(cx: &mut TestAppContext) {
15284 init_test(cx, |_| {});
15285 let mut cx = EditorLspTestContext::new_rust(
15286 lsp::ServerCapabilities {
15287 completion_provider: Some(lsp::CompletionOptions {
15288 ..Default::default()
15289 }),
15290 ..Default::default()
15291 },
15292 cx,
15293 )
15294 .await;
15295 cx.lsp
15296 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15297 Ok(Some(lsp::CompletionResponse::Array(vec![
15298 lsp::CompletionItem {
15299 label: "unsafe".into(),
15300 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15301 range: lsp::Range {
15302 start: lsp::Position {
15303 line: 1,
15304 character: 2,
15305 },
15306 end: lsp::Position {
15307 line: 1,
15308 character: 3,
15309 },
15310 },
15311 new_text: "unsafe".to_string(),
15312 })),
15313 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15314 ..Default::default()
15315 },
15316 ])))
15317 });
15318 cx.set_state("fn a() {}\n nˇ");
15319 cx.executor().run_until_parked();
15320 cx.update_editor(|editor, window, cx| {
15321 editor.show_completions(
15322 &ShowCompletions {
15323 trigger: Some("\n".into()),
15324 snippets_only: false,
15325 },
15326 window,
15327 cx,
15328 );
15329 });
15330 cx.executor().run_until_parked();
15331
15332 cx.update_editor(|editor, window, cx| {
15333 editor.confirm_completion(&Default::default(), window, cx)
15334 });
15335 cx.executor().run_until_parked();
15336 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15337}
15338
15339#[gpui::test]
15340async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15341 init_test(cx, |_| {});
15342 let language =
15343 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15344 let mut cx = EditorLspTestContext::new(
15345 language,
15346 lsp::ServerCapabilities {
15347 completion_provider: Some(lsp::CompletionOptions {
15348 ..lsp::CompletionOptions::default()
15349 }),
15350 ..lsp::ServerCapabilities::default()
15351 },
15352 cx,
15353 )
15354 .await;
15355
15356 cx.set_state(
15357 "#ifndef BAR_H
15358#define BAR_H
15359
15360#include <stdbool.h>
15361
15362int fn_branch(bool do_branch1, bool do_branch2);
15363
15364#endif // BAR_H
15365ˇ",
15366 );
15367 cx.executor().run_until_parked();
15368 cx.update_editor(|editor, window, cx| {
15369 editor.handle_input("#", window, cx);
15370 });
15371 cx.executor().run_until_parked();
15372 cx.update_editor(|editor, window, cx| {
15373 editor.handle_input("i", window, cx);
15374 });
15375 cx.executor().run_until_parked();
15376 cx.update_editor(|editor, window, cx| {
15377 editor.handle_input("n", window, cx);
15378 });
15379 cx.executor().run_until_parked();
15380 cx.assert_editor_state(
15381 "#ifndef BAR_H
15382#define BAR_H
15383
15384#include <stdbool.h>
15385
15386int fn_branch(bool do_branch1, bool do_branch2);
15387
15388#endif // BAR_H
15389#inˇ",
15390 );
15391
15392 cx.lsp
15393 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15394 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15395 is_incomplete: false,
15396 item_defaults: None,
15397 items: vec![lsp::CompletionItem {
15398 kind: Some(lsp::CompletionItemKind::SNIPPET),
15399 label_details: Some(lsp::CompletionItemLabelDetails {
15400 detail: Some("header".to_string()),
15401 description: None,
15402 }),
15403 label: " include".to_string(),
15404 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15405 range: lsp::Range {
15406 start: lsp::Position {
15407 line: 8,
15408 character: 1,
15409 },
15410 end: lsp::Position {
15411 line: 8,
15412 character: 1,
15413 },
15414 },
15415 new_text: "include \"$0\"".to_string(),
15416 })),
15417 sort_text: Some("40b67681include".to_string()),
15418 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15419 filter_text: Some("include".to_string()),
15420 insert_text: Some("include \"$0\"".to_string()),
15421 ..lsp::CompletionItem::default()
15422 }],
15423 })))
15424 });
15425 cx.update_editor(|editor, window, cx| {
15426 editor.show_completions(
15427 &ShowCompletions {
15428 trigger: None,
15429 snippets_only: false,
15430 },
15431 window,
15432 cx,
15433 );
15434 });
15435 cx.executor().run_until_parked();
15436 cx.update_editor(|editor, window, cx| {
15437 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15438 });
15439 cx.executor().run_until_parked();
15440 cx.assert_editor_state(
15441 "#ifndef BAR_H
15442#define BAR_H
15443
15444#include <stdbool.h>
15445
15446int fn_branch(bool do_branch1, bool do_branch2);
15447
15448#endif // BAR_H
15449#include \"ˇ\"",
15450 );
15451
15452 cx.lsp
15453 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15454 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15455 is_incomplete: true,
15456 item_defaults: None,
15457 items: vec![lsp::CompletionItem {
15458 kind: Some(lsp::CompletionItemKind::FILE),
15459 label: "AGL/".to_string(),
15460 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15461 range: lsp::Range {
15462 start: lsp::Position {
15463 line: 8,
15464 character: 10,
15465 },
15466 end: lsp::Position {
15467 line: 8,
15468 character: 11,
15469 },
15470 },
15471 new_text: "AGL/".to_string(),
15472 })),
15473 sort_text: Some("40b67681AGL/".to_string()),
15474 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15475 filter_text: Some("AGL/".to_string()),
15476 insert_text: Some("AGL/".to_string()),
15477 ..lsp::CompletionItem::default()
15478 }],
15479 })))
15480 });
15481 cx.update_editor(|editor, window, cx| {
15482 editor.show_completions(
15483 &ShowCompletions {
15484 trigger: None,
15485 snippets_only: false,
15486 },
15487 window,
15488 cx,
15489 );
15490 });
15491 cx.executor().run_until_parked();
15492 cx.update_editor(|editor, window, cx| {
15493 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15494 });
15495 cx.executor().run_until_parked();
15496 cx.assert_editor_state(
15497 r##"#ifndef BAR_H
15498#define BAR_H
15499
15500#include <stdbool.h>
15501
15502int fn_branch(bool do_branch1, bool do_branch2);
15503
15504#endif // BAR_H
15505#include "AGL/ˇ"##,
15506 );
15507
15508 cx.update_editor(|editor, window, cx| {
15509 editor.handle_input("\"", window, cx);
15510 });
15511 cx.executor().run_until_parked();
15512 cx.assert_editor_state(
15513 r##"#ifndef BAR_H
15514#define BAR_H
15515
15516#include <stdbool.h>
15517
15518int fn_branch(bool do_branch1, bool do_branch2);
15519
15520#endif // BAR_H
15521#include "AGL/"ˇ"##,
15522 );
15523}
15524
15525#[gpui::test]
15526async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15527 init_test(cx, |_| {});
15528
15529 let mut cx = EditorLspTestContext::new_rust(
15530 lsp::ServerCapabilities {
15531 completion_provider: Some(lsp::CompletionOptions {
15532 trigger_characters: Some(vec![".".to_string()]),
15533 resolve_provider: Some(true),
15534 ..Default::default()
15535 }),
15536 ..Default::default()
15537 },
15538 cx,
15539 )
15540 .await;
15541
15542 cx.set_state("fn main() { let a = 2ˇ; }");
15543 cx.simulate_keystroke(".");
15544 let completion_item = lsp::CompletionItem {
15545 label: "Some".into(),
15546 kind: Some(lsp::CompletionItemKind::SNIPPET),
15547 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15548 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15549 kind: lsp::MarkupKind::Markdown,
15550 value: "```rust\nSome(2)\n```".to_string(),
15551 })),
15552 deprecated: Some(false),
15553 sort_text: Some("Some".to_string()),
15554 filter_text: Some("Some".to_string()),
15555 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15556 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15557 range: lsp::Range {
15558 start: lsp::Position {
15559 line: 0,
15560 character: 22,
15561 },
15562 end: lsp::Position {
15563 line: 0,
15564 character: 22,
15565 },
15566 },
15567 new_text: "Some(2)".to_string(),
15568 })),
15569 additional_text_edits: Some(vec![lsp::TextEdit {
15570 range: lsp::Range {
15571 start: lsp::Position {
15572 line: 0,
15573 character: 20,
15574 },
15575 end: lsp::Position {
15576 line: 0,
15577 character: 22,
15578 },
15579 },
15580 new_text: "".to_string(),
15581 }]),
15582 ..Default::default()
15583 };
15584
15585 let closure_completion_item = completion_item.clone();
15586 let counter = Arc::new(AtomicUsize::new(0));
15587 let counter_clone = counter.clone();
15588 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15589 let task_completion_item = closure_completion_item.clone();
15590 counter_clone.fetch_add(1, atomic::Ordering::Release);
15591 async move {
15592 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15593 is_incomplete: true,
15594 item_defaults: None,
15595 items: vec![task_completion_item],
15596 })))
15597 }
15598 });
15599
15600 cx.condition(|editor, _| editor.context_menu_visible())
15601 .await;
15602 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15603 assert!(request.next().await.is_some());
15604 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15605
15606 cx.simulate_keystrokes("S o m");
15607 cx.condition(|editor, _| editor.context_menu_visible())
15608 .await;
15609 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15610 assert!(request.next().await.is_some());
15611 assert!(request.next().await.is_some());
15612 assert!(request.next().await.is_some());
15613 request.close();
15614 assert!(request.next().await.is_none());
15615 assert_eq!(
15616 counter.load(atomic::Ordering::Acquire),
15617 4,
15618 "With the completions menu open, only one LSP request should happen per input"
15619 );
15620}
15621
15622#[gpui::test]
15623async fn test_toggle_comment(cx: &mut TestAppContext) {
15624 init_test(cx, |_| {});
15625 let mut cx = EditorTestContext::new(cx).await;
15626 let language = Arc::new(Language::new(
15627 LanguageConfig {
15628 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15629 ..Default::default()
15630 },
15631 Some(tree_sitter_rust::LANGUAGE.into()),
15632 ));
15633 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15634
15635 // If multiple selections intersect a line, the line is only toggled once.
15636 cx.set_state(indoc! {"
15637 fn a() {
15638 «//b();
15639 ˇ»// «c();
15640 //ˇ» d();
15641 }
15642 "});
15643
15644 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15645
15646 cx.assert_editor_state(indoc! {"
15647 fn a() {
15648 «b();
15649 c();
15650 ˇ» d();
15651 }
15652 "});
15653
15654 // The comment prefix is inserted at the same column for every line in a
15655 // selection.
15656 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15657
15658 cx.assert_editor_state(indoc! {"
15659 fn a() {
15660 // «b();
15661 // c();
15662 ˇ»// d();
15663 }
15664 "});
15665
15666 // If a selection ends at the beginning of a line, that line is not toggled.
15667 cx.set_selections_state(indoc! {"
15668 fn a() {
15669 // b();
15670 «// c();
15671 ˇ» // d();
15672 }
15673 "});
15674
15675 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15676
15677 cx.assert_editor_state(indoc! {"
15678 fn a() {
15679 // b();
15680 «c();
15681 ˇ» // d();
15682 }
15683 "});
15684
15685 // If a selection span a single line and is empty, the line is toggled.
15686 cx.set_state(indoc! {"
15687 fn a() {
15688 a();
15689 b();
15690 ˇ
15691 }
15692 "});
15693
15694 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15695
15696 cx.assert_editor_state(indoc! {"
15697 fn a() {
15698 a();
15699 b();
15700 //•ˇ
15701 }
15702 "});
15703
15704 // If a selection span multiple lines, empty lines are not toggled.
15705 cx.set_state(indoc! {"
15706 fn a() {
15707 «a();
15708
15709 c();ˇ»
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 // «a();
15718
15719 // c();ˇ»
15720 }
15721 "});
15722
15723 // If a selection includes multiple comment prefixes, all lines are uncommented.
15724 cx.set_state(indoc! {"
15725 fn a() {
15726 «// a();
15727 /// b();
15728 //! c();ˇ»
15729 }
15730 "});
15731
15732 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15733
15734 cx.assert_editor_state(indoc! {"
15735 fn a() {
15736 «a();
15737 b();
15738 c();ˇ»
15739 }
15740 "});
15741}
15742
15743#[gpui::test]
15744async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15745 init_test(cx, |_| {});
15746 let mut cx = EditorTestContext::new(cx).await;
15747 let language = Arc::new(Language::new(
15748 LanguageConfig {
15749 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15750 ..Default::default()
15751 },
15752 Some(tree_sitter_rust::LANGUAGE.into()),
15753 ));
15754 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15755
15756 let toggle_comments = &ToggleComments {
15757 advance_downwards: false,
15758 ignore_indent: true,
15759 };
15760
15761 // If multiple selections intersect a line, the line is only toggled once.
15762 cx.set_state(indoc! {"
15763 fn a() {
15764 // «b();
15765 // c();
15766 // ˇ» d();
15767 }
15768 "});
15769
15770 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15771
15772 cx.assert_editor_state(indoc! {"
15773 fn a() {
15774 «b();
15775 c();
15776 ˇ» d();
15777 }
15778 "});
15779
15780 // The comment prefix is inserted at the beginning of each line
15781 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15782
15783 cx.assert_editor_state(indoc! {"
15784 fn a() {
15785 // «b();
15786 // c();
15787 // ˇ» d();
15788 }
15789 "});
15790
15791 // If a selection ends at the beginning of a line, that line is not toggled.
15792 cx.set_selections_state(indoc! {"
15793 fn a() {
15794 // b();
15795 // «c();
15796 ˇ»// d();
15797 }
15798 "});
15799
15800 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15801
15802 cx.assert_editor_state(indoc! {"
15803 fn a() {
15804 // b();
15805 «c();
15806 ˇ»// d();
15807 }
15808 "});
15809
15810 // If a selection span a single line and is empty, the line is toggled.
15811 cx.set_state(indoc! {"
15812 fn a() {
15813 a();
15814 b();
15815 ˇ
15816 }
15817 "});
15818
15819 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15820
15821 cx.assert_editor_state(indoc! {"
15822 fn a() {
15823 a();
15824 b();
15825 //ˇ
15826 }
15827 "});
15828
15829 // If a selection span multiple lines, empty lines are not toggled.
15830 cx.set_state(indoc! {"
15831 fn a() {
15832 «a();
15833
15834 c();ˇ»
15835 }
15836 "});
15837
15838 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15839
15840 cx.assert_editor_state(indoc! {"
15841 fn a() {
15842 // «a();
15843
15844 // c();ˇ»
15845 }
15846 "});
15847
15848 // If a selection includes multiple comment prefixes, all lines are uncommented.
15849 cx.set_state(indoc! {"
15850 fn a() {
15851 // «a();
15852 /// b();
15853 //! c();ˇ»
15854 }
15855 "});
15856
15857 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15858
15859 cx.assert_editor_state(indoc! {"
15860 fn a() {
15861 «a();
15862 b();
15863 c();ˇ»
15864 }
15865 "});
15866}
15867
15868#[gpui::test]
15869async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15870 init_test(cx, |_| {});
15871
15872 let language = Arc::new(Language::new(
15873 LanguageConfig {
15874 line_comments: vec!["// ".into()],
15875 ..Default::default()
15876 },
15877 Some(tree_sitter_rust::LANGUAGE.into()),
15878 ));
15879
15880 let mut cx = EditorTestContext::new(cx).await;
15881
15882 cx.language_registry().add(language.clone());
15883 cx.update_buffer(|buffer, cx| {
15884 buffer.set_language(Some(language), cx);
15885 });
15886
15887 let toggle_comments = &ToggleComments {
15888 advance_downwards: true,
15889 ignore_indent: false,
15890 };
15891
15892 // Single cursor on one line -> advance
15893 // Cursor moves horizontally 3 characters as well on non-blank line
15894 cx.set_state(indoc!(
15895 "fn a() {
15896 ˇdog();
15897 cat();
15898 }"
15899 ));
15900 cx.update_editor(|editor, window, cx| {
15901 editor.toggle_comments(toggle_comments, window, cx);
15902 });
15903 cx.assert_editor_state(indoc!(
15904 "fn a() {
15905 // dog();
15906 catˇ();
15907 }"
15908 ));
15909
15910 // Single selection on one line -> don't advance
15911 cx.set_state(indoc!(
15912 "fn a() {
15913 «dog()ˇ»;
15914 cat();
15915 }"
15916 ));
15917 cx.update_editor(|editor, window, cx| {
15918 editor.toggle_comments(toggle_comments, window, cx);
15919 });
15920 cx.assert_editor_state(indoc!(
15921 "fn a() {
15922 // «dog()ˇ»;
15923 cat();
15924 }"
15925 ));
15926
15927 // Multiple cursors on one line -> advance
15928 cx.set_state(indoc!(
15929 "fn a() {
15930 ˇdˇog();
15931 cat();
15932 }"
15933 ));
15934 cx.update_editor(|editor, window, cx| {
15935 editor.toggle_comments(toggle_comments, window, cx);
15936 });
15937 cx.assert_editor_state(indoc!(
15938 "fn a() {
15939 // dog();
15940 catˇ(ˇ);
15941 }"
15942 ));
15943
15944 // Multiple cursors on one line, with selection -> don't advance
15945 cx.set_state(indoc!(
15946 "fn a() {
15947 ˇdˇog«()ˇ»;
15948 cat();
15949 }"
15950 ));
15951 cx.update_editor(|editor, window, cx| {
15952 editor.toggle_comments(toggle_comments, window, cx);
15953 });
15954 cx.assert_editor_state(indoc!(
15955 "fn a() {
15956 // ˇdˇog«()ˇ»;
15957 cat();
15958 }"
15959 ));
15960
15961 // Single cursor on one line -> advance
15962 // Cursor moves to column 0 on blank line
15963 cx.set_state(indoc!(
15964 "fn a() {
15965 ˇdog();
15966
15967 cat();
15968 }"
15969 ));
15970 cx.update_editor(|editor, window, cx| {
15971 editor.toggle_comments(toggle_comments, window, cx);
15972 });
15973 cx.assert_editor_state(indoc!(
15974 "fn a() {
15975 // dog();
15976 ˇ
15977 cat();
15978 }"
15979 ));
15980
15981 // Single cursor on one line -> advance
15982 // Cursor starts and ends at column 0
15983 cx.set_state(indoc!(
15984 "fn a() {
15985 ˇ dog();
15986 cat();
15987 }"
15988 ));
15989 cx.update_editor(|editor, window, cx| {
15990 editor.toggle_comments(toggle_comments, window, cx);
15991 });
15992 cx.assert_editor_state(indoc!(
15993 "fn a() {
15994 // dog();
15995 ˇ cat();
15996 }"
15997 ));
15998}
15999
16000#[gpui::test]
16001async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16002 init_test(cx, |_| {});
16003
16004 let mut cx = EditorTestContext::new(cx).await;
16005
16006 let html_language = Arc::new(
16007 Language::new(
16008 LanguageConfig {
16009 name: "HTML".into(),
16010 block_comment: Some(BlockCommentConfig {
16011 start: "<!-- ".into(),
16012 prefix: "".into(),
16013 end: " -->".into(),
16014 tab_size: 0,
16015 }),
16016 ..Default::default()
16017 },
16018 Some(tree_sitter_html::LANGUAGE.into()),
16019 )
16020 .with_injection_query(
16021 r#"
16022 (script_element
16023 (raw_text) @injection.content
16024 (#set! injection.language "javascript"))
16025 "#,
16026 )
16027 .unwrap(),
16028 );
16029
16030 let javascript_language = Arc::new(Language::new(
16031 LanguageConfig {
16032 name: "JavaScript".into(),
16033 line_comments: vec!["// ".into()],
16034 ..Default::default()
16035 },
16036 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16037 ));
16038
16039 cx.language_registry().add(html_language.clone());
16040 cx.language_registry().add(javascript_language);
16041 cx.update_buffer(|buffer, cx| {
16042 buffer.set_language(Some(html_language), cx);
16043 });
16044
16045 // Toggle comments for empty selections
16046 cx.set_state(
16047 &r#"
16048 <p>A</p>ˇ
16049 <p>B</p>ˇ
16050 <p>C</p>ˇ
16051 "#
16052 .unindent(),
16053 );
16054 cx.update_editor(|editor, window, cx| {
16055 editor.toggle_comments(&ToggleComments::default(), window, cx)
16056 });
16057 cx.assert_editor_state(
16058 &r#"
16059 <!-- <p>A</p>ˇ -->
16060 <!-- <p>B</p>ˇ -->
16061 <!-- <p>C</p>ˇ -->
16062 "#
16063 .unindent(),
16064 );
16065 cx.update_editor(|editor, window, cx| {
16066 editor.toggle_comments(&ToggleComments::default(), window, cx)
16067 });
16068 cx.assert_editor_state(
16069 &r#"
16070 <p>A</p>ˇ
16071 <p>B</p>ˇ
16072 <p>C</p>ˇ
16073 "#
16074 .unindent(),
16075 );
16076
16077 // Toggle comments for mixture of empty and non-empty selections, where
16078 // multiple selections occupy a given line.
16079 cx.set_state(
16080 &r#"
16081 <p>A«</p>
16082 <p>ˇ»B</p>ˇ
16083 <p>C«</p>
16084 <p>ˇ»D</p>ˇ
16085 "#
16086 .unindent(),
16087 );
16088
16089 cx.update_editor(|editor, window, cx| {
16090 editor.toggle_comments(&ToggleComments::default(), window, cx)
16091 });
16092 cx.assert_editor_state(
16093 &r#"
16094 <!-- <p>A«</p>
16095 <p>ˇ»B</p>ˇ -->
16096 <!-- <p>C«</p>
16097 <p>ˇ»D</p>ˇ -->
16098 "#
16099 .unindent(),
16100 );
16101 cx.update_editor(|editor, window, cx| {
16102 editor.toggle_comments(&ToggleComments::default(), window, cx)
16103 });
16104 cx.assert_editor_state(
16105 &r#"
16106 <p>A«</p>
16107 <p>ˇ»B</p>ˇ
16108 <p>C«</p>
16109 <p>ˇ»D</p>ˇ
16110 "#
16111 .unindent(),
16112 );
16113
16114 // Toggle comments when different languages are active for different
16115 // selections.
16116 cx.set_state(
16117 &r#"
16118 ˇ<script>
16119 ˇvar x = new Y();
16120 ˇ</script>
16121 "#
16122 .unindent(),
16123 );
16124 cx.executor().run_until_parked();
16125 cx.update_editor(|editor, window, cx| {
16126 editor.toggle_comments(&ToggleComments::default(), window, cx)
16127 });
16128 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16129 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16130 cx.assert_editor_state(
16131 &r#"
16132 <!-- ˇ<script> -->
16133 // ˇvar x = new Y();
16134 <!-- ˇ</script> -->
16135 "#
16136 .unindent(),
16137 );
16138}
16139
16140#[gpui::test]
16141fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16142 init_test(cx, |_| {});
16143
16144 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16145 let multibuffer = cx.new(|cx| {
16146 let mut multibuffer = MultiBuffer::new(ReadWrite);
16147 multibuffer.push_excerpts(
16148 buffer.clone(),
16149 [
16150 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16151 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16152 ],
16153 cx,
16154 );
16155 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16156 multibuffer
16157 });
16158
16159 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16160 editor.update_in(cx, |editor, window, cx| {
16161 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16162 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16163 s.select_ranges([
16164 Point::new(0, 0)..Point::new(0, 0),
16165 Point::new(1, 0)..Point::new(1, 0),
16166 ])
16167 });
16168
16169 editor.handle_input("X", window, cx);
16170 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16171 assert_eq!(
16172 editor.selections.ranges(&editor.display_snapshot(cx)),
16173 [
16174 Point::new(0, 1)..Point::new(0, 1),
16175 Point::new(1, 1)..Point::new(1, 1),
16176 ]
16177 );
16178
16179 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16180 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16181 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16182 });
16183 editor.backspace(&Default::default(), window, cx);
16184 assert_eq!(editor.text(cx), "Xa\nbbb");
16185 assert_eq!(
16186 editor.selections.ranges(&editor.display_snapshot(cx)),
16187 [Point::new(1, 0)..Point::new(1, 0)]
16188 );
16189
16190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16191 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16192 });
16193 editor.backspace(&Default::default(), window, cx);
16194 assert_eq!(editor.text(cx), "X\nbb");
16195 assert_eq!(
16196 editor.selections.ranges(&editor.display_snapshot(cx)),
16197 [Point::new(0, 1)..Point::new(0, 1)]
16198 );
16199 });
16200}
16201
16202#[gpui::test]
16203fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16204 init_test(cx, |_| {});
16205
16206 let markers = vec![('[', ']').into(), ('(', ')').into()];
16207 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16208 indoc! {"
16209 [aaaa
16210 (bbbb]
16211 cccc)",
16212 },
16213 markers.clone(),
16214 );
16215 let excerpt_ranges = markers.into_iter().map(|marker| {
16216 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16217 ExcerptRange::new(context)
16218 });
16219 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16220 let multibuffer = cx.new(|cx| {
16221 let mut multibuffer = MultiBuffer::new(ReadWrite);
16222 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16223 multibuffer
16224 });
16225
16226 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16227 editor.update_in(cx, |editor, window, cx| {
16228 let (expected_text, selection_ranges) = marked_text_ranges(
16229 indoc! {"
16230 aaaa
16231 bˇbbb
16232 bˇbbˇb
16233 cccc"
16234 },
16235 true,
16236 );
16237 assert_eq!(editor.text(cx), expected_text);
16238 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16239 s.select_ranges(selection_ranges)
16240 });
16241
16242 editor.handle_input("X", window, cx);
16243
16244 let (expected_text, expected_selections) = marked_text_ranges(
16245 indoc! {"
16246 aaaa
16247 bXˇbbXb
16248 bXˇbbXˇb
16249 cccc"
16250 },
16251 false,
16252 );
16253 assert_eq!(editor.text(cx), expected_text);
16254 assert_eq!(
16255 editor.selections.ranges(&editor.display_snapshot(cx)),
16256 expected_selections
16257 );
16258
16259 editor.newline(&Newline, window, cx);
16260 let (expected_text, expected_selections) = marked_text_ranges(
16261 indoc! {"
16262 aaaa
16263 bX
16264 ˇbbX
16265 b
16266 bX
16267 ˇbbX
16268 ˇb
16269 cccc"
16270 },
16271 false,
16272 );
16273 assert_eq!(editor.text(cx), expected_text);
16274 assert_eq!(
16275 editor.selections.ranges(&editor.display_snapshot(cx)),
16276 expected_selections
16277 );
16278 });
16279}
16280
16281#[gpui::test]
16282fn test_refresh_selections(cx: &mut TestAppContext) {
16283 init_test(cx, |_| {});
16284
16285 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16286 let mut excerpt1_id = None;
16287 let multibuffer = cx.new(|cx| {
16288 let mut multibuffer = MultiBuffer::new(ReadWrite);
16289 excerpt1_id = multibuffer
16290 .push_excerpts(
16291 buffer.clone(),
16292 [
16293 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16294 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16295 ],
16296 cx,
16297 )
16298 .into_iter()
16299 .next();
16300 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16301 multibuffer
16302 });
16303
16304 let editor = cx.add_window(|window, cx| {
16305 let mut editor = build_editor(multibuffer.clone(), window, cx);
16306 let snapshot = editor.snapshot(window, cx);
16307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16308 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16309 });
16310 editor.begin_selection(
16311 Point::new(2, 1).to_display_point(&snapshot),
16312 true,
16313 1,
16314 window,
16315 cx,
16316 );
16317 assert_eq!(
16318 editor.selections.ranges(&editor.display_snapshot(cx)),
16319 [
16320 Point::new(1, 3)..Point::new(1, 3),
16321 Point::new(2, 1)..Point::new(2, 1),
16322 ]
16323 );
16324 editor
16325 });
16326
16327 // Refreshing selections is a no-op when excerpts haven't changed.
16328 _ = editor.update(cx, |editor, window, cx| {
16329 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16330 assert_eq!(
16331 editor.selections.ranges(&editor.display_snapshot(cx)),
16332 [
16333 Point::new(1, 3)..Point::new(1, 3),
16334 Point::new(2, 1)..Point::new(2, 1),
16335 ]
16336 );
16337 });
16338
16339 multibuffer.update(cx, |multibuffer, cx| {
16340 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16341 });
16342 _ = editor.update(cx, |editor, window, cx| {
16343 // Removing an excerpt causes the first selection to become degenerate.
16344 assert_eq!(
16345 editor.selections.ranges(&editor.display_snapshot(cx)),
16346 [
16347 Point::new(0, 0)..Point::new(0, 0),
16348 Point::new(0, 1)..Point::new(0, 1)
16349 ]
16350 );
16351
16352 // Refreshing selections will relocate the first selection to the original buffer
16353 // location.
16354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16355 assert_eq!(
16356 editor.selections.ranges(&editor.display_snapshot(cx)),
16357 [
16358 Point::new(0, 1)..Point::new(0, 1),
16359 Point::new(0, 3)..Point::new(0, 3)
16360 ]
16361 );
16362 assert!(editor.selections.pending_anchor().is_some());
16363 });
16364}
16365
16366#[gpui::test]
16367fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16368 init_test(cx, |_| {});
16369
16370 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16371 let mut excerpt1_id = None;
16372 let multibuffer = cx.new(|cx| {
16373 let mut multibuffer = MultiBuffer::new(ReadWrite);
16374 excerpt1_id = multibuffer
16375 .push_excerpts(
16376 buffer.clone(),
16377 [
16378 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16379 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16380 ],
16381 cx,
16382 )
16383 .into_iter()
16384 .next();
16385 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16386 multibuffer
16387 });
16388
16389 let editor = cx.add_window(|window, cx| {
16390 let mut editor = build_editor(multibuffer.clone(), window, cx);
16391 let snapshot = editor.snapshot(window, cx);
16392 editor.begin_selection(
16393 Point::new(1, 3).to_display_point(&snapshot),
16394 false,
16395 1,
16396 window,
16397 cx,
16398 );
16399 assert_eq!(
16400 editor.selections.ranges(&editor.display_snapshot(cx)),
16401 [Point::new(1, 3)..Point::new(1, 3)]
16402 );
16403 editor
16404 });
16405
16406 multibuffer.update(cx, |multibuffer, cx| {
16407 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16408 });
16409 _ = editor.update(cx, |editor, window, cx| {
16410 assert_eq!(
16411 editor.selections.ranges(&editor.display_snapshot(cx)),
16412 [Point::new(0, 0)..Point::new(0, 0)]
16413 );
16414
16415 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16417 assert_eq!(
16418 editor.selections.ranges(&editor.display_snapshot(cx)),
16419 [Point::new(0, 3)..Point::new(0, 3)]
16420 );
16421 assert!(editor.selections.pending_anchor().is_some());
16422 });
16423}
16424
16425#[gpui::test]
16426async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16427 init_test(cx, |_| {});
16428
16429 let language = Arc::new(
16430 Language::new(
16431 LanguageConfig {
16432 brackets: BracketPairConfig {
16433 pairs: vec![
16434 BracketPair {
16435 start: "{".to_string(),
16436 end: "}".to_string(),
16437 close: true,
16438 surround: true,
16439 newline: true,
16440 },
16441 BracketPair {
16442 start: "/* ".to_string(),
16443 end: " */".to_string(),
16444 close: true,
16445 surround: true,
16446 newline: true,
16447 },
16448 ],
16449 ..Default::default()
16450 },
16451 ..Default::default()
16452 },
16453 Some(tree_sitter_rust::LANGUAGE.into()),
16454 )
16455 .with_indents_query("")
16456 .unwrap(),
16457 );
16458
16459 let text = concat!(
16460 "{ }\n", //
16461 " x\n", //
16462 " /* */\n", //
16463 "x\n", //
16464 "{{} }\n", //
16465 );
16466
16467 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16468 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16469 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16470 editor
16471 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16472 .await;
16473
16474 editor.update_in(cx, |editor, window, cx| {
16475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16476 s.select_display_ranges([
16477 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16478 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16479 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16480 ])
16481 });
16482 editor.newline(&Newline, window, cx);
16483
16484 assert_eq!(
16485 editor.buffer().read(cx).read(cx).text(),
16486 concat!(
16487 "{ \n", // Suppress rustfmt
16488 "\n", //
16489 "}\n", //
16490 " x\n", //
16491 " /* \n", //
16492 " \n", //
16493 " */\n", //
16494 "x\n", //
16495 "{{} \n", //
16496 "}\n", //
16497 )
16498 );
16499 });
16500}
16501
16502#[gpui::test]
16503fn test_highlighted_ranges(cx: &mut TestAppContext) {
16504 init_test(cx, |_| {});
16505
16506 let editor = cx.add_window(|window, cx| {
16507 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16508 build_editor(buffer, window, cx)
16509 });
16510
16511 _ = editor.update(cx, |editor, window, cx| {
16512 struct Type1;
16513 struct Type2;
16514
16515 let buffer = editor.buffer.read(cx).snapshot(cx);
16516
16517 let anchor_range =
16518 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16519
16520 editor.highlight_background::<Type1>(
16521 &[
16522 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16523 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16524 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16525 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16526 ],
16527 |_| Hsla::red(),
16528 cx,
16529 );
16530 editor.highlight_background::<Type2>(
16531 &[
16532 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16533 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16534 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16535 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16536 ],
16537 |_| Hsla::green(),
16538 cx,
16539 );
16540
16541 let snapshot = editor.snapshot(window, cx);
16542 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16543 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16544 &snapshot,
16545 cx.theme(),
16546 );
16547 assert_eq!(
16548 highlighted_ranges,
16549 &[
16550 (
16551 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16552 Hsla::green(),
16553 ),
16554 (
16555 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16556 Hsla::red(),
16557 ),
16558 (
16559 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16560 Hsla::green(),
16561 ),
16562 (
16563 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16564 Hsla::red(),
16565 ),
16566 ]
16567 );
16568 assert_eq!(
16569 editor.sorted_background_highlights_in_range(
16570 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16571 &snapshot,
16572 cx.theme(),
16573 ),
16574 &[(
16575 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16576 Hsla::red(),
16577 )]
16578 );
16579 });
16580}
16581
16582#[gpui::test]
16583async fn test_following(cx: &mut TestAppContext) {
16584 init_test(cx, |_| {});
16585
16586 let fs = FakeFs::new(cx.executor());
16587 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16588
16589 let buffer = project.update(cx, |project, cx| {
16590 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16591 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16592 });
16593 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16594 let follower = cx.update(|cx| {
16595 cx.open_window(
16596 WindowOptions {
16597 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16598 gpui::Point::new(px(0.), px(0.)),
16599 gpui::Point::new(px(10.), px(80.)),
16600 ))),
16601 ..Default::default()
16602 },
16603 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16604 )
16605 .unwrap()
16606 });
16607
16608 let is_still_following = Rc::new(RefCell::new(true));
16609 let follower_edit_event_count = Rc::new(RefCell::new(0));
16610 let pending_update = Rc::new(RefCell::new(None));
16611 let leader_entity = leader.root(cx).unwrap();
16612 let follower_entity = follower.root(cx).unwrap();
16613 _ = follower.update(cx, {
16614 let update = pending_update.clone();
16615 let is_still_following = is_still_following.clone();
16616 let follower_edit_event_count = follower_edit_event_count.clone();
16617 |_, window, cx| {
16618 cx.subscribe_in(
16619 &leader_entity,
16620 window,
16621 move |_, leader, event, window, cx| {
16622 leader.read(cx).add_event_to_update_proto(
16623 event,
16624 &mut update.borrow_mut(),
16625 window,
16626 cx,
16627 );
16628 },
16629 )
16630 .detach();
16631
16632 cx.subscribe_in(
16633 &follower_entity,
16634 window,
16635 move |_, _, event: &EditorEvent, _window, _cx| {
16636 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16637 *is_still_following.borrow_mut() = false;
16638 }
16639
16640 if let EditorEvent::BufferEdited = event {
16641 *follower_edit_event_count.borrow_mut() += 1;
16642 }
16643 },
16644 )
16645 .detach();
16646 }
16647 });
16648
16649 // Update the selections only
16650 _ = leader.update(cx, |leader, window, cx| {
16651 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16652 s.select_ranges([1..1])
16653 });
16654 });
16655 follower
16656 .update(cx, |follower, window, cx| {
16657 follower.apply_update_proto(
16658 &project,
16659 pending_update.borrow_mut().take().unwrap(),
16660 window,
16661 cx,
16662 )
16663 })
16664 .unwrap()
16665 .await
16666 .unwrap();
16667 _ = follower.update(cx, |follower, _, cx| {
16668 assert_eq!(
16669 follower.selections.ranges(&follower.display_snapshot(cx)),
16670 vec![1..1]
16671 );
16672 });
16673 assert!(*is_still_following.borrow());
16674 assert_eq!(*follower_edit_event_count.borrow(), 0);
16675
16676 // Update the scroll position only
16677 _ = leader.update(cx, |leader, window, cx| {
16678 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16679 });
16680 follower
16681 .update(cx, |follower, window, cx| {
16682 follower.apply_update_proto(
16683 &project,
16684 pending_update.borrow_mut().take().unwrap(),
16685 window,
16686 cx,
16687 )
16688 })
16689 .unwrap()
16690 .await
16691 .unwrap();
16692 assert_eq!(
16693 follower
16694 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16695 .unwrap(),
16696 gpui::Point::new(1.5, 3.5)
16697 );
16698 assert!(*is_still_following.borrow());
16699 assert_eq!(*follower_edit_event_count.borrow(), 0);
16700
16701 // Update the selections and scroll position. The follower's scroll position is updated
16702 // via autoscroll, not via the leader's exact scroll position.
16703 _ = leader.update(cx, |leader, window, cx| {
16704 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16705 s.select_ranges([0..0])
16706 });
16707 leader.request_autoscroll(Autoscroll::newest(), cx);
16708 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16709 });
16710 follower
16711 .update(cx, |follower, window, cx| {
16712 follower.apply_update_proto(
16713 &project,
16714 pending_update.borrow_mut().take().unwrap(),
16715 window,
16716 cx,
16717 )
16718 })
16719 .unwrap()
16720 .await
16721 .unwrap();
16722 _ = follower.update(cx, |follower, _, cx| {
16723 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16724 assert_eq!(
16725 follower.selections.ranges(&follower.display_snapshot(cx)),
16726 vec![0..0]
16727 );
16728 });
16729 assert!(*is_still_following.borrow());
16730
16731 // Creating a pending selection that precedes another selection
16732 _ = leader.update(cx, |leader, window, cx| {
16733 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16734 s.select_ranges([1..1])
16735 });
16736 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16737 });
16738 follower
16739 .update(cx, |follower, window, cx| {
16740 follower.apply_update_proto(
16741 &project,
16742 pending_update.borrow_mut().take().unwrap(),
16743 window,
16744 cx,
16745 )
16746 })
16747 .unwrap()
16748 .await
16749 .unwrap();
16750 _ = follower.update(cx, |follower, _, cx| {
16751 assert_eq!(
16752 follower.selections.ranges(&follower.display_snapshot(cx)),
16753 vec![0..0, 1..1]
16754 );
16755 });
16756 assert!(*is_still_following.borrow());
16757
16758 // Extend the pending selection so that it surrounds another selection
16759 _ = leader.update(cx, |leader, window, cx| {
16760 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16761 });
16762 follower
16763 .update(cx, |follower, window, cx| {
16764 follower.apply_update_proto(
16765 &project,
16766 pending_update.borrow_mut().take().unwrap(),
16767 window,
16768 cx,
16769 )
16770 })
16771 .unwrap()
16772 .await
16773 .unwrap();
16774 _ = follower.update(cx, |follower, _, cx| {
16775 assert_eq!(
16776 follower.selections.ranges(&follower.display_snapshot(cx)),
16777 vec![0..2]
16778 );
16779 });
16780
16781 // Scrolling locally breaks the follow
16782 _ = follower.update(cx, |follower, window, cx| {
16783 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16784 follower.set_scroll_anchor(
16785 ScrollAnchor {
16786 anchor: top_anchor,
16787 offset: gpui::Point::new(0.0, 0.5),
16788 },
16789 window,
16790 cx,
16791 );
16792 });
16793 assert!(!(*is_still_following.borrow()));
16794}
16795
16796#[gpui::test]
16797async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16798 init_test(cx, |_| {});
16799
16800 let fs = FakeFs::new(cx.executor());
16801 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16802 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16803 let pane = workspace
16804 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16805 .unwrap();
16806
16807 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16808
16809 let leader = pane.update_in(cx, |_, window, cx| {
16810 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16811 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16812 });
16813
16814 // Start following the editor when it has no excerpts.
16815 let mut state_message =
16816 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16817 let workspace_entity = workspace.root(cx).unwrap();
16818 let follower_1 = cx
16819 .update_window(*workspace.deref(), |_, window, cx| {
16820 Editor::from_state_proto(
16821 workspace_entity,
16822 ViewId {
16823 creator: CollaboratorId::PeerId(PeerId::default()),
16824 id: 0,
16825 },
16826 &mut state_message,
16827 window,
16828 cx,
16829 )
16830 })
16831 .unwrap()
16832 .unwrap()
16833 .await
16834 .unwrap();
16835
16836 let update_message = Rc::new(RefCell::new(None));
16837 follower_1.update_in(cx, {
16838 let update = update_message.clone();
16839 |_, window, cx| {
16840 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16841 leader.read(cx).add_event_to_update_proto(
16842 event,
16843 &mut update.borrow_mut(),
16844 window,
16845 cx,
16846 );
16847 })
16848 .detach();
16849 }
16850 });
16851
16852 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16853 (
16854 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16855 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16856 )
16857 });
16858
16859 // Insert some excerpts.
16860 leader.update(cx, |leader, cx| {
16861 leader.buffer.update(cx, |multibuffer, cx| {
16862 multibuffer.set_excerpts_for_path(
16863 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16864 buffer_1.clone(),
16865 vec![
16866 Point::row_range(0..3),
16867 Point::row_range(1..6),
16868 Point::row_range(12..15),
16869 ],
16870 0,
16871 cx,
16872 );
16873 multibuffer.set_excerpts_for_path(
16874 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16875 buffer_2.clone(),
16876 vec![Point::row_range(0..6), Point::row_range(8..12)],
16877 0,
16878 cx,
16879 );
16880 });
16881 });
16882
16883 // Apply the update of adding the excerpts.
16884 follower_1
16885 .update_in(cx, |follower, window, cx| {
16886 follower.apply_update_proto(
16887 &project,
16888 update_message.borrow().clone().unwrap(),
16889 window,
16890 cx,
16891 )
16892 })
16893 .await
16894 .unwrap();
16895 assert_eq!(
16896 follower_1.update(cx, |editor, cx| editor.text(cx)),
16897 leader.update(cx, |editor, cx| editor.text(cx))
16898 );
16899 update_message.borrow_mut().take();
16900
16901 // Start following separately after it already has excerpts.
16902 let mut state_message =
16903 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16904 let workspace_entity = workspace.root(cx).unwrap();
16905 let follower_2 = cx
16906 .update_window(*workspace.deref(), |_, window, cx| {
16907 Editor::from_state_proto(
16908 workspace_entity,
16909 ViewId {
16910 creator: CollaboratorId::PeerId(PeerId::default()),
16911 id: 0,
16912 },
16913 &mut state_message,
16914 window,
16915 cx,
16916 )
16917 })
16918 .unwrap()
16919 .unwrap()
16920 .await
16921 .unwrap();
16922 assert_eq!(
16923 follower_2.update(cx, |editor, cx| editor.text(cx)),
16924 leader.update(cx, |editor, cx| editor.text(cx))
16925 );
16926
16927 // Remove some excerpts.
16928 leader.update(cx, |leader, cx| {
16929 leader.buffer.update(cx, |multibuffer, cx| {
16930 let excerpt_ids = multibuffer.excerpt_ids();
16931 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16932 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16933 });
16934 });
16935
16936 // Apply the update of removing the excerpts.
16937 follower_1
16938 .update_in(cx, |follower, window, cx| {
16939 follower.apply_update_proto(
16940 &project,
16941 update_message.borrow().clone().unwrap(),
16942 window,
16943 cx,
16944 )
16945 })
16946 .await
16947 .unwrap();
16948 follower_2
16949 .update_in(cx, |follower, window, cx| {
16950 follower.apply_update_proto(
16951 &project,
16952 update_message.borrow().clone().unwrap(),
16953 window,
16954 cx,
16955 )
16956 })
16957 .await
16958 .unwrap();
16959 update_message.borrow_mut().take();
16960 assert_eq!(
16961 follower_1.update(cx, |editor, cx| editor.text(cx)),
16962 leader.update(cx, |editor, cx| editor.text(cx))
16963 );
16964}
16965
16966#[gpui::test]
16967async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16968 init_test(cx, |_| {});
16969
16970 let mut cx = EditorTestContext::new(cx).await;
16971 let lsp_store =
16972 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16973
16974 cx.set_state(indoc! {"
16975 ˇfn func(abc def: i32) -> u32 {
16976 }
16977 "});
16978
16979 cx.update(|_, cx| {
16980 lsp_store.update(cx, |lsp_store, cx| {
16981 lsp_store
16982 .update_diagnostics(
16983 LanguageServerId(0),
16984 lsp::PublishDiagnosticsParams {
16985 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16986 version: None,
16987 diagnostics: vec![
16988 lsp::Diagnostic {
16989 range: lsp::Range::new(
16990 lsp::Position::new(0, 11),
16991 lsp::Position::new(0, 12),
16992 ),
16993 severity: Some(lsp::DiagnosticSeverity::ERROR),
16994 ..Default::default()
16995 },
16996 lsp::Diagnostic {
16997 range: lsp::Range::new(
16998 lsp::Position::new(0, 12),
16999 lsp::Position::new(0, 15),
17000 ),
17001 severity: Some(lsp::DiagnosticSeverity::ERROR),
17002 ..Default::default()
17003 },
17004 lsp::Diagnostic {
17005 range: lsp::Range::new(
17006 lsp::Position::new(0, 25),
17007 lsp::Position::new(0, 28),
17008 ),
17009 severity: Some(lsp::DiagnosticSeverity::ERROR),
17010 ..Default::default()
17011 },
17012 ],
17013 },
17014 None,
17015 DiagnosticSourceKind::Pushed,
17016 &[],
17017 cx,
17018 )
17019 .unwrap()
17020 });
17021 });
17022
17023 executor.run_until_parked();
17024
17025 cx.update_editor(|editor, window, cx| {
17026 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17027 });
17028
17029 cx.assert_editor_state(indoc! {"
17030 fn func(abc def: i32) -> ˇu32 {
17031 }
17032 "});
17033
17034 cx.update_editor(|editor, window, cx| {
17035 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17036 });
17037
17038 cx.assert_editor_state(indoc! {"
17039 fn func(abc ˇdef: i32) -> u32 {
17040 }
17041 "});
17042
17043 cx.update_editor(|editor, window, cx| {
17044 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17045 });
17046
17047 cx.assert_editor_state(indoc! {"
17048 fn func(abcˇ def: i32) -> u32 {
17049 }
17050 "});
17051
17052 cx.update_editor(|editor, window, cx| {
17053 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17054 });
17055
17056 cx.assert_editor_state(indoc! {"
17057 fn func(abc def: i32) -> ˇu32 {
17058 }
17059 "});
17060}
17061
17062#[gpui::test]
17063async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17064 init_test(cx, |_| {});
17065
17066 let mut cx = EditorTestContext::new(cx).await;
17067
17068 let diff_base = r#"
17069 use some::mod;
17070
17071 const A: u32 = 42;
17072
17073 fn main() {
17074 println!("hello");
17075
17076 println!("world");
17077 }
17078 "#
17079 .unindent();
17080
17081 // Edits are modified, removed, modified, added
17082 cx.set_state(
17083 &r#"
17084 use some::modified;
17085
17086 ˇ
17087 fn main() {
17088 println!("hello there");
17089
17090 println!("around the");
17091 println!("world");
17092 }
17093 "#
17094 .unindent(),
17095 );
17096
17097 cx.set_head_text(&diff_base);
17098 executor.run_until_parked();
17099
17100 cx.update_editor(|editor, window, cx| {
17101 //Wrap around the bottom of the buffer
17102 for _ in 0..3 {
17103 editor.go_to_next_hunk(&GoToHunk, window, cx);
17104 }
17105 });
17106
17107 cx.assert_editor_state(
17108 &r#"
17109 ˇuse some::modified;
17110
17111
17112 fn main() {
17113 println!("hello there");
17114
17115 println!("around the");
17116 println!("world");
17117 }
17118 "#
17119 .unindent(),
17120 );
17121
17122 cx.update_editor(|editor, window, cx| {
17123 //Wrap around the top of the buffer
17124 for _ in 0..2 {
17125 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17126 }
17127 });
17128
17129 cx.assert_editor_state(
17130 &r#"
17131 use some::modified;
17132
17133
17134 fn main() {
17135 ˇ println!("hello there");
17136
17137 println!("around the");
17138 println!("world");
17139 }
17140 "#
17141 .unindent(),
17142 );
17143
17144 cx.update_editor(|editor, window, cx| {
17145 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17146 });
17147
17148 cx.assert_editor_state(
17149 &r#"
17150 use some::modified;
17151
17152 ˇ
17153 fn main() {
17154 println!("hello there");
17155
17156 println!("around the");
17157 println!("world");
17158 }
17159 "#
17160 .unindent(),
17161 );
17162
17163 cx.update_editor(|editor, window, cx| {
17164 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17165 });
17166
17167 cx.assert_editor_state(
17168 &r#"
17169 ˇuse some::modified;
17170
17171
17172 fn main() {
17173 println!("hello there");
17174
17175 println!("around the");
17176 println!("world");
17177 }
17178 "#
17179 .unindent(),
17180 );
17181
17182 cx.update_editor(|editor, window, cx| {
17183 for _ in 0..2 {
17184 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17185 }
17186 });
17187
17188 cx.assert_editor_state(
17189 &r#"
17190 use some::modified;
17191
17192
17193 fn main() {
17194 ˇ println!("hello there");
17195
17196 println!("around the");
17197 println!("world");
17198 }
17199 "#
17200 .unindent(),
17201 );
17202
17203 cx.update_editor(|editor, window, cx| {
17204 editor.fold(&Fold, window, cx);
17205 });
17206
17207 cx.update_editor(|editor, window, cx| {
17208 editor.go_to_next_hunk(&GoToHunk, window, cx);
17209 });
17210
17211 cx.assert_editor_state(
17212 &r#"
17213 ˇuse some::modified;
17214
17215
17216 fn main() {
17217 println!("hello there");
17218
17219 println!("around the");
17220 println!("world");
17221 }
17222 "#
17223 .unindent(),
17224 );
17225}
17226
17227#[test]
17228fn test_split_words() {
17229 fn split(text: &str) -> Vec<&str> {
17230 split_words(text).collect()
17231 }
17232
17233 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17234 assert_eq!(split("hello_world"), &["hello_", "world"]);
17235 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17236 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17237 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17238 assert_eq!(split("helloworld"), &["helloworld"]);
17239
17240 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17241}
17242
17243#[test]
17244fn test_split_words_for_snippet_prefix() {
17245 fn split(text: &str) -> Vec<&str> {
17246 snippet_candidate_suffixes(text).collect()
17247 }
17248
17249 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17250 assert_eq!(split("hello_world"), &["hello_world"]);
17251 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17252 assert_eq!(split("Hello_World"), &["Hello_World"]);
17253 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17254 assert_eq!(split("helloworld"), &["helloworld"]);
17255 assert_eq!(
17256 split("this@is!@#$^many . symbols"),
17257 &[
17258 "this@is!@#$^many . symbols",
17259 "@is!@#$^many . symbols",
17260 "is!@#$^many . symbols",
17261 "!@#$^many . symbols",
17262 "@#$^many . symbols",
17263 "#$^many . symbols",
17264 "$^many . symbols",
17265 "^many . symbols",
17266 "many . symbols",
17267 " . symbols",
17268 " . symbols",
17269 " . symbols",
17270 ". symbols",
17271 " symbols",
17272 "symbols"
17273 ],
17274 );
17275}
17276
17277#[gpui::test]
17278async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17279 init_test(cx, |_| {});
17280
17281 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17282 let mut assert = |before, after| {
17283 let _state_context = cx.set_state(before);
17284 cx.run_until_parked();
17285 cx.update_editor(|editor, window, cx| {
17286 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17287 });
17288 cx.run_until_parked();
17289 cx.assert_editor_state(after);
17290 };
17291
17292 // Outside bracket jumps to outside of matching bracket
17293 assert("console.logˇ(var);", "console.log(var)ˇ;");
17294 assert("console.log(var)ˇ;", "console.logˇ(var);");
17295
17296 // Inside bracket jumps to inside of matching bracket
17297 assert("console.log(ˇvar);", "console.log(varˇ);");
17298 assert("console.log(varˇ);", "console.log(ˇvar);");
17299
17300 // When outside a bracket and inside, favor jumping to the inside bracket
17301 assert(
17302 "console.log('foo', [1, 2, 3]ˇ);",
17303 "console.log(ˇ'foo', [1, 2, 3]);",
17304 );
17305 assert(
17306 "console.log(ˇ'foo', [1, 2, 3]);",
17307 "console.log('foo', [1, 2, 3]ˇ);",
17308 );
17309
17310 // Bias forward if two options are equally likely
17311 assert(
17312 "let result = curried_fun()ˇ();",
17313 "let result = curried_fun()()ˇ;",
17314 );
17315
17316 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17317 assert(
17318 indoc! {"
17319 function test() {
17320 console.log('test')ˇ
17321 }"},
17322 indoc! {"
17323 function test() {
17324 console.logˇ('test')
17325 }"},
17326 );
17327}
17328
17329#[gpui::test]
17330async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17331 init_test(cx, |_| {});
17332
17333 let fs = FakeFs::new(cx.executor());
17334 fs.insert_tree(
17335 path!("/a"),
17336 json!({
17337 "main.rs": "fn main() { let a = 5; }",
17338 "other.rs": "// Test file",
17339 }),
17340 )
17341 .await;
17342 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17343
17344 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17345 language_registry.add(Arc::new(Language::new(
17346 LanguageConfig {
17347 name: "Rust".into(),
17348 matcher: LanguageMatcher {
17349 path_suffixes: vec!["rs".to_string()],
17350 ..Default::default()
17351 },
17352 brackets: BracketPairConfig {
17353 pairs: vec![BracketPair {
17354 start: "{".to_string(),
17355 end: "}".to_string(),
17356 close: true,
17357 surround: true,
17358 newline: true,
17359 }],
17360 disabled_scopes_by_bracket_ix: Vec::new(),
17361 },
17362 ..Default::default()
17363 },
17364 Some(tree_sitter_rust::LANGUAGE.into()),
17365 )));
17366 let mut fake_servers = language_registry.register_fake_lsp(
17367 "Rust",
17368 FakeLspAdapter {
17369 capabilities: lsp::ServerCapabilities {
17370 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17371 first_trigger_character: "{".to_string(),
17372 more_trigger_character: None,
17373 }),
17374 ..Default::default()
17375 },
17376 ..Default::default()
17377 },
17378 );
17379
17380 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17381
17382 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17383
17384 let worktree_id = workspace
17385 .update(cx, |workspace, _, cx| {
17386 workspace.project().update(cx, |project, cx| {
17387 project.worktrees(cx).next().unwrap().read(cx).id()
17388 })
17389 })
17390 .unwrap();
17391
17392 let buffer = project
17393 .update(cx, |project, cx| {
17394 project.open_local_buffer(path!("/a/main.rs"), cx)
17395 })
17396 .await
17397 .unwrap();
17398 let editor_handle = workspace
17399 .update(cx, |workspace, window, cx| {
17400 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17401 })
17402 .unwrap()
17403 .await
17404 .unwrap()
17405 .downcast::<Editor>()
17406 .unwrap();
17407
17408 cx.executor().start_waiting();
17409 let fake_server = fake_servers.next().await.unwrap();
17410
17411 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17412 |params, _| async move {
17413 assert_eq!(
17414 params.text_document_position.text_document.uri,
17415 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17416 );
17417 assert_eq!(
17418 params.text_document_position.position,
17419 lsp::Position::new(0, 21),
17420 );
17421
17422 Ok(Some(vec![lsp::TextEdit {
17423 new_text: "]".to_string(),
17424 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17425 }]))
17426 },
17427 );
17428
17429 editor_handle.update_in(cx, |editor, window, cx| {
17430 window.focus(&editor.focus_handle(cx));
17431 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17432 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17433 });
17434 editor.handle_input("{", window, cx);
17435 });
17436
17437 cx.executor().run_until_parked();
17438
17439 buffer.update(cx, |buffer, _| {
17440 assert_eq!(
17441 buffer.text(),
17442 "fn main() { let a = {5}; }",
17443 "No extra braces from on type formatting should appear in the buffer"
17444 )
17445 });
17446}
17447
17448#[gpui::test(iterations = 20, seeds(31))]
17449async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17450 init_test(cx, |_| {});
17451
17452 let mut cx = EditorLspTestContext::new_rust(
17453 lsp::ServerCapabilities {
17454 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17455 first_trigger_character: ".".to_string(),
17456 more_trigger_character: None,
17457 }),
17458 ..Default::default()
17459 },
17460 cx,
17461 )
17462 .await;
17463
17464 cx.update_buffer(|buffer, _| {
17465 // This causes autoindent to be async.
17466 buffer.set_sync_parse_timeout(Duration::ZERO)
17467 });
17468
17469 cx.set_state("fn c() {\n d()ˇ\n}\n");
17470 cx.simulate_keystroke("\n");
17471 cx.run_until_parked();
17472
17473 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17474 let mut request =
17475 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17476 let buffer_cloned = buffer_cloned.clone();
17477 async move {
17478 buffer_cloned.update(&mut cx, |buffer, _| {
17479 assert_eq!(
17480 buffer.text(),
17481 "fn c() {\n d()\n .\n}\n",
17482 "OnTypeFormatting should triggered after autoindent applied"
17483 )
17484 })?;
17485
17486 Ok(Some(vec![]))
17487 }
17488 });
17489
17490 cx.simulate_keystroke(".");
17491 cx.run_until_parked();
17492
17493 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17494 assert!(request.next().await.is_some());
17495 request.close();
17496 assert!(request.next().await.is_none());
17497}
17498
17499#[gpui::test]
17500async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17501 init_test(cx, |_| {});
17502
17503 let fs = FakeFs::new(cx.executor());
17504 fs.insert_tree(
17505 path!("/a"),
17506 json!({
17507 "main.rs": "fn main() { let a = 5; }",
17508 "other.rs": "// Test file",
17509 }),
17510 )
17511 .await;
17512
17513 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17514
17515 let server_restarts = Arc::new(AtomicUsize::new(0));
17516 let closure_restarts = Arc::clone(&server_restarts);
17517 let language_server_name = "test language server";
17518 let language_name: LanguageName = "Rust".into();
17519
17520 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17521 language_registry.add(Arc::new(Language::new(
17522 LanguageConfig {
17523 name: language_name.clone(),
17524 matcher: LanguageMatcher {
17525 path_suffixes: vec!["rs".to_string()],
17526 ..Default::default()
17527 },
17528 ..Default::default()
17529 },
17530 Some(tree_sitter_rust::LANGUAGE.into()),
17531 )));
17532 let mut fake_servers = language_registry.register_fake_lsp(
17533 "Rust",
17534 FakeLspAdapter {
17535 name: language_server_name,
17536 initialization_options: Some(json!({
17537 "testOptionValue": true
17538 })),
17539 initializer: Some(Box::new(move |fake_server| {
17540 let task_restarts = Arc::clone(&closure_restarts);
17541 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17542 task_restarts.fetch_add(1, atomic::Ordering::Release);
17543 futures::future::ready(Ok(()))
17544 });
17545 })),
17546 ..Default::default()
17547 },
17548 );
17549
17550 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17551 let _buffer = project
17552 .update(cx, |project, cx| {
17553 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17554 })
17555 .await
17556 .unwrap();
17557 let _fake_server = fake_servers.next().await.unwrap();
17558 update_test_language_settings(cx, |language_settings| {
17559 language_settings.languages.0.insert(
17560 language_name.clone().0,
17561 LanguageSettingsContent {
17562 tab_size: NonZeroU32::new(8),
17563 ..Default::default()
17564 },
17565 );
17566 });
17567 cx.executor().run_until_parked();
17568 assert_eq!(
17569 server_restarts.load(atomic::Ordering::Acquire),
17570 0,
17571 "Should not restart LSP server on an unrelated change"
17572 );
17573
17574 update_test_project_settings(cx, |project_settings| {
17575 project_settings.lsp.insert(
17576 "Some other server name".into(),
17577 LspSettings {
17578 binary: None,
17579 settings: None,
17580 initialization_options: Some(json!({
17581 "some other init value": false
17582 })),
17583 enable_lsp_tasks: false,
17584 fetch: None,
17585 },
17586 );
17587 });
17588 cx.executor().run_until_parked();
17589 assert_eq!(
17590 server_restarts.load(atomic::Ordering::Acquire),
17591 0,
17592 "Should not restart LSP server on an unrelated LSP settings change"
17593 );
17594
17595 update_test_project_settings(cx, |project_settings| {
17596 project_settings.lsp.insert(
17597 language_server_name.into(),
17598 LspSettings {
17599 binary: None,
17600 settings: None,
17601 initialization_options: Some(json!({
17602 "anotherInitValue": false
17603 })),
17604 enable_lsp_tasks: false,
17605 fetch: None,
17606 },
17607 );
17608 });
17609 cx.executor().run_until_parked();
17610 assert_eq!(
17611 server_restarts.load(atomic::Ordering::Acquire),
17612 1,
17613 "Should restart LSP server on a related LSP settings change"
17614 );
17615
17616 update_test_project_settings(cx, |project_settings| {
17617 project_settings.lsp.insert(
17618 language_server_name.into(),
17619 LspSettings {
17620 binary: None,
17621 settings: None,
17622 initialization_options: Some(json!({
17623 "anotherInitValue": false
17624 })),
17625 enable_lsp_tasks: false,
17626 fetch: None,
17627 },
17628 );
17629 });
17630 cx.executor().run_until_parked();
17631 assert_eq!(
17632 server_restarts.load(atomic::Ordering::Acquire),
17633 1,
17634 "Should not restart LSP server on a related LSP settings change that is the same"
17635 );
17636
17637 update_test_project_settings(cx, |project_settings| {
17638 project_settings.lsp.insert(
17639 language_server_name.into(),
17640 LspSettings {
17641 binary: None,
17642 settings: None,
17643 initialization_options: None,
17644 enable_lsp_tasks: false,
17645 fetch: None,
17646 },
17647 );
17648 });
17649 cx.executor().run_until_parked();
17650 assert_eq!(
17651 server_restarts.load(atomic::Ordering::Acquire),
17652 2,
17653 "Should restart LSP server on another related LSP settings change"
17654 );
17655}
17656
17657#[gpui::test]
17658async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17659 init_test(cx, |_| {});
17660
17661 let mut cx = EditorLspTestContext::new_rust(
17662 lsp::ServerCapabilities {
17663 completion_provider: Some(lsp::CompletionOptions {
17664 trigger_characters: Some(vec![".".to_string()]),
17665 resolve_provider: Some(true),
17666 ..Default::default()
17667 }),
17668 ..Default::default()
17669 },
17670 cx,
17671 )
17672 .await;
17673
17674 cx.set_state("fn main() { let a = 2ˇ; }");
17675 cx.simulate_keystroke(".");
17676 let completion_item = lsp::CompletionItem {
17677 label: "some".into(),
17678 kind: Some(lsp::CompletionItemKind::SNIPPET),
17679 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17680 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17681 kind: lsp::MarkupKind::Markdown,
17682 value: "```rust\nSome(2)\n```".to_string(),
17683 })),
17684 deprecated: Some(false),
17685 sort_text: Some("fffffff2".to_string()),
17686 filter_text: Some("some".to_string()),
17687 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17688 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17689 range: lsp::Range {
17690 start: lsp::Position {
17691 line: 0,
17692 character: 22,
17693 },
17694 end: lsp::Position {
17695 line: 0,
17696 character: 22,
17697 },
17698 },
17699 new_text: "Some(2)".to_string(),
17700 })),
17701 additional_text_edits: Some(vec![lsp::TextEdit {
17702 range: lsp::Range {
17703 start: lsp::Position {
17704 line: 0,
17705 character: 20,
17706 },
17707 end: lsp::Position {
17708 line: 0,
17709 character: 22,
17710 },
17711 },
17712 new_text: "".to_string(),
17713 }]),
17714 ..Default::default()
17715 };
17716
17717 let closure_completion_item = completion_item.clone();
17718 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17719 let task_completion_item = closure_completion_item.clone();
17720 async move {
17721 Ok(Some(lsp::CompletionResponse::Array(vec![
17722 task_completion_item,
17723 ])))
17724 }
17725 });
17726
17727 request.next().await;
17728
17729 cx.condition(|editor, _| editor.context_menu_visible())
17730 .await;
17731 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17732 editor
17733 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17734 .unwrap()
17735 });
17736 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17737
17738 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17739 let task_completion_item = completion_item.clone();
17740 async move { Ok(task_completion_item) }
17741 })
17742 .next()
17743 .await
17744 .unwrap();
17745 apply_additional_edits.await.unwrap();
17746 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17747}
17748
17749#[gpui::test]
17750async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17751 init_test(cx, |_| {});
17752
17753 let mut cx = EditorLspTestContext::new_rust(
17754 lsp::ServerCapabilities {
17755 completion_provider: Some(lsp::CompletionOptions {
17756 trigger_characters: Some(vec![".".to_string()]),
17757 resolve_provider: Some(true),
17758 ..Default::default()
17759 }),
17760 ..Default::default()
17761 },
17762 cx,
17763 )
17764 .await;
17765
17766 cx.set_state("fn main() { let a = 2ˇ; }");
17767 cx.simulate_keystroke(".");
17768
17769 let item1 = lsp::CompletionItem {
17770 label: "method id()".to_string(),
17771 filter_text: Some("id".to_string()),
17772 detail: None,
17773 documentation: None,
17774 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17775 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17776 new_text: ".id".to_string(),
17777 })),
17778 ..lsp::CompletionItem::default()
17779 };
17780
17781 let item2 = lsp::CompletionItem {
17782 label: "other".to_string(),
17783 filter_text: Some("other".to_string()),
17784 detail: None,
17785 documentation: None,
17786 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17787 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17788 new_text: ".other".to_string(),
17789 })),
17790 ..lsp::CompletionItem::default()
17791 };
17792
17793 let item1 = item1.clone();
17794 cx.set_request_handler::<lsp::request::Completion, _, _>({
17795 let item1 = item1.clone();
17796 move |_, _, _| {
17797 let item1 = item1.clone();
17798 let item2 = item2.clone();
17799 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17800 }
17801 })
17802 .next()
17803 .await;
17804
17805 cx.condition(|editor, _| editor.context_menu_visible())
17806 .await;
17807 cx.update_editor(|editor, _, _| {
17808 let context_menu = editor.context_menu.borrow_mut();
17809 let context_menu = context_menu
17810 .as_ref()
17811 .expect("Should have the context menu deployed");
17812 match context_menu {
17813 CodeContextMenu::Completions(completions_menu) => {
17814 let completions = completions_menu.completions.borrow_mut();
17815 assert_eq!(
17816 completions
17817 .iter()
17818 .map(|completion| &completion.label.text)
17819 .collect::<Vec<_>>(),
17820 vec!["method id()", "other"]
17821 )
17822 }
17823 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17824 }
17825 });
17826
17827 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17828 let item1 = item1.clone();
17829 move |_, item_to_resolve, _| {
17830 let item1 = item1.clone();
17831 async move {
17832 if item1 == item_to_resolve {
17833 Ok(lsp::CompletionItem {
17834 label: "method id()".to_string(),
17835 filter_text: Some("id".to_string()),
17836 detail: Some("Now resolved!".to_string()),
17837 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17838 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17839 range: lsp::Range::new(
17840 lsp::Position::new(0, 22),
17841 lsp::Position::new(0, 22),
17842 ),
17843 new_text: ".id".to_string(),
17844 })),
17845 ..lsp::CompletionItem::default()
17846 })
17847 } else {
17848 Ok(item_to_resolve)
17849 }
17850 }
17851 }
17852 })
17853 .next()
17854 .await
17855 .unwrap();
17856 cx.run_until_parked();
17857
17858 cx.update_editor(|editor, window, cx| {
17859 editor.context_menu_next(&Default::default(), window, cx);
17860 });
17861
17862 cx.update_editor(|editor, _, _| {
17863 let context_menu = editor.context_menu.borrow_mut();
17864 let context_menu = context_menu
17865 .as_ref()
17866 .expect("Should have the context menu deployed");
17867 match context_menu {
17868 CodeContextMenu::Completions(completions_menu) => {
17869 let completions = completions_menu.completions.borrow_mut();
17870 assert_eq!(
17871 completions
17872 .iter()
17873 .map(|completion| &completion.label.text)
17874 .collect::<Vec<_>>(),
17875 vec!["method id() Now resolved!", "other"],
17876 "Should update first completion label, but not second as the filter text did not match."
17877 );
17878 }
17879 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17880 }
17881 });
17882}
17883
17884#[gpui::test]
17885async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17886 init_test(cx, |_| {});
17887 let mut cx = EditorLspTestContext::new_rust(
17888 lsp::ServerCapabilities {
17889 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17890 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17891 completion_provider: Some(lsp::CompletionOptions {
17892 resolve_provider: Some(true),
17893 ..Default::default()
17894 }),
17895 ..Default::default()
17896 },
17897 cx,
17898 )
17899 .await;
17900 cx.set_state(indoc! {"
17901 struct TestStruct {
17902 field: i32
17903 }
17904
17905 fn mainˇ() {
17906 let unused_var = 42;
17907 let test_struct = TestStruct { field: 42 };
17908 }
17909 "});
17910 let symbol_range = cx.lsp_range(indoc! {"
17911 struct TestStruct {
17912 field: i32
17913 }
17914
17915 «fn main»() {
17916 let unused_var = 42;
17917 let test_struct = TestStruct { field: 42 };
17918 }
17919 "});
17920 let mut hover_requests =
17921 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17922 Ok(Some(lsp::Hover {
17923 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17924 kind: lsp::MarkupKind::Markdown,
17925 value: "Function documentation".to_string(),
17926 }),
17927 range: Some(symbol_range),
17928 }))
17929 });
17930
17931 // Case 1: Test that code action menu hide hover popover
17932 cx.dispatch_action(Hover);
17933 hover_requests.next().await;
17934 cx.condition(|editor, _| editor.hover_state.visible()).await;
17935 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17936 move |_, _, _| async move {
17937 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17938 lsp::CodeAction {
17939 title: "Remove unused variable".to_string(),
17940 kind: Some(CodeActionKind::QUICKFIX),
17941 edit: Some(lsp::WorkspaceEdit {
17942 changes: Some(
17943 [(
17944 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17945 vec![lsp::TextEdit {
17946 range: lsp::Range::new(
17947 lsp::Position::new(5, 4),
17948 lsp::Position::new(5, 27),
17949 ),
17950 new_text: "".to_string(),
17951 }],
17952 )]
17953 .into_iter()
17954 .collect(),
17955 ),
17956 ..Default::default()
17957 }),
17958 ..Default::default()
17959 },
17960 )]))
17961 },
17962 );
17963 cx.update_editor(|editor, window, cx| {
17964 editor.toggle_code_actions(
17965 &ToggleCodeActions {
17966 deployed_from: None,
17967 quick_launch: false,
17968 },
17969 window,
17970 cx,
17971 );
17972 });
17973 code_action_requests.next().await;
17974 cx.run_until_parked();
17975 cx.condition(|editor, _| editor.context_menu_visible())
17976 .await;
17977 cx.update_editor(|editor, _, _| {
17978 assert!(
17979 !editor.hover_state.visible(),
17980 "Hover popover should be hidden when code action menu is shown"
17981 );
17982 // Hide code actions
17983 editor.context_menu.take();
17984 });
17985
17986 // Case 2: Test that code completions hide hover popover
17987 cx.dispatch_action(Hover);
17988 hover_requests.next().await;
17989 cx.condition(|editor, _| editor.hover_state.visible()).await;
17990 let counter = Arc::new(AtomicUsize::new(0));
17991 let mut completion_requests =
17992 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17993 let counter = counter.clone();
17994 async move {
17995 counter.fetch_add(1, atomic::Ordering::Release);
17996 Ok(Some(lsp::CompletionResponse::Array(vec![
17997 lsp::CompletionItem {
17998 label: "main".into(),
17999 kind: Some(lsp::CompletionItemKind::FUNCTION),
18000 detail: Some("() -> ()".to_string()),
18001 ..Default::default()
18002 },
18003 lsp::CompletionItem {
18004 label: "TestStruct".into(),
18005 kind: Some(lsp::CompletionItemKind::STRUCT),
18006 detail: Some("struct TestStruct".to_string()),
18007 ..Default::default()
18008 },
18009 ])))
18010 }
18011 });
18012 cx.update_editor(|editor, window, cx| {
18013 editor.show_completions(
18014 &ShowCompletions {
18015 trigger: None,
18016 snippets_only: false,
18017 },
18018 window,
18019 cx,
18020 );
18021 });
18022 completion_requests.next().await;
18023 cx.condition(|editor, _| editor.context_menu_visible())
18024 .await;
18025 cx.update_editor(|editor, _, _| {
18026 assert!(
18027 !editor.hover_state.visible(),
18028 "Hover popover should be hidden when completion menu is shown"
18029 );
18030 });
18031}
18032
18033#[gpui::test]
18034async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18035 init_test(cx, |_| {});
18036
18037 let mut cx = EditorLspTestContext::new_rust(
18038 lsp::ServerCapabilities {
18039 completion_provider: Some(lsp::CompletionOptions {
18040 trigger_characters: Some(vec![".".to_string()]),
18041 resolve_provider: Some(true),
18042 ..Default::default()
18043 }),
18044 ..Default::default()
18045 },
18046 cx,
18047 )
18048 .await;
18049
18050 cx.set_state("fn main() { let a = 2ˇ; }");
18051 cx.simulate_keystroke(".");
18052
18053 let unresolved_item_1 = lsp::CompletionItem {
18054 label: "id".to_string(),
18055 filter_text: Some("id".to_string()),
18056 detail: None,
18057 documentation: None,
18058 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18059 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18060 new_text: ".id".to_string(),
18061 })),
18062 ..lsp::CompletionItem::default()
18063 };
18064 let resolved_item_1 = lsp::CompletionItem {
18065 additional_text_edits: Some(vec![lsp::TextEdit {
18066 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18067 new_text: "!!".to_string(),
18068 }]),
18069 ..unresolved_item_1.clone()
18070 };
18071 let unresolved_item_2 = lsp::CompletionItem {
18072 label: "other".to_string(),
18073 filter_text: Some("other".to_string()),
18074 detail: None,
18075 documentation: None,
18076 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18077 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18078 new_text: ".other".to_string(),
18079 })),
18080 ..lsp::CompletionItem::default()
18081 };
18082 let resolved_item_2 = lsp::CompletionItem {
18083 additional_text_edits: Some(vec![lsp::TextEdit {
18084 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18085 new_text: "??".to_string(),
18086 }]),
18087 ..unresolved_item_2.clone()
18088 };
18089
18090 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18091 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18092 cx.lsp
18093 .server
18094 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18095 let unresolved_item_1 = unresolved_item_1.clone();
18096 let resolved_item_1 = resolved_item_1.clone();
18097 let unresolved_item_2 = unresolved_item_2.clone();
18098 let resolved_item_2 = resolved_item_2.clone();
18099 let resolve_requests_1 = resolve_requests_1.clone();
18100 let resolve_requests_2 = resolve_requests_2.clone();
18101 move |unresolved_request, _| {
18102 let unresolved_item_1 = unresolved_item_1.clone();
18103 let resolved_item_1 = resolved_item_1.clone();
18104 let unresolved_item_2 = unresolved_item_2.clone();
18105 let resolved_item_2 = resolved_item_2.clone();
18106 let resolve_requests_1 = resolve_requests_1.clone();
18107 let resolve_requests_2 = resolve_requests_2.clone();
18108 async move {
18109 if unresolved_request == unresolved_item_1 {
18110 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18111 Ok(resolved_item_1.clone())
18112 } else if unresolved_request == unresolved_item_2 {
18113 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18114 Ok(resolved_item_2.clone())
18115 } else {
18116 panic!("Unexpected completion item {unresolved_request:?}")
18117 }
18118 }
18119 }
18120 })
18121 .detach();
18122
18123 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18124 let unresolved_item_1 = unresolved_item_1.clone();
18125 let unresolved_item_2 = unresolved_item_2.clone();
18126 async move {
18127 Ok(Some(lsp::CompletionResponse::Array(vec![
18128 unresolved_item_1,
18129 unresolved_item_2,
18130 ])))
18131 }
18132 })
18133 .next()
18134 .await;
18135
18136 cx.condition(|editor, _| editor.context_menu_visible())
18137 .await;
18138 cx.update_editor(|editor, _, _| {
18139 let context_menu = editor.context_menu.borrow_mut();
18140 let context_menu = context_menu
18141 .as_ref()
18142 .expect("Should have the context menu deployed");
18143 match context_menu {
18144 CodeContextMenu::Completions(completions_menu) => {
18145 let completions = completions_menu.completions.borrow_mut();
18146 assert_eq!(
18147 completions
18148 .iter()
18149 .map(|completion| &completion.label.text)
18150 .collect::<Vec<_>>(),
18151 vec!["id", "other"]
18152 )
18153 }
18154 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18155 }
18156 });
18157 cx.run_until_parked();
18158
18159 cx.update_editor(|editor, window, cx| {
18160 editor.context_menu_next(&ContextMenuNext, window, cx);
18161 });
18162 cx.run_until_parked();
18163 cx.update_editor(|editor, window, cx| {
18164 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18165 });
18166 cx.run_until_parked();
18167 cx.update_editor(|editor, window, cx| {
18168 editor.context_menu_next(&ContextMenuNext, window, cx);
18169 });
18170 cx.run_until_parked();
18171 cx.update_editor(|editor, window, cx| {
18172 editor
18173 .compose_completion(&ComposeCompletion::default(), window, cx)
18174 .expect("No task returned")
18175 })
18176 .await
18177 .expect("Completion failed");
18178 cx.run_until_parked();
18179
18180 cx.update_editor(|editor, _, cx| {
18181 assert_eq!(
18182 resolve_requests_1.load(atomic::Ordering::Acquire),
18183 1,
18184 "Should always resolve once despite multiple selections"
18185 );
18186 assert_eq!(
18187 resolve_requests_2.load(atomic::Ordering::Acquire),
18188 1,
18189 "Should always resolve once after multiple selections and applying the completion"
18190 );
18191 assert_eq!(
18192 editor.text(cx),
18193 "fn main() { let a = ??.other; }",
18194 "Should use resolved data when applying the completion"
18195 );
18196 });
18197}
18198
18199#[gpui::test]
18200async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18201 init_test(cx, |_| {});
18202
18203 let item_0 = lsp::CompletionItem {
18204 label: "abs".into(),
18205 insert_text: Some("abs".into()),
18206 data: Some(json!({ "very": "special"})),
18207 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18208 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18209 lsp::InsertReplaceEdit {
18210 new_text: "abs".to_string(),
18211 insert: lsp::Range::default(),
18212 replace: lsp::Range::default(),
18213 },
18214 )),
18215 ..lsp::CompletionItem::default()
18216 };
18217 let items = iter::once(item_0.clone())
18218 .chain((11..51).map(|i| lsp::CompletionItem {
18219 label: format!("item_{}", i),
18220 insert_text: Some(format!("item_{}", i)),
18221 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18222 ..lsp::CompletionItem::default()
18223 }))
18224 .collect::<Vec<_>>();
18225
18226 let default_commit_characters = vec!["?".to_string()];
18227 let default_data = json!({ "default": "data"});
18228 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18229 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18230 let default_edit_range = lsp::Range {
18231 start: lsp::Position {
18232 line: 0,
18233 character: 5,
18234 },
18235 end: lsp::Position {
18236 line: 0,
18237 character: 5,
18238 },
18239 };
18240
18241 let mut cx = EditorLspTestContext::new_rust(
18242 lsp::ServerCapabilities {
18243 completion_provider: Some(lsp::CompletionOptions {
18244 trigger_characters: Some(vec![".".to_string()]),
18245 resolve_provider: Some(true),
18246 ..Default::default()
18247 }),
18248 ..Default::default()
18249 },
18250 cx,
18251 )
18252 .await;
18253
18254 cx.set_state("fn main() { let a = 2ˇ; }");
18255 cx.simulate_keystroke(".");
18256
18257 let completion_data = default_data.clone();
18258 let completion_characters = default_commit_characters.clone();
18259 let completion_items = items.clone();
18260 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18261 let default_data = completion_data.clone();
18262 let default_commit_characters = completion_characters.clone();
18263 let items = completion_items.clone();
18264 async move {
18265 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18266 items,
18267 item_defaults: Some(lsp::CompletionListItemDefaults {
18268 data: Some(default_data.clone()),
18269 commit_characters: Some(default_commit_characters.clone()),
18270 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18271 default_edit_range,
18272 )),
18273 insert_text_format: Some(default_insert_text_format),
18274 insert_text_mode: Some(default_insert_text_mode),
18275 }),
18276 ..lsp::CompletionList::default()
18277 })))
18278 }
18279 })
18280 .next()
18281 .await;
18282
18283 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18284 cx.lsp
18285 .server
18286 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18287 let closure_resolved_items = resolved_items.clone();
18288 move |item_to_resolve, _| {
18289 let closure_resolved_items = closure_resolved_items.clone();
18290 async move {
18291 closure_resolved_items.lock().push(item_to_resolve.clone());
18292 Ok(item_to_resolve)
18293 }
18294 }
18295 })
18296 .detach();
18297
18298 cx.condition(|editor, _| editor.context_menu_visible())
18299 .await;
18300 cx.run_until_parked();
18301 cx.update_editor(|editor, _, _| {
18302 let menu = editor.context_menu.borrow_mut();
18303 match menu.as_ref().expect("should have the completions menu") {
18304 CodeContextMenu::Completions(completions_menu) => {
18305 assert_eq!(
18306 completions_menu
18307 .entries
18308 .borrow()
18309 .iter()
18310 .map(|mat| mat.string.clone())
18311 .collect::<Vec<String>>(),
18312 items
18313 .iter()
18314 .map(|completion| completion.label.clone())
18315 .collect::<Vec<String>>()
18316 );
18317 }
18318 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18319 }
18320 });
18321 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18322 // with 4 from the end.
18323 assert_eq!(
18324 *resolved_items.lock(),
18325 [&items[0..16], &items[items.len() - 4..items.len()]]
18326 .concat()
18327 .iter()
18328 .cloned()
18329 .map(|mut item| {
18330 if item.data.is_none() {
18331 item.data = Some(default_data.clone());
18332 }
18333 item
18334 })
18335 .collect::<Vec<lsp::CompletionItem>>(),
18336 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18337 );
18338 resolved_items.lock().clear();
18339
18340 cx.update_editor(|editor, window, cx| {
18341 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18342 });
18343 cx.run_until_parked();
18344 // Completions that have already been resolved are skipped.
18345 assert_eq!(
18346 *resolved_items.lock(),
18347 items[items.len() - 17..items.len() - 4]
18348 .iter()
18349 .cloned()
18350 .map(|mut item| {
18351 if item.data.is_none() {
18352 item.data = Some(default_data.clone());
18353 }
18354 item
18355 })
18356 .collect::<Vec<lsp::CompletionItem>>()
18357 );
18358 resolved_items.lock().clear();
18359}
18360
18361#[gpui::test]
18362async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18363 init_test(cx, |_| {});
18364
18365 let mut cx = EditorLspTestContext::new(
18366 Language::new(
18367 LanguageConfig {
18368 matcher: LanguageMatcher {
18369 path_suffixes: vec!["jsx".into()],
18370 ..Default::default()
18371 },
18372 overrides: [(
18373 "element".into(),
18374 LanguageConfigOverride {
18375 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18376 ..Default::default()
18377 },
18378 )]
18379 .into_iter()
18380 .collect(),
18381 ..Default::default()
18382 },
18383 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18384 )
18385 .with_override_query("(jsx_self_closing_element) @element")
18386 .unwrap(),
18387 lsp::ServerCapabilities {
18388 completion_provider: Some(lsp::CompletionOptions {
18389 trigger_characters: Some(vec![":".to_string()]),
18390 ..Default::default()
18391 }),
18392 ..Default::default()
18393 },
18394 cx,
18395 )
18396 .await;
18397
18398 cx.lsp
18399 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18400 Ok(Some(lsp::CompletionResponse::Array(vec![
18401 lsp::CompletionItem {
18402 label: "bg-blue".into(),
18403 ..Default::default()
18404 },
18405 lsp::CompletionItem {
18406 label: "bg-red".into(),
18407 ..Default::default()
18408 },
18409 lsp::CompletionItem {
18410 label: "bg-yellow".into(),
18411 ..Default::default()
18412 },
18413 ])))
18414 });
18415
18416 cx.set_state(r#"<p class="bgˇ" />"#);
18417
18418 // Trigger completion when typing a dash, because the dash is an extra
18419 // word character in the 'element' scope, which contains the cursor.
18420 cx.simulate_keystroke("-");
18421 cx.executor().run_until_parked();
18422 cx.update_editor(|editor, _, _| {
18423 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18424 {
18425 assert_eq!(
18426 completion_menu_entries(menu),
18427 &["bg-blue", "bg-red", "bg-yellow"]
18428 );
18429 } else {
18430 panic!("expected completion menu to be open");
18431 }
18432 });
18433
18434 cx.simulate_keystroke("l");
18435 cx.executor().run_until_parked();
18436 cx.update_editor(|editor, _, _| {
18437 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18438 {
18439 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18440 } else {
18441 panic!("expected completion menu to be open");
18442 }
18443 });
18444
18445 // When filtering completions, consider the character after the '-' to
18446 // be the start of a subword.
18447 cx.set_state(r#"<p class="yelˇ" />"#);
18448 cx.simulate_keystroke("l");
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!(completion_menu_entries(menu), &["bg-yellow"]);
18454 } else {
18455 panic!("expected completion menu to be open");
18456 }
18457 });
18458}
18459
18460fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18461 let entries = menu.entries.borrow();
18462 entries.iter().map(|mat| mat.string.clone()).collect()
18463}
18464
18465#[gpui::test]
18466async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18467 init_test(cx, |settings| {
18468 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18469 });
18470
18471 let fs = FakeFs::new(cx.executor());
18472 fs.insert_file(path!("/file.ts"), Default::default()).await;
18473
18474 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18475 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18476
18477 language_registry.add(Arc::new(Language::new(
18478 LanguageConfig {
18479 name: "TypeScript".into(),
18480 matcher: LanguageMatcher {
18481 path_suffixes: vec!["ts".to_string()],
18482 ..Default::default()
18483 },
18484 ..Default::default()
18485 },
18486 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18487 )));
18488 update_test_language_settings(cx, |settings| {
18489 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18490 });
18491
18492 let test_plugin = "test_plugin";
18493 let _ = language_registry.register_fake_lsp(
18494 "TypeScript",
18495 FakeLspAdapter {
18496 prettier_plugins: vec![test_plugin],
18497 ..Default::default()
18498 },
18499 );
18500
18501 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18502 let buffer = project
18503 .update(cx, |project, cx| {
18504 project.open_local_buffer(path!("/file.ts"), cx)
18505 })
18506 .await
18507 .unwrap();
18508
18509 let buffer_text = "one\ntwo\nthree\n";
18510 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18511 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18512 editor.update_in(cx, |editor, window, cx| {
18513 editor.set_text(buffer_text, window, cx)
18514 });
18515
18516 editor
18517 .update_in(cx, |editor, window, cx| {
18518 editor.perform_format(
18519 project.clone(),
18520 FormatTrigger::Manual,
18521 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18522 window,
18523 cx,
18524 )
18525 })
18526 .unwrap()
18527 .await;
18528 assert_eq!(
18529 editor.update(cx, |editor, cx| editor.text(cx)),
18530 buffer_text.to_string() + prettier_format_suffix,
18531 "Test prettier formatting was not applied to the original buffer text",
18532 );
18533
18534 update_test_language_settings(cx, |settings| {
18535 settings.defaults.formatter = Some(FormatterList::default())
18536 });
18537 let format = editor.update_in(cx, |editor, window, cx| {
18538 editor.perform_format(
18539 project.clone(),
18540 FormatTrigger::Manual,
18541 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18542 window,
18543 cx,
18544 )
18545 });
18546 format.await.unwrap();
18547 assert_eq!(
18548 editor.update(cx, |editor, cx| editor.text(cx)),
18549 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18550 "Autoformatting (via test prettier) was not applied to the original buffer text",
18551 );
18552}
18553
18554#[gpui::test]
18555async fn test_addition_reverts(cx: &mut TestAppContext) {
18556 init_test(cx, |_| {});
18557 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18558 let base_text = indoc! {r#"
18559 struct Row;
18560 struct Row1;
18561 struct Row2;
18562
18563 struct Row4;
18564 struct Row5;
18565 struct Row6;
18566
18567 struct Row8;
18568 struct Row9;
18569 struct Row10;"#};
18570
18571 // When addition hunks are not adjacent to carets, no hunk revert is performed
18572 assert_hunk_revert(
18573 indoc! {r#"struct Row;
18574 struct Row1;
18575 struct Row1.1;
18576 struct Row1.2;
18577 struct Row2;ˇ
18578
18579 struct Row4;
18580 struct Row5;
18581 struct Row6;
18582
18583 struct Row8;
18584 ˇstruct Row9;
18585 struct Row9.1;
18586 struct Row9.2;
18587 struct Row9.3;
18588 struct Row10;"#},
18589 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18590 indoc! {r#"struct Row;
18591 struct Row1;
18592 struct Row1.1;
18593 struct Row1.2;
18594 struct Row2;ˇ
18595
18596 struct Row4;
18597 struct Row5;
18598 struct Row6;
18599
18600 struct Row8;
18601 ˇstruct Row9;
18602 struct Row9.1;
18603 struct Row9.2;
18604 struct Row9.3;
18605 struct Row10;"#},
18606 base_text,
18607 &mut cx,
18608 );
18609 // Same for selections
18610 assert_hunk_revert(
18611 indoc! {r#"struct Row;
18612 struct Row1;
18613 struct Row2;
18614 struct Row2.1;
18615 struct Row2.2;
18616 «ˇ
18617 struct Row4;
18618 struct» Row5;
18619 «struct Row6;
18620 ˇ»
18621 struct Row9.1;
18622 struct Row9.2;
18623 struct Row9.3;
18624 struct Row8;
18625 struct Row9;
18626 struct Row10;"#},
18627 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18628 indoc! {r#"struct Row;
18629 struct Row1;
18630 struct Row2;
18631 struct Row2.1;
18632 struct Row2.2;
18633 «ˇ
18634 struct Row4;
18635 struct» Row5;
18636 «struct Row6;
18637 ˇ»
18638 struct Row9.1;
18639 struct Row9.2;
18640 struct Row9.3;
18641 struct Row8;
18642 struct Row9;
18643 struct Row10;"#},
18644 base_text,
18645 &mut cx,
18646 );
18647
18648 // When carets and selections intersect the addition hunks, those are reverted.
18649 // Adjacent carets got merged.
18650 assert_hunk_revert(
18651 indoc! {r#"struct Row;
18652 ˇ// something on the top
18653 struct Row1;
18654 struct Row2;
18655 struct Roˇw3.1;
18656 struct Row2.2;
18657 struct Row2.3;ˇ
18658
18659 struct Row4;
18660 struct ˇRow5.1;
18661 struct Row5.2;
18662 struct «Rowˇ»5.3;
18663 struct Row5;
18664 struct Row6;
18665 ˇ
18666 struct Row9.1;
18667 struct «Rowˇ»9.2;
18668 struct «ˇRow»9.3;
18669 struct Row8;
18670 struct Row9;
18671 «ˇ// something on bottom»
18672 struct Row10;"#},
18673 vec![
18674 DiffHunkStatusKind::Added,
18675 DiffHunkStatusKind::Added,
18676 DiffHunkStatusKind::Added,
18677 DiffHunkStatusKind::Added,
18678 DiffHunkStatusKind::Added,
18679 ],
18680 indoc! {r#"struct Row;
18681 ˇstruct Row1;
18682 struct Row2;
18683 ˇ
18684 struct Row4;
18685 ˇstruct Row5;
18686 struct Row6;
18687 ˇ
18688 ˇstruct Row8;
18689 struct Row9;
18690 ˇstruct Row10;"#},
18691 base_text,
18692 &mut cx,
18693 );
18694}
18695
18696#[gpui::test]
18697async fn test_modification_reverts(cx: &mut TestAppContext) {
18698 init_test(cx, |_| {});
18699 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18700 let base_text = indoc! {r#"
18701 struct Row;
18702 struct Row1;
18703 struct Row2;
18704
18705 struct Row4;
18706 struct Row5;
18707 struct Row6;
18708
18709 struct Row8;
18710 struct Row9;
18711 struct Row10;"#};
18712
18713 // Modification hunks behave the same as the addition ones.
18714 assert_hunk_revert(
18715 indoc! {r#"struct Row;
18716 struct Row1;
18717 struct Row33;
18718 ˇ
18719 struct Row4;
18720 struct Row5;
18721 struct Row6;
18722 ˇ
18723 struct Row99;
18724 struct Row9;
18725 struct Row10;"#},
18726 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18727 indoc! {r#"struct Row;
18728 struct Row1;
18729 struct Row33;
18730 ˇ
18731 struct Row4;
18732 struct Row5;
18733 struct Row6;
18734 ˇ
18735 struct Row99;
18736 struct Row9;
18737 struct Row10;"#},
18738 base_text,
18739 &mut cx,
18740 );
18741 assert_hunk_revert(
18742 indoc! {r#"struct Row;
18743 struct Row1;
18744 struct Row33;
18745 «ˇ
18746 struct Row4;
18747 struct» Row5;
18748 «struct Row6;
18749 ˇ»
18750 struct Row99;
18751 struct Row9;
18752 struct Row10;"#},
18753 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18754 indoc! {r#"struct Row;
18755 struct Row1;
18756 struct Row33;
18757 «ˇ
18758 struct Row4;
18759 struct» Row5;
18760 «struct Row6;
18761 ˇ»
18762 struct Row99;
18763 struct Row9;
18764 struct Row10;"#},
18765 base_text,
18766 &mut cx,
18767 );
18768
18769 assert_hunk_revert(
18770 indoc! {r#"ˇstruct Row1.1;
18771 struct Row1;
18772 «ˇstr»uct Row22;
18773
18774 struct ˇRow44;
18775 struct Row5;
18776 struct «Rˇ»ow66;ˇ
18777
18778 «struˇ»ct Row88;
18779 struct Row9;
18780 struct Row1011;ˇ"#},
18781 vec![
18782 DiffHunkStatusKind::Modified,
18783 DiffHunkStatusKind::Modified,
18784 DiffHunkStatusKind::Modified,
18785 DiffHunkStatusKind::Modified,
18786 DiffHunkStatusKind::Modified,
18787 DiffHunkStatusKind::Modified,
18788 ],
18789 indoc! {r#"struct Row;
18790 ˇstruct Row1;
18791 struct Row2;
18792 ˇ
18793 struct Row4;
18794 ˇstruct Row5;
18795 struct Row6;
18796 ˇ
18797 struct Row8;
18798 ˇstruct Row9;
18799 struct Row10;ˇ"#},
18800 base_text,
18801 &mut cx,
18802 );
18803}
18804
18805#[gpui::test]
18806async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18807 init_test(cx, |_| {});
18808 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18809 let base_text = indoc! {r#"
18810 one
18811
18812 two
18813 three
18814 "#};
18815
18816 cx.set_head_text(base_text);
18817 cx.set_state("\nˇ\n");
18818 cx.executor().run_until_parked();
18819 cx.update_editor(|editor, _window, cx| {
18820 editor.expand_selected_diff_hunks(cx);
18821 });
18822 cx.executor().run_until_parked();
18823 cx.update_editor(|editor, window, cx| {
18824 editor.backspace(&Default::default(), window, cx);
18825 });
18826 cx.run_until_parked();
18827 cx.assert_state_with_diff(
18828 indoc! {r#"
18829
18830 - two
18831 - threeˇ
18832 +
18833 "#}
18834 .to_string(),
18835 );
18836}
18837
18838#[gpui::test]
18839async fn test_deletion_reverts(cx: &mut TestAppContext) {
18840 init_test(cx, |_| {});
18841 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18842 let base_text = indoc! {r#"struct Row;
18843struct Row1;
18844struct Row2;
18845
18846struct Row4;
18847struct Row5;
18848struct Row6;
18849
18850struct Row8;
18851struct Row9;
18852struct Row10;"#};
18853
18854 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18855 assert_hunk_revert(
18856 indoc! {r#"struct Row;
18857 struct Row2;
18858
18859 ˇstruct Row4;
18860 struct Row5;
18861 struct Row6;
18862 ˇ
18863 struct Row8;
18864 struct Row10;"#},
18865 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18866 indoc! {r#"struct Row;
18867 struct Row2;
18868
18869 ˇstruct Row4;
18870 struct Row5;
18871 struct Row6;
18872 ˇ
18873 struct Row8;
18874 struct Row10;"#},
18875 base_text,
18876 &mut cx,
18877 );
18878 assert_hunk_revert(
18879 indoc! {r#"struct Row;
18880 struct Row2;
18881
18882 «ˇstruct Row4;
18883 struct» Row5;
18884 «struct Row6;
18885 ˇ»
18886 struct Row8;
18887 struct Row10;"#},
18888 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18889 indoc! {r#"struct Row;
18890 struct Row2;
18891
18892 «ˇstruct Row4;
18893 struct» Row5;
18894 «struct Row6;
18895 ˇ»
18896 struct Row8;
18897 struct Row10;"#},
18898 base_text,
18899 &mut cx,
18900 );
18901
18902 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18903 assert_hunk_revert(
18904 indoc! {r#"struct Row;
18905 ˇstruct Row2;
18906
18907 struct Row4;
18908 struct Row5;
18909 struct Row6;
18910
18911 struct Row8;ˇ
18912 struct Row10;"#},
18913 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18914 indoc! {r#"struct Row;
18915 struct Row1;
18916 ˇstruct Row2;
18917
18918 struct Row4;
18919 struct Row5;
18920 struct Row6;
18921
18922 struct Row8;ˇ
18923 struct Row9;
18924 struct Row10;"#},
18925 base_text,
18926 &mut cx,
18927 );
18928 assert_hunk_revert(
18929 indoc! {r#"struct Row;
18930 struct Row2«ˇ;
18931 struct Row4;
18932 struct» Row5;
18933 «struct Row6;
18934
18935 struct Row8;ˇ»
18936 struct Row10;"#},
18937 vec![
18938 DiffHunkStatusKind::Deleted,
18939 DiffHunkStatusKind::Deleted,
18940 DiffHunkStatusKind::Deleted,
18941 ],
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}
18957
18958#[gpui::test]
18959async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18960 init_test(cx, |_| {});
18961
18962 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18963 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18964 let base_text_3 =
18965 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18966
18967 let text_1 = edit_first_char_of_every_line(base_text_1);
18968 let text_2 = edit_first_char_of_every_line(base_text_2);
18969 let text_3 = edit_first_char_of_every_line(base_text_3);
18970
18971 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18972 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18973 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18974
18975 let multibuffer = cx.new(|cx| {
18976 let mut multibuffer = MultiBuffer::new(ReadWrite);
18977 multibuffer.push_excerpts(
18978 buffer_1.clone(),
18979 [
18980 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18981 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18982 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18983 ],
18984 cx,
18985 );
18986 multibuffer.push_excerpts(
18987 buffer_2.clone(),
18988 [
18989 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18990 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18991 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18992 ],
18993 cx,
18994 );
18995 multibuffer.push_excerpts(
18996 buffer_3.clone(),
18997 [
18998 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18999 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19000 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19001 ],
19002 cx,
19003 );
19004 multibuffer
19005 });
19006
19007 let fs = FakeFs::new(cx.executor());
19008 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19009 let (editor, cx) = cx
19010 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19011 editor.update_in(cx, |editor, _window, cx| {
19012 for (buffer, diff_base) in [
19013 (buffer_1.clone(), base_text_1),
19014 (buffer_2.clone(), base_text_2),
19015 (buffer_3.clone(), base_text_3),
19016 ] {
19017 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19018 editor
19019 .buffer
19020 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19021 }
19022 });
19023 cx.executor().run_until_parked();
19024
19025 editor.update_in(cx, |editor, window, cx| {
19026 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}");
19027 editor.select_all(&SelectAll, window, cx);
19028 editor.git_restore(&Default::default(), window, cx);
19029 });
19030 cx.executor().run_until_parked();
19031
19032 // When all ranges are selected, all buffer hunks are reverted.
19033 editor.update(cx, |editor, cx| {
19034 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");
19035 });
19036 buffer_1.update(cx, |buffer, _| {
19037 assert_eq!(buffer.text(), base_text_1);
19038 });
19039 buffer_2.update(cx, |buffer, _| {
19040 assert_eq!(buffer.text(), base_text_2);
19041 });
19042 buffer_3.update(cx, |buffer, _| {
19043 assert_eq!(buffer.text(), base_text_3);
19044 });
19045
19046 editor.update_in(cx, |editor, window, cx| {
19047 editor.undo(&Default::default(), window, cx);
19048 });
19049
19050 editor.update_in(cx, |editor, window, cx| {
19051 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19052 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19053 });
19054 editor.git_restore(&Default::default(), window, cx);
19055 });
19056
19057 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19058 // but not affect buffer_2 and its related excerpts.
19059 editor.update(cx, |editor, cx| {
19060 assert_eq!(
19061 editor.text(cx),
19062 "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}"
19063 );
19064 });
19065 buffer_1.update(cx, |buffer, _| {
19066 assert_eq!(buffer.text(), base_text_1);
19067 });
19068 buffer_2.update(cx, |buffer, _| {
19069 assert_eq!(
19070 buffer.text(),
19071 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19072 );
19073 });
19074 buffer_3.update(cx, |buffer, _| {
19075 assert_eq!(
19076 buffer.text(),
19077 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19078 );
19079 });
19080
19081 fn edit_first_char_of_every_line(text: &str) -> String {
19082 text.split('\n')
19083 .map(|line| format!("X{}", &line[1..]))
19084 .collect::<Vec<_>>()
19085 .join("\n")
19086 }
19087}
19088
19089#[gpui::test]
19090async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19091 init_test(cx, |_| {});
19092
19093 let cols = 4;
19094 let rows = 10;
19095 let sample_text_1 = sample_text(rows, cols, 'a');
19096 assert_eq!(
19097 sample_text_1,
19098 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19099 );
19100 let sample_text_2 = sample_text(rows, cols, 'l');
19101 assert_eq!(
19102 sample_text_2,
19103 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19104 );
19105 let sample_text_3 = sample_text(rows, cols, 'v');
19106 assert_eq!(
19107 sample_text_3,
19108 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19109 );
19110
19111 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19112 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19113 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19114
19115 let multi_buffer = cx.new(|cx| {
19116 let mut multibuffer = MultiBuffer::new(ReadWrite);
19117 multibuffer.push_excerpts(
19118 buffer_1.clone(),
19119 [
19120 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19121 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19122 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19123 ],
19124 cx,
19125 );
19126 multibuffer.push_excerpts(
19127 buffer_2.clone(),
19128 [
19129 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19130 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19131 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19132 ],
19133 cx,
19134 );
19135 multibuffer.push_excerpts(
19136 buffer_3.clone(),
19137 [
19138 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19139 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19140 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19141 ],
19142 cx,
19143 );
19144 multibuffer
19145 });
19146
19147 let fs = FakeFs::new(cx.executor());
19148 fs.insert_tree(
19149 "/a",
19150 json!({
19151 "main.rs": sample_text_1,
19152 "other.rs": sample_text_2,
19153 "lib.rs": sample_text_3,
19154 }),
19155 )
19156 .await;
19157 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19158 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19159 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19160 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19161 Editor::new(
19162 EditorMode::full(),
19163 multi_buffer,
19164 Some(project.clone()),
19165 window,
19166 cx,
19167 )
19168 });
19169 let multibuffer_item_id = workspace
19170 .update(cx, |workspace, window, cx| {
19171 assert!(
19172 workspace.active_item(cx).is_none(),
19173 "active item should be None before the first item is added"
19174 );
19175 workspace.add_item_to_active_pane(
19176 Box::new(multi_buffer_editor.clone()),
19177 None,
19178 true,
19179 window,
19180 cx,
19181 );
19182 let active_item = workspace
19183 .active_item(cx)
19184 .expect("should have an active item after adding the multi buffer");
19185 assert_eq!(
19186 active_item.buffer_kind(cx),
19187 ItemBufferKind::Multibuffer,
19188 "A multi buffer was expected to active after adding"
19189 );
19190 active_item.item_id()
19191 })
19192 .unwrap();
19193 cx.executor().run_until_parked();
19194
19195 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19196 editor.change_selections(
19197 SelectionEffects::scroll(Autoscroll::Next),
19198 window,
19199 cx,
19200 |s| s.select_ranges(Some(1..2)),
19201 );
19202 editor.open_excerpts(&OpenExcerpts, window, cx);
19203 });
19204 cx.executor().run_until_parked();
19205 let first_item_id = workspace
19206 .update(cx, |workspace, window, cx| {
19207 let active_item = workspace
19208 .active_item(cx)
19209 .expect("should have an active item after navigating into the 1st buffer");
19210 let first_item_id = active_item.item_id();
19211 assert_ne!(
19212 first_item_id, multibuffer_item_id,
19213 "Should navigate into the 1st buffer and activate it"
19214 );
19215 assert_eq!(
19216 active_item.buffer_kind(cx),
19217 ItemBufferKind::Singleton,
19218 "New active item should be a singleton buffer"
19219 );
19220 assert_eq!(
19221 active_item
19222 .act_as::<Editor>(cx)
19223 .expect("should have navigated into an editor for the 1st buffer")
19224 .read(cx)
19225 .text(cx),
19226 sample_text_1
19227 );
19228
19229 workspace
19230 .go_back(workspace.active_pane().downgrade(), window, cx)
19231 .detach_and_log_err(cx);
19232
19233 first_item_id
19234 })
19235 .unwrap();
19236 cx.executor().run_until_parked();
19237 workspace
19238 .update(cx, |workspace, _, cx| {
19239 let active_item = workspace
19240 .active_item(cx)
19241 .expect("should have an active item after navigating back");
19242 assert_eq!(
19243 active_item.item_id(),
19244 multibuffer_item_id,
19245 "Should navigate back to the multi buffer"
19246 );
19247 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19248 })
19249 .unwrap();
19250
19251 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19252 editor.change_selections(
19253 SelectionEffects::scroll(Autoscroll::Next),
19254 window,
19255 cx,
19256 |s| s.select_ranges(Some(39..40)),
19257 );
19258 editor.open_excerpts(&OpenExcerpts, window, cx);
19259 });
19260 cx.executor().run_until_parked();
19261 let second_item_id = workspace
19262 .update(cx, |workspace, window, cx| {
19263 let active_item = workspace
19264 .active_item(cx)
19265 .expect("should have an active item after navigating into the 2nd buffer");
19266 let second_item_id = active_item.item_id();
19267 assert_ne!(
19268 second_item_id, multibuffer_item_id,
19269 "Should navigate away from the multibuffer"
19270 );
19271 assert_ne!(
19272 second_item_id, first_item_id,
19273 "Should navigate into the 2nd buffer and activate it"
19274 );
19275 assert_eq!(
19276 active_item.buffer_kind(cx),
19277 ItemBufferKind::Singleton,
19278 "New active item should be a singleton buffer"
19279 );
19280 assert_eq!(
19281 active_item
19282 .act_as::<Editor>(cx)
19283 .expect("should have navigated into an editor")
19284 .read(cx)
19285 .text(cx),
19286 sample_text_2
19287 );
19288
19289 workspace
19290 .go_back(workspace.active_pane().downgrade(), window, cx)
19291 .detach_and_log_err(cx);
19292
19293 second_item_id
19294 })
19295 .unwrap();
19296 cx.executor().run_until_parked();
19297 workspace
19298 .update(cx, |workspace, _, cx| {
19299 let active_item = workspace
19300 .active_item(cx)
19301 .expect("should have an active item after navigating back from the 2nd buffer");
19302 assert_eq!(
19303 active_item.item_id(),
19304 multibuffer_item_id,
19305 "Should navigate back from the 2nd buffer to the multi buffer"
19306 );
19307 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19308 })
19309 .unwrap();
19310
19311 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19312 editor.change_selections(
19313 SelectionEffects::scroll(Autoscroll::Next),
19314 window,
19315 cx,
19316 |s| s.select_ranges(Some(70..70)),
19317 );
19318 editor.open_excerpts(&OpenExcerpts, window, cx);
19319 });
19320 cx.executor().run_until_parked();
19321 workspace
19322 .update(cx, |workspace, window, cx| {
19323 let active_item = workspace
19324 .active_item(cx)
19325 .expect("should have an active item after navigating into the 3rd buffer");
19326 let third_item_id = active_item.item_id();
19327 assert_ne!(
19328 third_item_id, multibuffer_item_id,
19329 "Should navigate into the 3rd buffer and activate it"
19330 );
19331 assert_ne!(third_item_id, first_item_id);
19332 assert_ne!(third_item_id, second_item_id);
19333 assert_eq!(
19334 active_item.buffer_kind(cx),
19335 ItemBufferKind::Singleton,
19336 "New active item should be a singleton buffer"
19337 );
19338 assert_eq!(
19339 active_item
19340 .act_as::<Editor>(cx)
19341 .expect("should have navigated into an editor")
19342 .read(cx)
19343 .text(cx),
19344 sample_text_3
19345 );
19346
19347 workspace
19348 .go_back(workspace.active_pane().downgrade(), window, cx)
19349 .detach_and_log_err(cx);
19350 })
19351 .unwrap();
19352 cx.executor().run_until_parked();
19353 workspace
19354 .update(cx, |workspace, _, cx| {
19355 let active_item = workspace
19356 .active_item(cx)
19357 .expect("should have an active item after navigating back from the 3rd buffer");
19358 assert_eq!(
19359 active_item.item_id(),
19360 multibuffer_item_id,
19361 "Should navigate back from the 3rd buffer to the multi buffer"
19362 );
19363 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19364 })
19365 .unwrap();
19366}
19367
19368#[gpui::test]
19369async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19370 init_test(cx, |_| {});
19371
19372 let mut cx = EditorTestContext::new(cx).await;
19373
19374 let diff_base = r#"
19375 use some::mod;
19376
19377 const A: u32 = 42;
19378
19379 fn main() {
19380 println!("hello");
19381
19382 println!("world");
19383 }
19384 "#
19385 .unindent();
19386
19387 cx.set_state(
19388 &r#"
19389 use some::modified;
19390
19391 ˇ
19392 fn main() {
19393 println!("hello there");
19394
19395 println!("around the");
19396 println!("world");
19397 }
19398 "#
19399 .unindent(),
19400 );
19401
19402 cx.set_head_text(&diff_base);
19403 executor.run_until_parked();
19404
19405 cx.update_editor(|editor, window, cx| {
19406 editor.go_to_next_hunk(&GoToHunk, window, cx);
19407 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19408 });
19409 executor.run_until_parked();
19410 cx.assert_state_with_diff(
19411 r#"
19412 use some::modified;
19413
19414
19415 fn main() {
19416 - println!("hello");
19417 + ˇ println!("hello there");
19418
19419 println!("around the");
19420 println!("world");
19421 }
19422 "#
19423 .unindent(),
19424 );
19425
19426 cx.update_editor(|editor, window, cx| {
19427 for _ in 0..2 {
19428 editor.go_to_next_hunk(&GoToHunk, window, cx);
19429 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19430 }
19431 });
19432 executor.run_until_parked();
19433 cx.assert_state_with_diff(
19434 r#"
19435 - use some::mod;
19436 + ˇuse some::modified;
19437
19438
19439 fn main() {
19440 - println!("hello");
19441 + println!("hello there");
19442
19443 + println!("around the");
19444 println!("world");
19445 }
19446 "#
19447 .unindent(),
19448 );
19449
19450 cx.update_editor(|editor, window, cx| {
19451 editor.go_to_next_hunk(&GoToHunk, window, cx);
19452 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19453 });
19454 executor.run_until_parked();
19455 cx.assert_state_with_diff(
19456 r#"
19457 - use some::mod;
19458 + use some::modified;
19459
19460 - const A: u32 = 42;
19461 ˇ
19462 fn main() {
19463 - println!("hello");
19464 + println!("hello there");
19465
19466 + println!("around the");
19467 println!("world");
19468 }
19469 "#
19470 .unindent(),
19471 );
19472
19473 cx.update_editor(|editor, window, cx| {
19474 editor.cancel(&Cancel, window, cx);
19475 });
19476
19477 cx.assert_state_with_diff(
19478 r#"
19479 use some::modified;
19480
19481 ˇ
19482 fn main() {
19483 println!("hello there");
19484
19485 println!("around the");
19486 println!("world");
19487 }
19488 "#
19489 .unindent(),
19490 );
19491}
19492
19493#[gpui::test]
19494async fn test_diff_base_change_with_expanded_diff_hunks(
19495 executor: BackgroundExecutor,
19496 cx: &mut TestAppContext,
19497) {
19498 init_test(cx, |_| {});
19499
19500 let mut cx = EditorTestContext::new(cx).await;
19501
19502 let diff_base = r#"
19503 use some::mod1;
19504 use some::mod2;
19505
19506 const A: u32 = 42;
19507 const B: u32 = 42;
19508 const C: u32 = 42;
19509
19510 fn main() {
19511 println!("hello");
19512
19513 println!("world");
19514 }
19515 "#
19516 .unindent();
19517
19518 cx.set_state(
19519 &r#"
19520 use some::mod2;
19521
19522 const A: u32 = 42;
19523 const C: u32 = 42;
19524
19525 fn main(ˇ) {
19526 //println!("hello");
19527
19528 println!("world");
19529 //
19530 //
19531 }
19532 "#
19533 .unindent(),
19534 );
19535
19536 cx.set_head_text(&diff_base);
19537 executor.run_until_parked();
19538
19539 cx.update_editor(|editor, window, cx| {
19540 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19541 });
19542 executor.run_until_parked();
19543 cx.assert_state_with_diff(
19544 r#"
19545 - use some::mod1;
19546 use some::mod2;
19547
19548 const A: u32 = 42;
19549 - const B: u32 = 42;
19550 const C: u32 = 42;
19551
19552 fn main(ˇ) {
19553 - println!("hello");
19554 + //println!("hello");
19555
19556 println!("world");
19557 + //
19558 + //
19559 }
19560 "#
19561 .unindent(),
19562 );
19563
19564 cx.set_head_text("new diff base!");
19565 executor.run_until_parked();
19566 cx.assert_state_with_diff(
19567 r#"
19568 - new diff base!
19569 + use some::mod2;
19570 +
19571 + const A: u32 = 42;
19572 + const C: u32 = 42;
19573 +
19574 + fn main(ˇ) {
19575 + //println!("hello");
19576 +
19577 + println!("world");
19578 + //
19579 + //
19580 + }
19581 "#
19582 .unindent(),
19583 );
19584}
19585
19586#[gpui::test]
19587async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19588 init_test(cx, |_| {});
19589
19590 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19591 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19592 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19593 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19594 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19595 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19596
19597 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19598 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19599 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19600
19601 let multi_buffer = cx.new(|cx| {
19602 let mut multibuffer = MultiBuffer::new(ReadWrite);
19603 multibuffer.push_excerpts(
19604 buffer_1.clone(),
19605 [
19606 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19607 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19608 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19609 ],
19610 cx,
19611 );
19612 multibuffer.push_excerpts(
19613 buffer_2.clone(),
19614 [
19615 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19616 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19617 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19618 ],
19619 cx,
19620 );
19621 multibuffer.push_excerpts(
19622 buffer_3.clone(),
19623 [
19624 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19625 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19626 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19627 ],
19628 cx,
19629 );
19630 multibuffer
19631 });
19632
19633 let editor =
19634 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19635 editor
19636 .update(cx, |editor, _window, cx| {
19637 for (buffer, diff_base) in [
19638 (buffer_1.clone(), file_1_old),
19639 (buffer_2.clone(), file_2_old),
19640 (buffer_3.clone(), file_3_old),
19641 ] {
19642 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19643 editor
19644 .buffer
19645 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19646 }
19647 })
19648 .unwrap();
19649
19650 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19651 cx.run_until_parked();
19652
19653 cx.assert_editor_state(
19654 &"
19655 ˇaaa
19656 ccc
19657 ddd
19658
19659 ggg
19660 hhh
19661
19662
19663 lll
19664 mmm
19665 NNN
19666
19667 qqq
19668 rrr
19669
19670 uuu
19671 111
19672 222
19673 333
19674
19675 666
19676 777
19677
19678 000
19679 !!!"
19680 .unindent(),
19681 );
19682
19683 cx.update_editor(|editor, window, cx| {
19684 editor.select_all(&SelectAll, window, cx);
19685 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19686 });
19687 cx.executor().run_until_parked();
19688
19689 cx.assert_state_with_diff(
19690 "
19691 «aaa
19692 - bbb
19693 ccc
19694 ddd
19695
19696 ggg
19697 hhh
19698
19699
19700 lll
19701 mmm
19702 - nnn
19703 + NNN
19704
19705 qqq
19706 rrr
19707
19708 uuu
19709 111
19710 222
19711 333
19712
19713 + 666
19714 777
19715
19716 000
19717 !!!ˇ»"
19718 .unindent(),
19719 );
19720}
19721
19722#[gpui::test]
19723async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19724 init_test(cx, |_| {});
19725
19726 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19727 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19728
19729 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19730 let multi_buffer = cx.new(|cx| {
19731 let mut multibuffer = MultiBuffer::new(ReadWrite);
19732 multibuffer.push_excerpts(
19733 buffer.clone(),
19734 [
19735 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19736 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19737 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19738 ],
19739 cx,
19740 );
19741 multibuffer
19742 });
19743
19744 let editor =
19745 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19746 editor
19747 .update(cx, |editor, _window, cx| {
19748 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19749 editor
19750 .buffer
19751 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19752 })
19753 .unwrap();
19754
19755 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19756 cx.run_until_parked();
19757
19758 cx.update_editor(|editor, window, cx| {
19759 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19760 });
19761 cx.executor().run_until_parked();
19762
19763 // When the start of a hunk coincides with the start of its excerpt,
19764 // the hunk is expanded. When the start of a hunk is earlier than
19765 // the start of its excerpt, the hunk is not expanded.
19766 cx.assert_state_with_diff(
19767 "
19768 ˇaaa
19769 - bbb
19770 + BBB
19771
19772 - ddd
19773 - eee
19774 + DDD
19775 + EEE
19776 fff
19777
19778 iii
19779 "
19780 .unindent(),
19781 );
19782}
19783
19784#[gpui::test]
19785async fn test_edits_around_expanded_insertion_hunks(
19786 executor: BackgroundExecutor,
19787 cx: &mut TestAppContext,
19788) {
19789 init_test(cx, |_| {});
19790
19791 let mut cx = EditorTestContext::new(cx).await;
19792
19793 let diff_base = r#"
19794 use some::mod1;
19795 use some::mod2;
19796
19797 const A: u32 = 42;
19798
19799 fn main() {
19800 println!("hello");
19801
19802 println!("world");
19803 }
19804 "#
19805 .unindent();
19806 executor.run_until_parked();
19807 cx.set_state(
19808 &r#"
19809 use some::mod1;
19810 use some::mod2;
19811
19812 const A: u32 = 42;
19813 const B: u32 = 42;
19814 const C: u32 = 42;
19815 ˇ
19816
19817 fn main() {
19818 println!("hello");
19819
19820 println!("world");
19821 }
19822 "#
19823 .unindent(),
19824 );
19825
19826 cx.set_head_text(&diff_base);
19827 executor.run_until_parked();
19828
19829 cx.update_editor(|editor, window, cx| {
19830 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19831 });
19832 executor.run_until_parked();
19833
19834 cx.assert_state_with_diff(
19835 r#"
19836 use some::mod1;
19837 use some::mod2;
19838
19839 const A: u32 = 42;
19840 + const B: u32 = 42;
19841 + const C: u32 = 42;
19842 + ˇ
19843
19844 fn main() {
19845 println!("hello");
19846
19847 println!("world");
19848 }
19849 "#
19850 .unindent(),
19851 );
19852
19853 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19854 executor.run_until_parked();
19855
19856 cx.assert_state_with_diff(
19857 r#"
19858 use some::mod1;
19859 use some::mod2;
19860
19861 const A: u32 = 42;
19862 + const B: u32 = 42;
19863 + const C: u32 = 42;
19864 + const D: u32 = 42;
19865 + ˇ
19866
19867 fn main() {
19868 println!("hello");
19869
19870 println!("world");
19871 }
19872 "#
19873 .unindent(),
19874 );
19875
19876 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19877 executor.run_until_parked();
19878
19879 cx.assert_state_with_diff(
19880 r#"
19881 use some::mod1;
19882 use some::mod2;
19883
19884 const A: u32 = 42;
19885 + const B: u32 = 42;
19886 + const C: u32 = 42;
19887 + const D: u32 = 42;
19888 + const E: u32 = 42;
19889 + ˇ
19890
19891 fn main() {
19892 println!("hello");
19893
19894 println!("world");
19895 }
19896 "#
19897 .unindent(),
19898 );
19899
19900 cx.update_editor(|editor, window, cx| {
19901 editor.delete_line(&DeleteLine, window, cx);
19902 });
19903 executor.run_until_parked();
19904
19905 cx.assert_state_with_diff(
19906 r#"
19907 use some::mod1;
19908 use some::mod2;
19909
19910 const A: u32 = 42;
19911 + const B: u32 = 42;
19912 + const C: u32 = 42;
19913 + const D: u32 = 42;
19914 + const E: u32 = 42;
19915 ˇ
19916 fn main() {
19917 println!("hello");
19918
19919 println!("world");
19920 }
19921 "#
19922 .unindent(),
19923 );
19924
19925 cx.update_editor(|editor, window, cx| {
19926 editor.move_up(&MoveUp, window, cx);
19927 editor.delete_line(&DeleteLine, window, cx);
19928 editor.move_up(&MoveUp, window, cx);
19929 editor.delete_line(&DeleteLine, window, cx);
19930 editor.move_up(&MoveUp, window, cx);
19931 editor.delete_line(&DeleteLine, window, cx);
19932 });
19933 executor.run_until_parked();
19934 cx.assert_state_with_diff(
19935 r#"
19936 use some::mod1;
19937 use some::mod2;
19938
19939 const A: u32 = 42;
19940 + const B: u32 = 42;
19941 ˇ
19942 fn main() {
19943 println!("hello");
19944
19945 println!("world");
19946 }
19947 "#
19948 .unindent(),
19949 );
19950
19951 cx.update_editor(|editor, window, cx| {
19952 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19953 editor.delete_line(&DeleteLine, window, cx);
19954 });
19955 executor.run_until_parked();
19956 cx.assert_state_with_diff(
19957 r#"
19958 ˇ
19959 fn main() {
19960 println!("hello");
19961
19962 println!("world");
19963 }
19964 "#
19965 .unindent(),
19966 );
19967}
19968
19969#[gpui::test]
19970async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19971 init_test(cx, |_| {});
19972
19973 let mut cx = EditorTestContext::new(cx).await;
19974 cx.set_head_text(indoc! { "
19975 one
19976 two
19977 three
19978 four
19979 five
19980 "
19981 });
19982 cx.set_state(indoc! { "
19983 one
19984 ˇthree
19985 five
19986 "});
19987 cx.run_until_parked();
19988 cx.update_editor(|editor, window, cx| {
19989 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19990 });
19991 cx.assert_state_with_diff(
19992 indoc! { "
19993 one
19994 - two
19995 ˇthree
19996 - four
19997 five
19998 "}
19999 .to_string(),
20000 );
20001 cx.update_editor(|editor, window, cx| {
20002 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20003 });
20004
20005 cx.assert_state_with_diff(
20006 indoc! { "
20007 one
20008 ˇthree
20009 five
20010 "}
20011 .to_string(),
20012 );
20013
20014 cx.set_state(indoc! { "
20015 one
20016 ˇTWO
20017 three
20018 four
20019 five
20020 "});
20021 cx.run_until_parked();
20022 cx.update_editor(|editor, window, cx| {
20023 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20024 });
20025
20026 cx.assert_state_with_diff(
20027 indoc! { "
20028 one
20029 - two
20030 + ˇTWO
20031 three
20032 four
20033 five
20034 "}
20035 .to_string(),
20036 );
20037 cx.update_editor(|editor, window, cx| {
20038 editor.move_up(&Default::default(), window, cx);
20039 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20040 });
20041 cx.assert_state_with_diff(
20042 indoc! { "
20043 one
20044 ˇTWO
20045 three
20046 four
20047 five
20048 "}
20049 .to_string(),
20050 );
20051}
20052
20053#[gpui::test]
20054async fn test_edits_around_expanded_deletion_hunks(
20055 executor: BackgroundExecutor,
20056 cx: &mut TestAppContext,
20057) {
20058 init_test(cx, |_| {});
20059
20060 let mut cx = EditorTestContext::new(cx).await;
20061
20062 let diff_base = r#"
20063 use some::mod1;
20064 use some::mod2;
20065
20066 const A: u32 = 42;
20067 const B: u32 = 42;
20068 const C: u32 = 42;
20069
20070
20071 fn main() {
20072 println!("hello");
20073
20074 println!("world");
20075 }
20076 "#
20077 .unindent();
20078 executor.run_until_parked();
20079 cx.set_state(
20080 &r#"
20081 use some::mod1;
20082 use some::mod2;
20083
20084 ˇconst B: u32 = 42;
20085 const C: u32 = 42;
20086
20087
20088 fn main() {
20089 println!("hello");
20090
20091 println!("world");
20092 }
20093 "#
20094 .unindent(),
20095 );
20096
20097 cx.set_head_text(&diff_base);
20098 executor.run_until_parked();
20099
20100 cx.update_editor(|editor, window, cx| {
20101 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20102 });
20103 executor.run_until_parked();
20104
20105 cx.assert_state_with_diff(
20106 r#"
20107 use some::mod1;
20108 use some::mod2;
20109
20110 - const A: u32 = 42;
20111 ˇconst B: u32 = 42;
20112 const C: u32 = 42;
20113
20114
20115 fn main() {
20116 println!("hello");
20117
20118 println!("world");
20119 }
20120 "#
20121 .unindent(),
20122 );
20123
20124 cx.update_editor(|editor, window, cx| {
20125 editor.delete_line(&DeleteLine, window, cx);
20126 });
20127 executor.run_until_parked();
20128 cx.assert_state_with_diff(
20129 r#"
20130 use some::mod1;
20131 use some::mod2;
20132
20133 - const A: u32 = 42;
20134 - const B: u32 = 42;
20135 ˇconst C: u32 = 42;
20136
20137
20138 fn main() {
20139 println!("hello");
20140
20141 println!("world");
20142 }
20143 "#
20144 .unindent(),
20145 );
20146
20147 cx.update_editor(|editor, window, cx| {
20148 editor.delete_line(&DeleteLine, window, cx);
20149 });
20150 executor.run_until_parked();
20151 cx.assert_state_with_diff(
20152 r#"
20153 use some::mod1;
20154 use some::mod2;
20155
20156 - const A: u32 = 42;
20157 - const B: u32 = 42;
20158 - const C: u32 = 42;
20159 ˇ
20160
20161 fn main() {
20162 println!("hello");
20163
20164 println!("world");
20165 }
20166 "#
20167 .unindent(),
20168 );
20169
20170 cx.update_editor(|editor, window, cx| {
20171 editor.handle_input("replacement", window, cx);
20172 });
20173 executor.run_until_parked();
20174 cx.assert_state_with_diff(
20175 r#"
20176 use some::mod1;
20177 use some::mod2;
20178
20179 - const A: u32 = 42;
20180 - const B: u32 = 42;
20181 - const C: u32 = 42;
20182 -
20183 + replacementˇ
20184
20185 fn main() {
20186 println!("hello");
20187
20188 println!("world");
20189 }
20190 "#
20191 .unindent(),
20192 );
20193}
20194
20195#[gpui::test]
20196async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20197 init_test(cx, |_| {});
20198
20199 let mut cx = EditorTestContext::new(cx).await;
20200
20201 let base_text = r#"
20202 one
20203 two
20204 three
20205 four
20206 five
20207 "#
20208 .unindent();
20209 executor.run_until_parked();
20210 cx.set_state(
20211 &r#"
20212 one
20213 two
20214 fˇour
20215 five
20216 "#
20217 .unindent(),
20218 );
20219
20220 cx.set_head_text(&base_text);
20221 executor.run_until_parked();
20222
20223 cx.update_editor(|editor, window, cx| {
20224 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20225 });
20226 executor.run_until_parked();
20227
20228 cx.assert_state_with_diff(
20229 r#"
20230 one
20231 two
20232 - three
20233 fˇour
20234 five
20235 "#
20236 .unindent(),
20237 );
20238
20239 cx.update_editor(|editor, window, cx| {
20240 editor.backspace(&Backspace, window, cx);
20241 editor.backspace(&Backspace, window, cx);
20242 });
20243 executor.run_until_parked();
20244 cx.assert_state_with_diff(
20245 r#"
20246 one
20247 two
20248 - threeˇ
20249 - four
20250 + our
20251 five
20252 "#
20253 .unindent(),
20254 );
20255}
20256
20257#[gpui::test]
20258async fn test_edit_after_expanded_modification_hunk(
20259 executor: BackgroundExecutor,
20260 cx: &mut TestAppContext,
20261) {
20262 init_test(cx, |_| {});
20263
20264 let mut cx = EditorTestContext::new(cx).await;
20265
20266 let diff_base = r#"
20267 use some::mod1;
20268 use some::mod2;
20269
20270 const A: u32 = 42;
20271 const B: u32 = 42;
20272 const C: u32 = 42;
20273 const D: u32 = 42;
20274
20275
20276 fn main() {
20277 println!("hello");
20278
20279 println!("world");
20280 }"#
20281 .unindent();
20282
20283 cx.set_state(
20284 &r#"
20285 use some::mod1;
20286 use some::mod2;
20287
20288 const A: u32 = 42;
20289 const B: u32 = 42;
20290 const C: u32 = 43ˇ
20291 const D: u32 = 42;
20292
20293
20294 fn main() {
20295 println!("hello");
20296
20297 println!("world");
20298 }"#
20299 .unindent(),
20300 );
20301
20302 cx.set_head_text(&diff_base);
20303 executor.run_until_parked();
20304 cx.update_editor(|editor, window, cx| {
20305 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20306 });
20307 executor.run_until_parked();
20308
20309 cx.assert_state_with_diff(
20310 r#"
20311 use some::mod1;
20312 use some::mod2;
20313
20314 const A: u32 = 42;
20315 const B: u32 = 42;
20316 - const C: u32 = 42;
20317 + const C: u32 = 43ˇ
20318 const D: u32 = 42;
20319
20320
20321 fn main() {
20322 println!("hello");
20323
20324 println!("world");
20325 }"#
20326 .unindent(),
20327 );
20328
20329 cx.update_editor(|editor, window, cx| {
20330 editor.handle_input("\nnew_line\n", window, cx);
20331 });
20332 executor.run_until_parked();
20333
20334 cx.assert_state_with_diff(
20335 r#"
20336 use some::mod1;
20337 use some::mod2;
20338
20339 const A: u32 = 42;
20340 const B: u32 = 42;
20341 - const C: u32 = 42;
20342 + const C: u32 = 43
20343 + new_line
20344 + ˇ
20345 const D: u32 = 42;
20346
20347
20348 fn main() {
20349 println!("hello");
20350
20351 println!("world");
20352 }"#
20353 .unindent(),
20354 );
20355}
20356
20357#[gpui::test]
20358async fn test_stage_and_unstage_added_file_hunk(
20359 executor: BackgroundExecutor,
20360 cx: &mut TestAppContext,
20361) {
20362 init_test(cx, |_| {});
20363
20364 let mut cx = EditorTestContext::new(cx).await;
20365 cx.update_editor(|editor, _, cx| {
20366 editor.set_expand_all_diff_hunks(cx);
20367 });
20368
20369 let working_copy = r#"
20370 ˇfn main() {
20371 println!("hello, world!");
20372 }
20373 "#
20374 .unindent();
20375
20376 cx.set_state(&working_copy);
20377 executor.run_until_parked();
20378
20379 cx.assert_state_with_diff(
20380 r#"
20381 + ˇfn main() {
20382 + println!("hello, world!");
20383 + }
20384 "#
20385 .unindent(),
20386 );
20387 cx.assert_index_text(None);
20388
20389 cx.update_editor(|editor, window, cx| {
20390 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20391 });
20392 executor.run_until_parked();
20393 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20394 cx.assert_state_with_diff(
20395 r#"
20396 + ˇfn main() {
20397 + println!("hello, world!");
20398 + }
20399 "#
20400 .unindent(),
20401 );
20402
20403 cx.update_editor(|editor, window, cx| {
20404 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20405 });
20406 executor.run_until_parked();
20407 cx.assert_index_text(None);
20408}
20409
20410async fn setup_indent_guides_editor(
20411 text: &str,
20412 cx: &mut TestAppContext,
20413) -> (BufferId, EditorTestContext) {
20414 init_test(cx, |_| {});
20415
20416 let mut cx = EditorTestContext::new(cx).await;
20417
20418 let buffer_id = cx.update_editor(|editor, window, cx| {
20419 editor.set_text(text, window, cx);
20420 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20421
20422 buffer_ids[0]
20423 });
20424
20425 (buffer_id, cx)
20426}
20427
20428fn assert_indent_guides(
20429 range: Range<u32>,
20430 expected: Vec<IndentGuide>,
20431 active_indices: Option<Vec<usize>>,
20432 cx: &mut EditorTestContext,
20433) {
20434 let indent_guides = cx.update_editor(|editor, window, cx| {
20435 let snapshot = editor.snapshot(window, cx).display_snapshot;
20436 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20437 editor,
20438 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20439 true,
20440 &snapshot,
20441 cx,
20442 );
20443
20444 indent_guides.sort_by(|a, b| {
20445 a.depth.cmp(&b.depth).then(
20446 a.start_row
20447 .cmp(&b.start_row)
20448 .then(a.end_row.cmp(&b.end_row)),
20449 )
20450 });
20451 indent_guides
20452 });
20453
20454 if let Some(expected) = active_indices {
20455 let active_indices = cx.update_editor(|editor, window, cx| {
20456 let snapshot = editor.snapshot(window, cx).display_snapshot;
20457 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20458 });
20459
20460 assert_eq!(
20461 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20462 expected,
20463 "Active indent guide indices do not match"
20464 );
20465 }
20466
20467 assert_eq!(indent_guides, expected, "Indent guides do not match");
20468}
20469
20470fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20471 IndentGuide {
20472 buffer_id,
20473 start_row: MultiBufferRow(start_row),
20474 end_row: MultiBufferRow(end_row),
20475 depth,
20476 tab_size: 4,
20477 settings: IndentGuideSettings {
20478 enabled: true,
20479 line_width: 1,
20480 active_line_width: 1,
20481 coloring: IndentGuideColoring::default(),
20482 background_coloring: IndentGuideBackgroundColoring::default(),
20483 },
20484 }
20485}
20486
20487#[gpui::test]
20488async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20489 let (buffer_id, mut cx) = setup_indent_guides_editor(
20490 &"
20491 fn main() {
20492 let a = 1;
20493 }"
20494 .unindent(),
20495 cx,
20496 )
20497 .await;
20498
20499 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20500}
20501
20502#[gpui::test]
20503async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20504 let (buffer_id, mut cx) = setup_indent_guides_editor(
20505 &"
20506 fn main() {
20507 let a = 1;
20508 let b = 2;
20509 }"
20510 .unindent(),
20511 cx,
20512 )
20513 .await;
20514
20515 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20516}
20517
20518#[gpui::test]
20519async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20520 let (buffer_id, mut cx) = setup_indent_guides_editor(
20521 &"
20522 fn main() {
20523 let a = 1;
20524 if a == 3 {
20525 let b = 2;
20526 } else {
20527 let c = 3;
20528 }
20529 }"
20530 .unindent(),
20531 cx,
20532 )
20533 .await;
20534
20535 assert_indent_guides(
20536 0..8,
20537 vec![
20538 indent_guide(buffer_id, 1, 6, 0),
20539 indent_guide(buffer_id, 3, 3, 1),
20540 indent_guide(buffer_id, 5, 5, 1),
20541 ],
20542 None,
20543 &mut cx,
20544 );
20545}
20546
20547#[gpui::test]
20548async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20549 let (buffer_id, mut cx) = setup_indent_guides_editor(
20550 &"
20551 fn main() {
20552 let a = 1;
20553 let b = 2;
20554 let c = 3;
20555 }"
20556 .unindent(),
20557 cx,
20558 )
20559 .await;
20560
20561 assert_indent_guides(
20562 0..5,
20563 vec![
20564 indent_guide(buffer_id, 1, 3, 0),
20565 indent_guide(buffer_id, 2, 2, 1),
20566 ],
20567 None,
20568 &mut cx,
20569 );
20570}
20571
20572#[gpui::test]
20573async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20574 let (buffer_id, mut cx) = setup_indent_guides_editor(
20575 &"
20576 fn main() {
20577 let a = 1;
20578
20579 let c = 3;
20580 }"
20581 .unindent(),
20582 cx,
20583 )
20584 .await;
20585
20586 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20587}
20588
20589#[gpui::test]
20590async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20591 let (buffer_id, mut cx) = setup_indent_guides_editor(
20592 &"
20593 fn main() {
20594 let a = 1;
20595
20596 let c = 3;
20597
20598 if a == 3 {
20599 let b = 2;
20600 } else {
20601 let c = 3;
20602 }
20603 }"
20604 .unindent(),
20605 cx,
20606 )
20607 .await;
20608
20609 assert_indent_guides(
20610 0..11,
20611 vec![
20612 indent_guide(buffer_id, 1, 9, 0),
20613 indent_guide(buffer_id, 6, 6, 1),
20614 indent_guide(buffer_id, 8, 8, 1),
20615 ],
20616 None,
20617 &mut cx,
20618 );
20619}
20620
20621#[gpui::test]
20622async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20623 let (buffer_id, mut cx) = setup_indent_guides_editor(
20624 &"
20625 fn main() {
20626 let a = 1;
20627
20628 let c = 3;
20629
20630 if a == 3 {
20631 let b = 2;
20632 } else {
20633 let c = 3;
20634 }
20635 }"
20636 .unindent(),
20637 cx,
20638 )
20639 .await;
20640
20641 assert_indent_guides(
20642 1..11,
20643 vec![
20644 indent_guide(buffer_id, 1, 9, 0),
20645 indent_guide(buffer_id, 6, 6, 1),
20646 indent_guide(buffer_id, 8, 8, 1),
20647 ],
20648 None,
20649 &mut cx,
20650 );
20651}
20652
20653#[gpui::test]
20654async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20655 let (buffer_id, mut cx) = setup_indent_guides_editor(
20656 &"
20657 fn main() {
20658 let a = 1;
20659
20660 let c = 3;
20661
20662 if a == 3 {
20663 let b = 2;
20664 } else {
20665 let c = 3;
20666 }
20667 }"
20668 .unindent(),
20669 cx,
20670 )
20671 .await;
20672
20673 assert_indent_guides(
20674 1..10,
20675 vec![
20676 indent_guide(buffer_id, 1, 9, 0),
20677 indent_guide(buffer_id, 6, 6, 1),
20678 indent_guide(buffer_id, 8, 8, 1),
20679 ],
20680 None,
20681 &mut cx,
20682 );
20683}
20684
20685#[gpui::test]
20686async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20687 let (buffer_id, mut cx) = setup_indent_guides_editor(
20688 &"
20689 fn main() {
20690 if a {
20691 b(
20692 c,
20693 d,
20694 )
20695 } else {
20696 e(
20697 f
20698 )
20699 }
20700 }"
20701 .unindent(),
20702 cx,
20703 )
20704 .await;
20705
20706 assert_indent_guides(
20707 0..11,
20708 vec![
20709 indent_guide(buffer_id, 1, 10, 0),
20710 indent_guide(buffer_id, 2, 5, 1),
20711 indent_guide(buffer_id, 7, 9, 1),
20712 indent_guide(buffer_id, 3, 4, 2),
20713 indent_guide(buffer_id, 8, 8, 2),
20714 ],
20715 None,
20716 &mut cx,
20717 );
20718
20719 cx.update_editor(|editor, window, cx| {
20720 editor.fold_at(MultiBufferRow(2), window, cx);
20721 assert_eq!(
20722 editor.display_text(cx),
20723 "
20724 fn main() {
20725 if a {
20726 b(⋯
20727 )
20728 } else {
20729 e(
20730 f
20731 )
20732 }
20733 }"
20734 .unindent()
20735 );
20736 });
20737
20738 assert_indent_guides(
20739 0..11,
20740 vec![
20741 indent_guide(buffer_id, 1, 10, 0),
20742 indent_guide(buffer_id, 2, 5, 1),
20743 indent_guide(buffer_id, 7, 9, 1),
20744 indent_guide(buffer_id, 8, 8, 2),
20745 ],
20746 None,
20747 &mut cx,
20748 );
20749}
20750
20751#[gpui::test]
20752async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20753 let (buffer_id, mut cx) = setup_indent_guides_editor(
20754 &"
20755 block1
20756 block2
20757 block3
20758 block4
20759 block2
20760 block1
20761 block1"
20762 .unindent(),
20763 cx,
20764 )
20765 .await;
20766
20767 assert_indent_guides(
20768 1..10,
20769 vec![
20770 indent_guide(buffer_id, 1, 4, 0),
20771 indent_guide(buffer_id, 2, 3, 1),
20772 indent_guide(buffer_id, 3, 3, 2),
20773 ],
20774 None,
20775 &mut cx,
20776 );
20777}
20778
20779#[gpui::test]
20780async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20781 let (buffer_id, mut cx) = setup_indent_guides_editor(
20782 &"
20783 block1
20784 block2
20785 block3
20786
20787 block1
20788 block1"
20789 .unindent(),
20790 cx,
20791 )
20792 .await;
20793
20794 assert_indent_guides(
20795 0..6,
20796 vec![
20797 indent_guide(buffer_id, 1, 2, 0),
20798 indent_guide(buffer_id, 2, 2, 1),
20799 ],
20800 None,
20801 &mut cx,
20802 );
20803}
20804
20805#[gpui::test]
20806async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20807 let (buffer_id, mut cx) = setup_indent_guides_editor(
20808 &"
20809 function component() {
20810 \treturn (
20811 \t\t\t
20812 \t\t<div>
20813 \t\t\t<abc></abc>
20814 \t\t</div>
20815 \t)
20816 }"
20817 .unindent(),
20818 cx,
20819 )
20820 .await;
20821
20822 assert_indent_guides(
20823 0..8,
20824 vec![
20825 indent_guide(buffer_id, 1, 6, 0),
20826 indent_guide(buffer_id, 2, 5, 1),
20827 indent_guide(buffer_id, 4, 4, 2),
20828 ],
20829 None,
20830 &mut cx,
20831 );
20832}
20833
20834#[gpui::test]
20835async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20836 let (buffer_id, mut cx) = setup_indent_guides_editor(
20837 &"
20838 function component() {
20839 \treturn (
20840 \t
20841 \t\t<div>
20842 \t\t\t<abc></abc>
20843 \t\t</div>
20844 \t)
20845 }"
20846 .unindent(),
20847 cx,
20848 )
20849 .await;
20850
20851 assert_indent_guides(
20852 0..8,
20853 vec![
20854 indent_guide(buffer_id, 1, 6, 0),
20855 indent_guide(buffer_id, 2, 5, 1),
20856 indent_guide(buffer_id, 4, 4, 2),
20857 ],
20858 None,
20859 &mut cx,
20860 );
20861}
20862
20863#[gpui::test]
20864async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20865 let (buffer_id, mut cx) = setup_indent_guides_editor(
20866 &"
20867 block1
20868
20869
20870
20871 block2
20872 "
20873 .unindent(),
20874 cx,
20875 )
20876 .await;
20877
20878 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20879}
20880
20881#[gpui::test]
20882async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20883 let (buffer_id, mut cx) = setup_indent_guides_editor(
20884 &"
20885 def a:
20886 \tb = 3
20887 \tif True:
20888 \t\tc = 4
20889 \t\td = 5
20890 \tprint(b)
20891 "
20892 .unindent(),
20893 cx,
20894 )
20895 .await;
20896
20897 assert_indent_guides(
20898 0..6,
20899 vec![
20900 indent_guide(buffer_id, 1, 5, 0),
20901 indent_guide(buffer_id, 3, 4, 1),
20902 ],
20903 None,
20904 &mut cx,
20905 );
20906}
20907
20908#[gpui::test]
20909async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20910 let (buffer_id, mut cx) = setup_indent_guides_editor(
20911 &"
20912 fn main() {
20913 let a = 1;
20914 }"
20915 .unindent(),
20916 cx,
20917 )
20918 .await;
20919
20920 cx.update_editor(|editor, window, cx| {
20921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20922 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20923 });
20924 });
20925
20926 assert_indent_guides(
20927 0..3,
20928 vec![indent_guide(buffer_id, 1, 1, 0)],
20929 Some(vec![0]),
20930 &mut cx,
20931 );
20932}
20933
20934#[gpui::test]
20935async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20936 let (buffer_id, mut cx) = setup_indent_guides_editor(
20937 &"
20938 fn main() {
20939 if 1 == 2 {
20940 let a = 1;
20941 }
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..4,
20956 vec![
20957 indent_guide(buffer_id, 1, 3, 0),
20958 indent_guide(buffer_id, 2, 2, 1),
20959 ],
20960 Some(vec![1]),
20961 &mut cx,
20962 );
20963
20964 cx.update_editor(|editor, window, cx| {
20965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20966 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20967 });
20968 });
20969
20970 assert_indent_guides(
20971 0..4,
20972 vec![
20973 indent_guide(buffer_id, 1, 3, 0),
20974 indent_guide(buffer_id, 2, 2, 1),
20975 ],
20976 Some(vec![1]),
20977 &mut cx,
20978 );
20979
20980 cx.update_editor(|editor, window, cx| {
20981 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20982 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20983 });
20984 });
20985
20986 assert_indent_guides(
20987 0..4,
20988 vec![
20989 indent_guide(buffer_id, 1, 3, 0),
20990 indent_guide(buffer_id, 2, 2, 1),
20991 ],
20992 Some(vec![0]),
20993 &mut cx,
20994 );
20995}
20996
20997#[gpui::test]
20998async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20999 let (buffer_id, mut cx) = setup_indent_guides_editor(
21000 &"
21001 fn main() {
21002 let a = 1;
21003
21004 let b = 2;
21005 }"
21006 .unindent(),
21007 cx,
21008 )
21009 .await;
21010
21011 cx.update_editor(|editor, window, cx| {
21012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21013 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21014 });
21015 });
21016
21017 assert_indent_guides(
21018 0..5,
21019 vec![indent_guide(buffer_id, 1, 3, 0)],
21020 Some(vec![0]),
21021 &mut cx,
21022 );
21023}
21024
21025#[gpui::test]
21026async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21027 let (buffer_id, mut cx) = setup_indent_guides_editor(
21028 &"
21029 def m:
21030 a = 1
21031 pass"
21032 .unindent(),
21033 cx,
21034 )
21035 .await;
21036
21037 cx.update_editor(|editor, window, cx| {
21038 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21039 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21040 });
21041 });
21042
21043 assert_indent_guides(
21044 0..3,
21045 vec![indent_guide(buffer_id, 1, 2, 0)],
21046 Some(vec![0]),
21047 &mut cx,
21048 );
21049}
21050
21051#[gpui::test]
21052async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21053 init_test(cx, |_| {});
21054 let mut cx = EditorTestContext::new(cx).await;
21055 let text = indoc! {
21056 "
21057 impl A {
21058 fn b() {
21059 0;
21060 3;
21061 5;
21062 6;
21063 7;
21064 }
21065 }
21066 "
21067 };
21068 let base_text = indoc! {
21069 "
21070 impl A {
21071 fn b() {
21072 0;
21073 1;
21074 2;
21075 3;
21076 4;
21077 }
21078 fn c() {
21079 5;
21080 6;
21081 7;
21082 }
21083 }
21084 "
21085 };
21086
21087 cx.update_editor(|editor, window, cx| {
21088 editor.set_text(text, window, cx);
21089
21090 editor.buffer().update(cx, |multibuffer, cx| {
21091 let buffer = multibuffer.as_singleton().unwrap();
21092 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21093
21094 multibuffer.set_all_diff_hunks_expanded(cx);
21095 multibuffer.add_diff(diff, cx);
21096
21097 buffer.read(cx).remote_id()
21098 })
21099 });
21100 cx.run_until_parked();
21101
21102 cx.assert_state_with_diff(
21103 indoc! { "
21104 impl A {
21105 fn b() {
21106 0;
21107 - 1;
21108 - 2;
21109 3;
21110 - 4;
21111 - }
21112 - fn c() {
21113 5;
21114 6;
21115 7;
21116 }
21117 }
21118 ˇ"
21119 }
21120 .to_string(),
21121 );
21122
21123 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21124 editor
21125 .snapshot(window, cx)
21126 .buffer_snapshot()
21127 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21128 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21129 .collect::<Vec<_>>()
21130 });
21131 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21132 assert_eq!(
21133 actual_guides,
21134 vec![
21135 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21136 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21137 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21138 ]
21139 );
21140}
21141
21142#[gpui::test]
21143async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21144 init_test(cx, |_| {});
21145 let mut cx = EditorTestContext::new(cx).await;
21146
21147 let diff_base = r#"
21148 a
21149 b
21150 c
21151 "#
21152 .unindent();
21153
21154 cx.set_state(
21155 &r#"
21156 ˇA
21157 b
21158 C
21159 "#
21160 .unindent(),
21161 );
21162 cx.set_head_text(&diff_base);
21163 cx.update_editor(|editor, window, cx| {
21164 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21165 });
21166 executor.run_until_parked();
21167
21168 let both_hunks_expanded = r#"
21169 - a
21170 + ˇA
21171 b
21172 - c
21173 + C
21174 "#
21175 .unindent();
21176
21177 cx.assert_state_with_diff(both_hunks_expanded.clone());
21178
21179 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21180 let snapshot = editor.snapshot(window, cx);
21181 let hunks = editor
21182 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21183 .collect::<Vec<_>>();
21184 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21185 let buffer_id = hunks[0].buffer_id;
21186 hunks
21187 .into_iter()
21188 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21189 .collect::<Vec<_>>()
21190 });
21191 assert_eq!(hunk_ranges.len(), 2);
21192
21193 cx.update_editor(|editor, _, cx| {
21194 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21195 });
21196 executor.run_until_parked();
21197
21198 let second_hunk_expanded = r#"
21199 ˇA
21200 b
21201 - c
21202 + C
21203 "#
21204 .unindent();
21205
21206 cx.assert_state_with_diff(second_hunk_expanded);
21207
21208 cx.update_editor(|editor, _, cx| {
21209 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21210 });
21211 executor.run_until_parked();
21212
21213 cx.assert_state_with_diff(both_hunks_expanded.clone());
21214
21215 cx.update_editor(|editor, _, cx| {
21216 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21217 });
21218 executor.run_until_parked();
21219
21220 let first_hunk_expanded = r#"
21221 - a
21222 + ˇA
21223 b
21224 C
21225 "#
21226 .unindent();
21227
21228 cx.assert_state_with_diff(first_hunk_expanded);
21229
21230 cx.update_editor(|editor, _, cx| {
21231 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21232 });
21233 executor.run_until_parked();
21234
21235 cx.assert_state_with_diff(both_hunks_expanded);
21236
21237 cx.set_state(
21238 &r#"
21239 ˇA
21240 b
21241 "#
21242 .unindent(),
21243 );
21244 cx.run_until_parked();
21245
21246 // TODO this cursor position seems bad
21247 cx.assert_state_with_diff(
21248 r#"
21249 - ˇa
21250 + A
21251 b
21252 "#
21253 .unindent(),
21254 );
21255
21256 cx.update_editor(|editor, window, cx| {
21257 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21258 });
21259
21260 cx.assert_state_with_diff(
21261 r#"
21262 - ˇa
21263 + A
21264 b
21265 - c
21266 "#
21267 .unindent(),
21268 );
21269
21270 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21271 let snapshot = editor.snapshot(window, cx);
21272 let hunks = editor
21273 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21274 .collect::<Vec<_>>();
21275 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21276 let buffer_id = hunks[0].buffer_id;
21277 hunks
21278 .into_iter()
21279 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21280 .collect::<Vec<_>>()
21281 });
21282 assert_eq!(hunk_ranges.len(), 2);
21283
21284 cx.update_editor(|editor, _, cx| {
21285 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21286 });
21287 executor.run_until_parked();
21288
21289 cx.assert_state_with_diff(
21290 r#"
21291 - ˇa
21292 + A
21293 b
21294 "#
21295 .unindent(),
21296 );
21297}
21298
21299#[gpui::test]
21300async fn test_toggle_deletion_hunk_at_start_of_file(
21301 executor: BackgroundExecutor,
21302 cx: &mut TestAppContext,
21303) {
21304 init_test(cx, |_| {});
21305 let mut cx = EditorTestContext::new(cx).await;
21306
21307 let diff_base = r#"
21308 a
21309 b
21310 c
21311 "#
21312 .unindent();
21313
21314 cx.set_state(
21315 &r#"
21316 ˇb
21317 c
21318 "#
21319 .unindent(),
21320 );
21321 cx.set_head_text(&diff_base);
21322 cx.update_editor(|editor, window, cx| {
21323 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21324 });
21325 executor.run_until_parked();
21326
21327 let hunk_expanded = r#"
21328 - a
21329 ˇb
21330 c
21331 "#
21332 .unindent();
21333
21334 cx.assert_state_with_diff(hunk_expanded.clone());
21335
21336 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21337 let snapshot = editor.snapshot(window, cx);
21338 let hunks = editor
21339 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21340 .collect::<Vec<_>>();
21341 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21342 let buffer_id = hunks[0].buffer_id;
21343 hunks
21344 .into_iter()
21345 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21346 .collect::<Vec<_>>()
21347 });
21348 assert_eq!(hunk_ranges.len(), 1);
21349
21350 cx.update_editor(|editor, _, cx| {
21351 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21352 });
21353 executor.run_until_parked();
21354
21355 let hunk_collapsed = r#"
21356 ˇb
21357 c
21358 "#
21359 .unindent();
21360
21361 cx.assert_state_with_diff(hunk_collapsed);
21362
21363 cx.update_editor(|editor, _, cx| {
21364 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21365 });
21366 executor.run_until_parked();
21367
21368 cx.assert_state_with_diff(hunk_expanded);
21369}
21370
21371#[gpui::test]
21372async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21373 init_test(cx, |_| {});
21374
21375 let fs = FakeFs::new(cx.executor());
21376 fs.insert_tree(
21377 path!("/test"),
21378 json!({
21379 ".git": {},
21380 "file-1": "ONE\n",
21381 "file-2": "TWO\n",
21382 "file-3": "THREE\n",
21383 }),
21384 )
21385 .await;
21386
21387 fs.set_head_for_repo(
21388 path!("/test/.git").as_ref(),
21389 &[
21390 ("file-1", "one\n".into()),
21391 ("file-2", "two\n".into()),
21392 ("file-3", "three\n".into()),
21393 ],
21394 "deadbeef",
21395 );
21396
21397 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21398 let mut buffers = vec![];
21399 for i in 1..=3 {
21400 let buffer = project
21401 .update(cx, |project, cx| {
21402 let path = format!(path!("/test/file-{}"), i);
21403 project.open_local_buffer(path, cx)
21404 })
21405 .await
21406 .unwrap();
21407 buffers.push(buffer);
21408 }
21409
21410 let multibuffer = cx.new(|cx| {
21411 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21412 multibuffer.set_all_diff_hunks_expanded(cx);
21413 for buffer in &buffers {
21414 let snapshot = buffer.read(cx).snapshot();
21415 multibuffer.set_excerpts_for_path(
21416 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21417 buffer.clone(),
21418 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21419 2,
21420 cx,
21421 );
21422 }
21423 multibuffer
21424 });
21425
21426 let editor = cx.add_window(|window, cx| {
21427 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21428 });
21429 cx.run_until_parked();
21430
21431 let snapshot = editor
21432 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21433 .unwrap();
21434 let hunks = snapshot
21435 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21436 .map(|hunk| match hunk {
21437 DisplayDiffHunk::Unfolded {
21438 display_row_range, ..
21439 } => display_row_range,
21440 DisplayDiffHunk::Folded { .. } => unreachable!(),
21441 })
21442 .collect::<Vec<_>>();
21443 assert_eq!(
21444 hunks,
21445 [
21446 DisplayRow(2)..DisplayRow(4),
21447 DisplayRow(7)..DisplayRow(9),
21448 DisplayRow(12)..DisplayRow(14),
21449 ]
21450 );
21451}
21452
21453#[gpui::test]
21454async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21455 init_test(cx, |_| {});
21456
21457 let mut cx = EditorTestContext::new(cx).await;
21458 cx.set_head_text(indoc! { "
21459 one
21460 two
21461 three
21462 four
21463 five
21464 "
21465 });
21466 cx.set_index_text(indoc! { "
21467 one
21468 two
21469 three
21470 four
21471 five
21472 "
21473 });
21474 cx.set_state(indoc! {"
21475 one
21476 TWO
21477 ˇTHREE
21478 FOUR
21479 five
21480 "});
21481 cx.run_until_parked();
21482 cx.update_editor(|editor, window, cx| {
21483 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21484 });
21485 cx.run_until_parked();
21486 cx.assert_index_text(Some(indoc! {"
21487 one
21488 TWO
21489 THREE
21490 FOUR
21491 five
21492 "}));
21493 cx.set_state(indoc! { "
21494 one
21495 TWO
21496 ˇTHREE-HUNDRED
21497 FOUR
21498 five
21499 "});
21500 cx.run_until_parked();
21501 cx.update_editor(|editor, window, cx| {
21502 let snapshot = editor.snapshot(window, cx);
21503 let hunks = editor
21504 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21505 .collect::<Vec<_>>();
21506 assert_eq!(hunks.len(), 1);
21507 assert_eq!(
21508 hunks[0].status(),
21509 DiffHunkStatus {
21510 kind: DiffHunkStatusKind::Modified,
21511 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21512 }
21513 );
21514
21515 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21516 });
21517 cx.run_until_parked();
21518 cx.assert_index_text(Some(indoc! {"
21519 one
21520 TWO
21521 THREE-HUNDRED
21522 FOUR
21523 five
21524 "}));
21525}
21526
21527#[gpui::test]
21528fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21529 init_test(cx, |_| {});
21530
21531 let editor = cx.add_window(|window, cx| {
21532 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21533 build_editor(buffer, window, cx)
21534 });
21535
21536 let render_args = Arc::new(Mutex::new(None));
21537 let snapshot = editor
21538 .update(cx, |editor, window, cx| {
21539 let snapshot = editor.buffer().read(cx).snapshot(cx);
21540 let range =
21541 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21542
21543 struct RenderArgs {
21544 row: MultiBufferRow,
21545 folded: bool,
21546 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21547 }
21548
21549 let crease = Crease::inline(
21550 range,
21551 FoldPlaceholder::test(),
21552 {
21553 let toggle_callback = render_args.clone();
21554 move |row, folded, callback, _window, _cx| {
21555 *toggle_callback.lock() = Some(RenderArgs {
21556 row,
21557 folded,
21558 callback,
21559 });
21560 div()
21561 }
21562 },
21563 |_row, _folded, _window, _cx| div(),
21564 );
21565
21566 editor.insert_creases(Some(crease), cx);
21567 let snapshot = editor.snapshot(window, cx);
21568 let _div =
21569 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21570 snapshot
21571 })
21572 .unwrap();
21573
21574 let render_args = render_args.lock().take().unwrap();
21575 assert_eq!(render_args.row, MultiBufferRow(1));
21576 assert!(!render_args.folded);
21577 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21578
21579 cx.update_window(*editor, |_, window, cx| {
21580 (render_args.callback)(true, window, cx)
21581 })
21582 .unwrap();
21583 let snapshot = editor
21584 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21585 .unwrap();
21586 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21587
21588 cx.update_window(*editor, |_, window, cx| {
21589 (render_args.callback)(false, window, cx)
21590 })
21591 .unwrap();
21592 let snapshot = editor
21593 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21594 .unwrap();
21595 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21596}
21597
21598#[gpui::test]
21599async fn test_input_text(cx: &mut TestAppContext) {
21600 init_test(cx, |_| {});
21601 let mut cx = EditorTestContext::new(cx).await;
21602
21603 cx.set_state(
21604 &r#"ˇone
21605 two
21606
21607 three
21608 fourˇ
21609 five
21610
21611 siˇx"#
21612 .unindent(),
21613 );
21614
21615 cx.dispatch_action(HandleInput(String::new()));
21616 cx.assert_editor_state(
21617 &r#"ˇone
21618 two
21619
21620 three
21621 fourˇ
21622 five
21623
21624 siˇx"#
21625 .unindent(),
21626 );
21627
21628 cx.dispatch_action(HandleInput("AAAA".to_string()));
21629 cx.assert_editor_state(
21630 &r#"AAAAˇone
21631 two
21632
21633 three
21634 fourAAAAˇ
21635 five
21636
21637 siAAAAˇx"#
21638 .unindent(),
21639 );
21640}
21641
21642#[gpui::test]
21643async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21644 init_test(cx, |_| {});
21645
21646 let mut cx = EditorTestContext::new(cx).await;
21647 cx.set_state(
21648 r#"let foo = 1;
21649let foo = 2;
21650let foo = 3;
21651let fooˇ = 4;
21652let foo = 5;
21653let foo = 6;
21654let foo = 7;
21655let foo = 8;
21656let foo = 9;
21657let foo = 10;
21658let foo = 11;
21659let foo = 12;
21660let foo = 13;
21661let foo = 14;
21662let foo = 15;"#,
21663 );
21664
21665 cx.update_editor(|e, window, cx| {
21666 assert_eq!(
21667 e.next_scroll_position,
21668 NextScrollCursorCenterTopBottom::Center,
21669 "Default next scroll direction is center",
21670 );
21671
21672 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21673 assert_eq!(
21674 e.next_scroll_position,
21675 NextScrollCursorCenterTopBottom::Top,
21676 "After center, next scroll direction should be top",
21677 );
21678
21679 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21680 assert_eq!(
21681 e.next_scroll_position,
21682 NextScrollCursorCenterTopBottom::Bottom,
21683 "After top, next scroll direction should be bottom",
21684 );
21685
21686 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21687 assert_eq!(
21688 e.next_scroll_position,
21689 NextScrollCursorCenterTopBottom::Center,
21690 "After bottom, scrolling should start over",
21691 );
21692
21693 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21694 assert_eq!(
21695 e.next_scroll_position,
21696 NextScrollCursorCenterTopBottom::Top,
21697 "Scrolling continues if retriggered fast enough"
21698 );
21699 });
21700
21701 cx.executor()
21702 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21703 cx.executor().run_until_parked();
21704 cx.update_editor(|e, _, _| {
21705 assert_eq!(
21706 e.next_scroll_position,
21707 NextScrollCursorCenterTopBottom::Center,
21708 "If scrolling is not triggered fast enough, it should reset"
21709 );
21710 });
21711}
21712
21713#[gpui::test]
21714async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21715 init_test(cx, |_| {});
21716 let mut cx = EditorLspTestContext::new_rust(
21717 lsp::ServerCapabilities {
21718 definition_provider: Some(lsp::OneOf::Left(true)),
21719 references_provider: Some(lsp::OneOf::Left(true)),
21720 ..lsp::ServerCapabilities::default()
21721 },
21722 cx,
21723 )
21724 .await;
21725
21726 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21727 let go_to_definition = cx
21728 .lsp
21729 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21730 move |params, _| async move {
21731 if empty_go_to_definition {
21732 Ok(None)
21733 } else {
21734 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21735 uri: params.text_document_position_params.text_document.uri,
21736 range: lsp::Range::new(
21737 lsp::Position::new(4, 3),
21738 lsp::Position::new(4, 6),
21739 ),
21740 })))
21741 }
21742 },
21743 );
21744 let references = cx
21745 .lsp
21746 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21747 Ok(Some(vec![lsp::Location {
21748 uri: params.text_document_position.text_document.uri,
21749 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21750 }]))
21751 });
21752 (go_to_definition, references)
21753 };
21754
21755 cx.set_state(
21756 &r#"fn one() {
21757 let mut a = ˇtwo();
21758 }
21759
21760 fn two() {}"#
21761 .unindent(),
21762 );
21763 set_up_lsp_handlers(false, &mut cx);
21764 let navigated = cx
21765 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21766 .await
21767 .expect("Failed to navigate to definition");
21768 assert_eq!(
21769 navigated,
21770 Navigated::Yes,
21771 "Should have navigated to definition from the GetDefinition response"
21772 );
21773 cx.assert_editor_state(
21774 &r#"fn one() {
21775 let mut a = two();
21776 }
21777
21778 fn «twoˇ»() {}"#
21779 .unindent(),
21780 );
21781
21782 let editors = cx.update_workspace(|workspace, _, cx| {
21783 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21784 });
21785 cx.update_editor(|_, _, test_editor_cx| {
21786 assert_eq!(
21787 editors.len(),
21788 1,
21789 "Initially, only one, test, editor should be open in the workspace"
21790 );
21791 assert_eq!(
21792 test_editor_cx.entity(),
21793 editors.last().expect("Asserted len is 1").clone()
21794 );
21795 });
21796
21797 set_up_lsp_handlers(true, &mut cx);
21798 let navigated = cx
21799 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21800 .await
21801 .expect("Failed to navigate to lookup references");
21802 assert_eq!(
21803 navigated,
21804 Navigated::Yes,
21805 "Should have navigated to references as a fallback after empty GoToDefinition response"
21806 );
21807 // We should not change the selections in the existing file,
21808 // if opening another milti buffer with the references
21809 cx.assert_editor_state(
21810 &r#"fn one() {
21811 let mut a = two();
21812 }
21813
21814 fn «twoˇ»() {}"#
21815 .unindent(),
21816 );
21817 let editors = cx.update_workspace(|workspace, _, cx| {
21818 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21819 });
21820 cx.update_editor(|_, _, test_editor_cx| {
21821 assert_eq!(
21822 editors.len(),
21823 2,
21824 "After falling back to references search, we open a new editor with the results"
21825 );
21826 let references_fallback_text = editors
21827 .into_iter()
21828 .find(|new_editor| *new_editor != test_editor_cx.entity())
21829 .expect("Should have one non-test editor now")
21830 .read(test_editor_cx)
21831 .text(test_editor_cx);
21832 assert_eq!(
21833 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21834 "Should use the range from the references response and not the GoToDefinition one"
21835 );
21836 });
21837}
21838
21839#[gpui::test]
21840async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21841 init_test(cx, |_| {});
21842 cx.update(|cx| {
21843 let mut editor_settings = EditorSettings::get_global(cx).clone();
21844 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21845 EditorSettings::override_global(editor_settings, cx);
21846 });
21847 let mut cx = EditorLspTestContext::new_rust(
21848 lsp::ServerCapabilities {
21849 definition_provider: Some(lsp::OneOf::Left(true)),
21850 references_provider: Some(lsp::OneOf::Left(true)),
21851 ..lsp::ServerCapabilities::default()
21852 },
21853 cx,
21854 )
21855 .await;
21856 let original_state = r#"fn one() {
21857 let mut a = ˇtwo();
21858 }
21859
21860 fn two() {}"#
21861 .unindent();
21862 cx.set_state(&original_state);
21863
21864 let mut go_to_definition = cx
21865 .lsp
21866 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21867 move |_, _| async move { Ok(None) },
21868 );
21869 let _references = cx
21870 .lsp
21871 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21872 panic!("Should not call for references with no go to definition fallback")
21873 });
21874
21875 let navigated = cx
21876 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21877 .await
21878 .expect("Failed to navigate to lookup references");
21879 go_to_definition
21880 .next()
21881 .await
21882 .expect("Should have called the go_to_definition handler");
21883
21884 assert_eq!(
21885 navigated,
21886 Navigated::No,
21887 "Should have navigated to references as a fallback after empty GoToDefinition response"
21888 );
21889 cx.assert_editor_state(&original_state);
21890 let editors = cx.update_workspace(|workspace, _, cx| {
21891 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21892 });
21893 cx.update_editor(|_, _, _| {
21894 assert_eq!(
21895 editors.len(),
21896 1,
21897 "After unsuccessful fallback, no other editor should have been opened"
21898 );
21899 });
21900}
21901
21902#[gpui::test]
21903async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21904 init_test(cx, |_| {});
21905 let mut cx = EditorLspTestContext::new_rust(
21906 lsp::ServerCapabilities {
21907 references_provider: Some(lsp::OneOf::Left(true)),
21908 ..lsp::ServerCapabilities::default()
21909 },
21910 cx,
21911 )
21912 .await;
21913
21914 cx.set_state(
21915 &r#"
21916 fn one() {
21917 let mut a = two();
21918 }
21919
21920 fn ˇtwo() {}"#
21921 .unindent(),
21922 );
21923 cx.lsp
21924 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21925 Ok(Some(vec![
21926 lsp::Location {
21927 uri: params.text_document_position.text_document.uri.clone(),
21928 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21929 },
21930 lsp::Location {
21931 uri: params.text_document_position.text_document.uri,
21932 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21933 },
21934 ]))
21935 });
21936 let navigated = cx
21937 .update_editor(|editor, window, cx| {
21938 editor.find_all_references(&FindAllReferences, window, cx)
21939 })
21940 .unwrap()
21941 .await
21942 .expect("Failed to navigate to references");
21943 assert_eq!(
21944 navigated,
21945 Navigated::Yes,
21946 "Should have navigated to references from the FindAllReferences response"
21947 );
21948 cx.assert_editor_state(
21949 &r#"fn one() {
21950 let mut a = two();
21951 }
21952
21953 fn ˇtwo() {}"#
21954 .unindent(),
21955 );
21956
21957 let editors = cx.update_workspace(|workspace, _, cx| {
21958 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21959 });
21960 cx.update_editor(|_, _, _| {
21961 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21962 });
21963
21964 cx.set_state(
21965 &r#"fn one() {
21966 let mut a = ˇtwo();
21967 }
21968
21969 fn two() {}"#
21970 .unindent(),
21971 );
21972 let navigated = cx
21973 .update_editor(|editor, window, cx| {
21974 editor.find_all_references(&FindAllReferences, window, cx)
21975 })
21976 .unwrap()
21977 .await
21978 .expect("Failed to navigate to references");
21979 assert_eq!(
21980 navigated,
21981 Navigated::Yes,
21982 "Should have navigated to references from the FindAllReferences response"
21983 );
21984 cx.assert_editor_state(
21985 &r#"fn one() {
21986 let mut a = ˇtwo();
21987 }
21988
21989 fn two() {}"#
21990 .unindent(),
21991 );
21992 let editors = cx.update_workspace(|workspace, _, cx| {
21993 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21994 });
21995 cx.update_editor(|_, _, _| {
21996 assert_eq!(
21997 editors.len(),
21998 2,
21999 "should have re-used the previous multibuffer"
22000 );
22001 });
22002
22003 cx.set_state(
22004 &r#"fn one() {
22005 let mut a = ˇtwo();
22006 }
22007 fn three() {}
22008 fn two() {}"#
22009 .unindent(),
22010 );
22011 cx.lsp
22012 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22013 Ok(Some(vec![
22014 lsp::Location {
22015 uri: params.text_document_position.text_document.uri.clone(),
22016 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22017 },
22018 lsp::Location {
22019 uri: params.text_document_position.text_document.uri,
22020 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22021 },
22022 ]))
22023 });
22024 let navigated = cx
22025 .update_editor(|editor, window, cx| {
22026 editor.find_all_references(&FindAllReferences, window, cx)
22027 })
22028 .unwrap()
22029 .await
22030 .expect("Failed to navigate to references");
22031 assert_eq!(
22032 navigated,
22033 Navigated::Yes,
22034 "Should have navigated to references from the FindAllReferences response"
22035 );
22036 cx.assert_editor_state(
22037 &r#"fn one() {
22038 let mut a = ˇtwo();
22039 }
22040 fn three() {}
22041 fn two() {}"#
22042 .unindent(),
22043 );
22044 let editors = cx.update_workspace(|workspace, _, cx| {
22045 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22046 });
22047 cx.update_editor(|_, _, _| {
22048 assert_eq!(
22049 editors.len(),
22050 3,
22051 "should have used a new multibuffer as offsets changed"
22052 );
22053 });
22054}
22055#[gpui::test]
22056async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22057 init_test(cx, |_| {});
22058
22059 let language = Arc::new(Language::new(
22060 LanguageConfig::default(),
22061 Some(tree_sitter_rust::LANGUAGE.into()),
22062 ));
22063
22064 let text = r#"
22065 #[cfg(test)]
22066 mod tests() {
22067 #[test]
22068 fn runnable_1() {
22069 let a = 1;
22070 }
22071
22072 #[test]
22073 fn runnable_2() {
22074 let a = 1;
22075 let b = 2;
22076 }
22077 }
22078 "#
22079 .unindent();
22080
22081 let fs = FakeFs::new(cx.executor());
22082 fs.insert_file("/file.rs", Default::default()).await;
22083
22084 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22085 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22086 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22087 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22088 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22089
22090 let editor = cx.new_window_entity(|window, cx| {
22091 Editor::new(
22092 EditorMode::full(),
22093 multi_buffer,
22094 Some(project.clone()),
22095 window,
22096 cx,
22097 )
22098 });
22099
22100 editor.update_in(cx, |editor, window, cx| {
22101 let snapshot = editor.buffer().read(cx).snapshot(cx);
22102 editor.tasks.insert(
22103 (buffer.read(cx).remote_id(), 3),
22104 RunnableTasks {
22105 templates: vec![],
22106 offset: snapshot.anchor_before(43),
22107 column: 0,
22108 extra_variables: HashMap::default(),
22109 context_range: BufferOffset(43)..BufferOffset(85),
22110 },
22111 );
22112 editor.tasks.insert(
22113 (buffer.read(cx).remote_id(), 8),
22114 RunnableTasks {
22115 templates: vec![],
22116 offset: snapshot.anchor_before(86),
22117 column: 0,
22118 extra_variables: HashMap::default(),
22119 context_range: BufferOffset(86)..BufferOffset(191),
22120 },
22121 );
22122
22123 // Test finding task when cursor is inside function body
22124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22125 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22126 });
22127 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22128 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22129
22130 // Test finding task when cursor is on function name
22131 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22132 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22133 });
22134 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22135 assert_eq!(row, 8, "Should find task when cursor is on function name");
22136 });
22137}
22138
22139#[gpui::test]
22140async fn test_folding_buffers(cx: &mut TestAppContext) {
22141 init_test(cx, |_| {});
22142
22143 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22144 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22145 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22146
22147 let fs = FakeFs::new(cx.executor());
22148 fs.insert_tree(
22149 path!("/a"),
22150 json!({
22151 "first.rs": sample_text_1,
22152 "second.rs": sample_text_2,
22153 "third.rs": sample_text_3,
22154 }),
22155 )
22156 .await;
22157 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22158 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22159 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22160 let worktree = project.update(cx, |project, cx| {
22161 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22162 assert_eq!(worktrees.len(), 1);
22163 worktrees.pop().unwrap()
22164 });
22165 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22166
22167 let buffer_1 = project
22168 .update(cx, |project, cx| {
22169 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22170 })
22171 .await
22172 .unwrap();
22173 let buffer_2 = project
22174 .update(cx, |project, cx| {
22175 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22176 })
22177 .await
22178 .unwrap();
22179 let buffer_3 = project
22180 .update(cx, |project, cx| {
22181 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22182 })
22183 .await
22184 .unwrap();
22185
22186 let multi_buffer = cx.new(|cx| {
22187 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22188 multi_buffer.push_excerpts(
22189 buffer_1.clone(),
22190 [
22191 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22192 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22193 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22194 ],
22195 cx,
22196 );
22197 multi_buffer.push_excerpts(
22198 buffer_2.clone(),
22199 [
22200 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22201 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22202 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22203 ],
22204 cx,
22205 );
22206 multi_buffer.push_excerpts(
22207 buffer_3.clone(),
22208 [
22209 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22210 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22211 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22212 ],
22213 cx,
22214 );
22215 multi_buffer
22216 });
22217 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22218 Editor::new(
22219 EditorMode::full(),
22220 multi_buffer.clone(),
22221 Some(project.clone()),
22222 window,
22223 cx,
22224 )
22225 });
22226
22227 assert_eq!(
22228 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22229 "\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",
22230 );
22231
22232 multi_buffer_editor.update(cx, |editor, cx| {
22233 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22234 });
22235 assert_eq!(
22236 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22237 "\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",
22238 "After folding the first buffer, its text should not be displayed"
22239 );
22240
22241 multi_buffer_editor.update(cx, |editor, cx| {
22242 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22243 });
22244 assert_eq!(
22245 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22246 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22247 "After folding the second buffer, its text should not be displayed"
22248 );
22249
22250 multi_buffer_editor.update(cx, |editor, cx| {
22251 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22252 });
22253 assert_eq!(
22254 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22255 "\n\n\n\n\n",
22256 "After folding the third buffer, its text should not be displayed"
22257 );
22258
22259 // Emulate selection inside the fold logic, that should work
22260 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22261 editor
22262 .snapshot(window, cx)
22263 .next_line_boundary(Point::new(0, 4));
22264 });
22265
22266 multi_buffer_editor.update(cx, |editor, cx| {
22267 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22268 });
22269 assert_eq!(
22270 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22271 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22272 "After unfolding the second buffer, its text should be displayed"
22273 );
22274
22275 // Typing inside of buffer 1 causes that buffer to be unfolded.
22276 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22277 assert_eq!(
22278 multi_buffer
22279 .read(cx)
22280 .snapshot(cx)
22281 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22282 .collect::<String>(),
22283 "bbbb"
22284 );
22285 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22286 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22287 });
22288 editor.handle_input("B", window, cx);
22289 });
22290
22291 assert_eq!(
22292 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22293 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22294 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22295 );
22296
22297 multi_buffer_editor.update(cx, |editor, cx| {
22298 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22299 });
22300 assert_eq!(
22301 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22302 "\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",
22303 "After unfolding the all buffers, all original text should be displayed"
22304 );
22305}
22306
22307#[gpui::test]
22308async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22309 init_test(cx, |_| {});
22310
22311 let sample_text_1 = "1111\n2222\n3333".to_string();
22312 let sample_text_2 = "4444\n5555\n6666".to_string();
22313 let sample_text_3 = "7777\n8888\n9999".to_string();
22314
22315 let fs = FakeFs::new(cx.executor());
22316 fs.insert_tree(
22317 path!("/a"),
22318 json!({
22319 "first.rs": sample_text_1,
22320 "second.rs": sample_text_2,
22321 "third.rs": sample_text_3,
22322 }),
22323 )
22324 .await;
22325 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22326 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22327 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22328 let worktree = project.update(cx, |project, cx| {
22329 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22330 assert_eq!(worktrees.len(), 1);
22331 worktrees.pop().unwrap()
22332 });
22333 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22334
22335 let buffer_1 = project
22336 .update(cx, |project, cx| {
22337 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22338 })
22339 .await
22340 .unwrap();
22341 let buffer_2 = project
22342 .update(cx, |project, cx| {
22343 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22344 })
22345 .await
22346 .unwrap();
22347 let buffer_3 = project
22348 .update(cx, |project, cx| {
22349 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22350 })
22351 .await
22352 .unwrap();
22353
22354 let multi_buffer = cx.new(|cx| {
22355 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22356 multi_buffer.push_excerpts(
22357 buffer_1.clone(),
22358 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22359 cx,
22360 );
22361 multi_buffer.push_excerpts(
22362 buffer_2.clone(),
22363 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22364 cx,
22365 );
22366 multi_buffer.push_excerpts(
22367 buffer_3.clone(),
22368 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22369 cx,
22370 );
22371 multi_buffer
22372 });
22373
22374 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22375 Editor::new(
22376 EditorMode::full(),
22377 multi_buffer,
22378 Some(project.clone()),
22379 window,
22380 cx,
22381 )
22382 });
22383
22384 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22385 assert_eq!(
22386 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22387 full_text,
22388 );
22389
22390 multi_buffer_editor.update(cx, |editor, cx| {
22391 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22392 });
22393 assert_eq!(
22394 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22395 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22396 "After folding the first buffer, its text should not be displayed"
22397 );
22398
22399 multi_buffer_editor.update(cx, |editor, cx| {
22400 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22401 });
22402
22403 assert_eq!(
22404 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22405 "\n\n\n\n\n\n7777\n8888\n9999",
22406 "After folding the second buffer, its text should not be displayed"
22407 );
22408
22409 multi_buffer_editor.update(cx, |editor, cx| {
22410 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22411 });
22412 assert_eq!(
22413 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22414 "\n\n\n\n\n",
22415 "After folding the third buffer, its text should not be displayed"
22416 );
22417
22418 multi_buffer_editor.update(cx, |editor, cx| {
22419 editor.unfold_buffer(buffer_2.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",
22424 "After unfolding the second buffer, its text should be displayed"
22425 );
22426
22427 multi_buffer_editor.update(cx, |editor, cx| {
22428 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22429 });
22430 assert_eq!(
22431 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22432 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22433 "After unfolding the first buffer, its text should be displayed"
22434 );
22435
22436 multi_buffer_editor.update(cx, |editor, cx| {
22437 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22438 });
22439 assert_eq!(
22440 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22441 full_text,
22442 "After unfolding all buffers, all original text should be displayed"
22443 );
22444}
22445
22446#[gpui::test]
22447async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22448 init_test(cx, |_| {});
22449
22450 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22451
22452 let fs = FakeFs::new(cx.executor());
22453 fs.insert_tree(
22454 path!("/a"),
22455 json!({
22456 "main.rs": sample_text,
22457 }),
22458 )
22459 .await;
22460 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22461 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22462 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22463 let worktree = project.update(cx, |project, cx| {
22464 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22465 assert_eq!(worktrees.len(), 1);
22466 worktrees.pop().unwrap()
22467 });
22468 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22469
22470 let buffer_1 = project
22471 .update(cx, |project, cx| {
22472 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22473 })
22474 .await
22475 .unwrap();
22476
22477 let multi_buffer = cx.new(|cx| {
22478 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22479 multi_buffer.push_excerpts(
22480 buffer_1.clone(),
22481 [ExcerptRange::new(
22482 Point::new(0, 0)
22483 ..Point::new(
22484 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22485 0,
22486 ),
22487 )],
22488 cx,
22489 );
22490 multi_buffer
22491 });
22492 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22493 Editor::new(
22494 EditorMode::full(),
22495 multi_buffer,
22496 Some(project.clone()),
22497 window,
22498 cx,
22499 )
22500 });
22501
22502 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22503 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22504 enum TestHighlight {}
22505 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22506 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22507 editor.highlight_text::<TestHighlight>(
22508 vec![highlight_range.clone()],
22509 HighlightStyle::color(Hsla::green()),
22510 cx,
22511 );
22512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22513 s.select_ranges(Some(highlight_range))
22514 });
22515 });
22516
22517 let full_text = format!("\n\n{sample_text}");
22518 assert_eq!(
22519 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22520 full_text,
22521 );
22522}
22523
22524#[gpui::test]
22525async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22526 init_test(cx, |_| {});
22527 cx.update(|cx| {
22528 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22529 "keymaps/default-linux.json",
22530 cx,
22531 )
22532 .unwrap();
22533 cx.bind_keys(default_key_bindings);
22534 });
22535
22536 let (editor, cx) = cx.add_window_view(|window, cx| {
22537 let multi_buffer = MultiBuffer::build_multi(
22538 [
22539 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22540 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22541 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22542 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22543 ],
22544 cx,
22545 );
22546 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22547
22548 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22549 // fold all but the second buffer, so that we test navigating between two
22550 // adjacent folded buffers, as well as folded buffers at the start and
22551 // end the multibuffer
22552 editor.fold_buffer(buffer_ids[0], cx);
22553 editor.fold_buffer(buffer_ids[2], cx);
22554 editor.fold_buffer(buffer_ids[3], cx);
22555
22556 editor
22557 });
22558 cx.simulate_resize(size(px(1000.), px(1000.)));
22559
22560 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22561 cx.assert_excerpts_with_selections(indoc! {"
22562 [EXCERPT]
22563 ˇ[FOLDED]
22564 [EXCERPT]
22565 a1
22566 b1
22567 [EXCERPT]
22568 [FOLDED]
22569 [EXCERPT]
22570 [FOLDED]
22571 "
22572 });
22573 cx.simulate_keystroke("down");
22574 cx.assert_excerpts_with_selections(indoc! {"
22575 [EXCERPT]
22576 [FOLDED]
22577 [EXCERPT]
22578 ˇa1
22579 b1
22580 [EXCERPT]
22581 [FOLDED]
22582 [EXCERPT]
22583 [FOLDED]
22584 "
22585 });
22586 cx.simulate_keystroke("down");
22587 cx.assert_excerpts_with_selections(indoc! {"
22588 [EXCERPT]
22589 [FOLDED]
22590 [EXCERPT]
22591 a1
22592 ˇb1
22593 [EXCERPT]
22594 [FOLDED]
22595 [EXCERPT]
22596 [FOLDED]
22597 "
22598 });
22599 cx.simulate_keystroke("down");
22600 cx.assert_excerpts_with_selections(indoc! {"
22601 [EXCERPT]
22602 [FOLDED]
22603 [EXCERPT]
22604 a1
22605 b1
22606 ˇ[EXCERPT]
22607 [FOLDED]
22608 [EXCERPT]
22609 [FOLDED]
22610 "
22611 });
22612 cx.simulate_keystroke("down");
22613 cx.assert_excerpts_with_selections(indoc! {"
22614 [EXCERPT]
22615 [FOLDED]
22616 [EXCERPT]
22617 a1
22618 b1
22619 [EXCERPT]
22620 ˇ[FOLDED]
22621 [EXCERPT]
22622 [FOLDED]
22623 "
22624 });
22625 for _ in 0..5 {
22626 cx.simulate_keystroke("down");
22627 cx.assert_excerpts_with_selections(indoc! {"
22628 [EXCERPT]
22629 [FOLDED]
22630 [EXCERPT]
22631 a1
22632 b1
22633 [EXCERPT]
22634 [FOLDED]
22635 [EXCERPT]
22636 ˇ[FOLDED]
22637 "
22638 });
22639 }
22640
22641 cx.simulate_keystroke("up");
22642 cx.assert_excerpts_with_selections(indoc! {"
22643 [EXCERPT]
22644 [FOLDED]
22645 [EXCERPT]
22646 a1
22647 b1
22648 [EXCERPT]
22649 ˇ[FOLDED]
22650 [EXCERPT]
22651 [FOLDED]
22652 "
22653 });
22654 cx.simulate_keystroke("up");
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 cx.simulate_keystroke("up");
22668 cx.assert_excerpts_with_selections(indoc! {"
22669 [EXCERPT]
22670 [FOLDED]
22671 [EXCERPT]
22672 a1
22673 ˇb1
22674 [EXCERPT]
22675 [FOLDED]
22676 [EXCERPT]
22677 [FOLDED]
22678 "
22679 });
22680 cx.simulate_keystroke("up");
22681 cx.assert_excerpts_with_selections(indoc! {"
22682 [EXCERPT]
22683 [FOLDED]
22684 [EXCERPT]
22685 ˇa1
22686 b1
22687 [EXCERPT]
22688 [FOLDED]
22689 [EXCERPT]
22690 [FOLDED]
22691 "
22692 });
22693 for _ in 0..5 {
22694 cx.simulate_keystroke("up");
22695 cx.assert_excerpts_with_selections(indoc! {"
22696 [EXCERPT]
22697 ˇ[FOLDED]
22698 [EXCERPT]
22699 a1
22700 b1
22701 [EXCERPT]
22702 [FOLDED]
22703 [EXCERPT]
22704 [FOLDED]
22705 "
22706 });
22707 }
22708}
22709
22710#[gpui::test]
22711async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22712 init_test(cx, |_| {});
22713
22714 // Simple insertion
22715 assert_highlighted_edits(
22716 "Hello, world!",
22717 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22718 true,
22719 cx,
22720 |highlighted_edits, cx| {
22721 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22722 assert_eq!(highlighted_edits.highlights.len(), 1);
22723 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22724 assert_eq!(
22725 highlighted_edits.highlights[0].1.background_color,
22726 Some(cx.theme().status().created_background)
22727 );
22728 },
22729 )
22730 .await;
22731
22732 // Replacement
22733 assert_highlighted_edits(
22734 "This is a test.",
22735 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22736 false,
22737 cx,
22738 |highlighted_edits, cx| {
22739 assert_eq!(highlighted_edits.text, "That is a test.");
22740 assert_eq!(highlighted_edits.highlights.len(), 1);
22741 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22742 assert_eq!(
22743 highlighted_edits.highlights[0].1.background_color,
22744 Some(cx.theme().status().created_background)
22745 );
22746 },
22747 )
22748 .await;
22749
22750 // Multiple edits
22751 assert_highlighted_edits(
22752 "Hello, world!",
22753 vec![
22754 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22755 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22756 ],
22757 false,
22758 cx,
22759 |highlighted_edits, cx| {
22760 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22761 assert_eq!(highlighted_edits.highlights.len(), 2);
22762 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22763 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22764 assert_eq!(
22765 highlighted_edits.highlights[0].1.background_color,
22766 Some(cx.theme().status().created_background)
22767 );
22768 assert_eq!(
22769 highlighted_edits.highlights[1].1.background_color,
22770 Some(cx.theme().status().created_background)
22771 );
22772 },
22773 )
22774 .await;
22775
22776 // Multiple lines with edits
22777 assert_highlighted_edits(
22778 "First line\nSecond line\nThird line\nFourth line",
22779 vec![
22780 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22781 (
22782 Point::new(2, 0)..Point::new(2, 10),
22783 "New third line".to_string(),
22784 ),
22785 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22786 ],
22787 false,
22788 cx,
22789 |highlighted_edits, cx| {
22790 assert_eq!(
22791 highlighted_edits.text,
22792 "Second modified\nNew third line\nFourth updated line"
22793 );
22794 assert_eq!(highlighted_edits.highlights.len(), 3);
22795 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22796 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22797 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22798 for highlight in &highlighted_edits.highlights {
22799 assert_eq!(
22800 highlight.1.background_color,
22801 Some(cx.theme().status().created_background)
22802 );
22803 }
22804 },
22805 )
22806 .await;
22807}
22808
22809#[gpui::test]
22810async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22811 init_test(cx, |_| {});
22812
22813 // Deletion
22814 assert_highlighted_edits(
22815 "Hello, world!",
22816 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22817 true,
22818 cx,
22819 |highlighted_edits, cx| {
22820 assert_eq!(highlighted_edits.text, "Hello, world!");
22821 assert_eq!(highlighted_edits.highlights.len(), 1);
22822 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22823 assert_eq!(
22824 highlighted_edits.highlights[0].1.background_color,
22825 Some(cx.theme().status().deleted_background)
22826 );
22827 },
22828 )
22829 .await;
22830
22831 // Insertion
22832 assert_highlighted_edits(
22833 "Hello, world!",
22834 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22835 true,
22836 cx,
22837 |highlighted_edits, cx| {
22838 assert_eq!(highlighted_edits.highlights.len(), 1);
22839 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22840 assert_eq!(
22841 highlighted_edits.highlights[0].1.background_color,
22842 Some(cx.theme().status().created_background)
22843 );
22844 },
22845 )
22846 .await;
22847}
22848
22849async fn assert_highlighted_edits(
22850 text: &str,
22851 edits: Vec<(Range<Point>, String)>,
22852 include_deletions: bool,
22853 cx: &mut TestAppContext,
22854 assertion_fn: impl Fn(HighlightedText, &App),
22855) {
22856 let window = cx.add_window(|window, cx| {
22857 let buffer = MultiBuffer::build_simple(text, cx);
22858 Editor::new(EditorMode::full(), buffer, None, window, cx)
22859 });
22860 let cx = &mut VisualTestContext::from_window(*window, cx);
22861
22862 let (buffer, snapshot) = window
22863 .update(cx, |editor, _window, cx| {
22864 (
22865 editor.buffer().clone(),
22866 editor.buffer().read(cx).snapshot(cx),
22867 )
22868 })
22869 .unwrap();
22870
22871 let edits = edits
22872 .into_iter()
22873 .map(|(range, edit)| {
22874 (
22875 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22876 edit,
22877 )
22878 })
22879 .collect::<Vec<_>>();
22880
22881 let text_anchor_edits = edits
22882 .clone()
22883 .into_iter()
22884 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22885 .collect::<Vec<_>>();
22886
22887 let edit_preview = window
22888 .update(cx, |_, _window, cx| {
22889 buffer
22890 .read(cx)
22891 .as_singleton()
22892 .unwrap()
22893 .read(cx)
22894 .preview_edits(text_anchor_edits.into(), cx)
22895 })
22896 .unwrap()
22897 .await;
22898
22899 cx.update(|_window, cx| {
22900 let highlighted_edits = edit_prediction_edit_text(
22901 snapshot.as_singleton().unwrap().2,
22902 &edits,
22903 &edit_preview,
22904 include_deletions,
22905 cx,
22906 );
22907 assertion_fn(highlighted_edits, cx)
22908 });
22909}
22910
22911#[track_caller]
22912fn assert_breakpoint(
22913 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22914 path: &Arc<Path>,
22915 expected: Vec<(u32, Breakpoint)>,
22916) {
22917 if expected.is_empty() {
22918 assert!(!breakpoints.contains_key(path), "{}", path.display());
22919 } else {
22920 let mut breakpoint = breakpoints
22921 .get(path)
22922 .unwrap()
22923 .iter()
22924 .map(|breakpoint| {
22925 (
22926 breakpoint.row,
22927 Breakpoint {
22928 message: breakpoint.message.clone(),
22929 state: breakpoint.state,
22930 condition: breakpoint.condition.clone(),
22931 hit_condition: breakpoint.hit_condition.clone(),
22932 },
22933 )
22934 })
22935 .collect::<Vec<_>>();
22936
22937 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22938
22939 assert_eq!(expected, breakpoint);
22940 }
22941}
22942
22943fn add_log_breakpoint_at_cursor(
22944 editor: &mut Editor,
22945 log_message: &str,
22946 window: &mut Window,
22947 cx: &mut Context<Editor>,
22948) {
22949 let (anchor, bp) = editor
22950 .breakpoints_at_cursors(window, cx)
22951 .first()
22952 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22953 .unwrap_or_else(|| {
22954 let snapshot = editor.snapshot(window, cx);
22955 let cursor_position: Point =
22956 editor.selections.newest(&snapshot.display_snapshot).head();
22957
22958 let breakpoint_position = snapshot
22959 .buffer_snapshot()
22960 .anchor_before(Point::new(cursor_position.row, 0));
22961
22962 (breakpoint_position, Breakpoint::new_log(log_message))
22963 });
22964
22965 editor.edit_breakpoint_at_anchor(
22966 anchor,
22967 bp,
22968 BreakpointEditAction::EditLogMessage(log_message.into()),
22969 cx,
22970 );
22971}
22972
22973#[gpui::test]
22974async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22975 init_test(cx, |_| {});
22976
22977 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22978 let fs = FakeFs::new(cx.executor());
22979 fs.insert_tree(
22980 path!("/a"),
22981 json!({
22982 "main.rs": sample_text,
22983 }),
22984 )
22985 .await;
22986 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22987 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22988 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22989
22990 let fs = FakeFs::new(cx.executor());
22991 fs.insert_tree(
22992 path!("/a"),
22993 json!({
22994 "main.rs": sample_text,
22995 }),
22996 )
22997 .await;
22998 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22999 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23000 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23001 let worktree_id = workspace
23002 .update(cx, |workspace, _window, cx| {
23003 workspace.project().update(cx, |project, cx| {
23004 project.worktrees(cx).next().unwrap().read(cx).id()
23005 })
23006 })
23007 .unwrap();
23008
23009 let buffer = project
23010 .update(cx, |project, cx| {
23011 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23012 })
23013 .await
23014 .unwrap();
23015
23016 let (editor, cx) = cx.add_window_view(|window, cx| {
23017 Editor::new(
23018 EditorMode::full(),
23019 MultiBuffer::build_from_buffer(buffer, cx),
23020 Some(project.clone()),
23021 window,
23022 cx,
23023 )
23024 });
23025
23026 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23027 let abs_path = project.read_with(cx, |project, cx| {
23028 project
23029 .absolute_path(&project_path, cx)
23030 .map(Arc::from)
23031 .unwrap()
23032 });
23033
23034 // assert we can add breakpoint on the first line
23035 editor.update_in(cx, |editor, window, cx| {
23036 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23037 editor.move_to_end(&MoveToEnd, window, cx);
23038 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23039 });
23040
23041 let breakpoints = editor.update(cx, |editor, cx| {
23042 editor
23043 .breakpoint_store()
23044 .as_ref()
23045 .unwrap()
23046 .read(cx)
23047 .all_source_breakpoints(cx)
23048 });
23049
23050 assert_eq!(1, breakpoints.len());
23051 assert_breakpoint(
23052 &breakpoints,
23053 &abs_path,
23054 vec![
23055 (0, Breakpoint::new_standard()),
23056 (3, Breakpoint::new_standard()),
23057 ],
23058 );
23059
23060 editor.update_in(cx, |editor, window, cx| {
23061 editor.move_to_beginning(&MoveToBeginning, window, cx);
23062 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23063 });
23064
23065 let breakpoints = editor.update(cx, |editor, cx| {
23066 editor
23067 .breakpoint_store()
23068 .as_ref()
23069 .unwrap()
23070 .read(cx)
23071 .all_source_breakpoints(cx)
23072 });
23073
23074 assert_eq!(1, breakpoints.len());
23075 assert_breakpoint(
23076 &breakpoints,
23077 &abs_path,
23078 vec![(3, Breakpoint::new_standard())],
23079 );
23080
23081 editor.update_in(cx, |editor, window, cx| {
23082 editor.move_to_end(&MoveToEnd, window, cx);
23083 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23084 });
23085
23086 let breakpoints = editor.update(cx, |editor, cx| {
23087 editor
23088 .breakpoint_store()
23089 .as_ref()
23090 .unwrap()
23091 .read(cx)
23092 .all_source_breakpoints(cx)
23093 });
23094
23095 assert_eq!(0, breakpoints.len());
23096 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23097}
23098
23099#[gpui::test]
23100async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23101 init_test(cx, |_| {});
23102
23103 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23104
23105 let fs = FakeFs::new(cx.executor());
23106 fs.insert_tree(
23107 path!("/a"),
23108 json!({
23109 "main.rs": sample_text,
23110 }),
23111 )
23112 .await;
23113 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23114 let (workspace, cx) =
23115 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23116
23117 let worktree_id = workspace.update(cx, |workspace, cx| {
23118 workspace.project().update(cx, |project, cx| {
23119 project.worktrees(cx).next().unwrap().read(cx).id()
23120 })
23121 });
23122
23123 let buffer = project
23124 .update(cx, |project, cx| {
23125 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23126 })
23127 .await
23128 .unwrap();
23129
23130 let (editor, cx) = cx.add_window_view(|window, cx| {
23131 Editor::new(
23132 EditorMode::full(),
23133 MultiBuffer::build_from_buffer(buffer, cx),
23134 Some(project.clone()),
23135 window,
23136 cx,
23137 )
23138 });
23139
23140 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23141 let abs_path = project.read_with(cx, |project, cx| {
23142 project
23143 .absolute_path(&project_path, cx)
23144 .map(Arc::from)
23145 .unwrap()
23146 });
23147
23148 editor.update_in(cx, |editor, window, cx| {
23149 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23150 });
23151
23152 let breakpoints = editor.update(cx, |editor, cx| {
23153 editor
23154 .breakpoint_store()
23155 .as_ref()
23156 .unwrap()
23157 .read(cx)
23158 .all_source_breakpoints(cx)
23159 });
23160
23161 assert_breakpoint(
23162 &breakpoints,
23163 &abs_path,
23164 vec![(0, Breakpoint::new_log("hello world"))],
23165 );
23166
23167 // Removing a log message from a log breakpoint should remove it
23168 editor.update_in(cx, |editor, window, cx| {
23169 add_log_breakpoint_at_cursor(editor, "", window, cx);
23170 });
23171
23172 let breakpoints = editor.update(cx, |editor, cx| {
23173 editor
23174 .breakpoint_store()
23175 .as_ref()
23176 .unwrap()
23177 .read(cx)
23178 .all_source_breakpoints(cx)
23179 });
23180
23181 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23182
23183 editor.update_in(cx, |editor, window, cx| {
23184 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23185 editor.move_to_end(&MoveToEnd, window, cx);
23186 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23187 // Not adding a log message to a standard breakpoint shouldn't remove it
23188 add_log_breakpoint_at_cursor(editor, "", window, cx);
23189 });
23190
23191 let breakpoints = editor.update(cx, |editor, cx| {
23192 editor
23193 .breakpoint_store()
23194 .as_ref()
23195 .unwrap()
23196 .read(cx)
23197 .all_source_breakpoints(cx)
23198 });
23199
23200 assert_breakpoint(
23201 &breakpoints,
23202 &abs_path,
23203 vec![
23204 (0, Breakpoint::new_standard()),
23205 (3, Breakpoint::new_standard()),
23206 ],
23207 );
23208
23209 editor.update_in(cx, |editor, window, cx| {
23210 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23211 });
23212
23213 let breakpoints = editor.update(cx, |editor, cx| {
23214 editor
23215 .breakpoint_store()
23216 .as_ref()
23217 .unwrap()
23218 .read(cx)
23219 .all_source_breakpoints(cx)
23220 });
23221
23222 assert_breakpoint(
23223 &breakpoints,
23224 &abs_path,
23225 vec![
23226 (0, Breakpoint::new_standard()),
23227 (3, Breakpoint::new_log("hello world")),
23228 ],
23229 );
23230
23231 editor.update_in(cx, |editor, window, cx| {
23232 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23233 });
23234
23235 let breakpoints = editor.update(cx, |editor, cx| {
23236 editor
23237 .breakpoint_store()
23238 .as_ref()
23239 .unwrap()
23240 .read(cx)
23241 .all_source_breakpoints(cx)
23242 });
23243
23244 assert_breakpoint(
23245 &breakpoints,
23246 &abs_path,
23247 vec![
23248 (0, Breakpoint::new_standard()),
23249 (3, Breakpoint::new_log("hello Earth!!")),
23250 ],
23251 );
23252}
23253
23254/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23255/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23256/// or when breakpoints were placed out of order. This tests for a regression too
23257#[gpui::test]
23258async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23259 init_test(cx, |_| {});
23260
23261 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23262 let fs = FakeFs::new(cx.executor());
23263 fs.insert_tree(
23264 path!("/a"),
23265 json!({
23266 "main.rs": sample_text,
23267 }),
23268 )
23269 .await;
23270 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23271 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23272 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23273
23274 let fs = FakeFs::new(cx.executor());
23275 fs.insert_tree(
23276 path!("/a"),
23277 json!({
23278 "main.rs": sample_text,
23279 }),
23280 )
23281 .await;
23282 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23283 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23284 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23285 let worktree_id = workspace
23286 .update(cx, |workspace, _window, cx| {
23287 workspace.project().update(cx, |project, cx| {
23288 project.worktrees(cx).next().unwrap().read(cx).id()
23289 })
23290 })
23291 .unwrap();
23292
23293 let buffer = project
23294 .update(cx, |project, cx| {
23295 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23296 })
23297 .await
23298 .unwrap();
23299
23300 let (editor, cx) = cx.add_window_view(|window, cx| {
23301 Editor::new(
23302 EditorMode::full(),
23303 MultiBuffer::build_from_buffer(buffer, cx),
23304 Some(project.clone()),
23305 window,
23306 cx,
23307 )
23308 });
23309
23310 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23311 let abs_path = project.read_with(cx, |project, cx| {
23312 project
23313 .absolute_path(&project_path, cx)
23314 .map(Arc::from)
23315 .unwrap()
23316 });
23317
23318 // assert we can add breakpoint on the first line
23319 editor.update_in(cx, |editor, window, cx| {
23320 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23321 editor.move_to_end(&MoveToEnd, window, cx);
23322 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23323 editor.move_up(&MoveUp, window, cx);
23324 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23325 });
23326
23327 let breakpoints = editor.update(cx, |editor, cx| {
23328 editor
23329 .breakpoint_store()
23330 .as_ref()
23331 .unwrap()
23332 .read(cx)
23333 .all_source_breakpoints(cx)
23334 });
23335
23336 assert_eq!(1, breakpoints.len());
23337 assert_breakpoint(
23338 &breakpoints,
23339 &abs_path,
23340 vec![
23341 (0, Breakpoint::new_standard()),
23342 (2, Breakpoint::new_standard()),
23343 (3, Breakpoint::new_standard()),
23344 ],
23345 );
23346
23347 editor.update_in(cx, |editor, window, cx| {
23348 editor.move_to_beginning(&MoveToBeginning, window, cx);
23349 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23350 editor.move_to_end(&MoveToEnd, window, cx);
23351 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23352 // Disabling a breakpoint that doesn't exist should do nothing
23353 editor.move_up(&MoveUp, window, cx);
23354 editor.move_up(&MoveUp, window, cx);
23355 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23356 });
23357
23358 let breakpoints = editor.update(cx, |editor, cx| {
23359 editor
23360 .breakpoint_store()
23361 .as_ref()
23362 .unwrap()
23363 .read(cx)
23364 .all_source_breakpoints(cx)
23365 });
23366
23367 let disable_breakpoint = {
23368 let mut bp = Breakpoint::new_standard();
23369 bp.state = BreakpointState::Disabled;
23370 bp
23371 };
23372
23373 assert_eq!(1, breakpoints.len());
23374 assert_breakpoint(
23375 &breakpoints,
23376 &abs_path,
23377 vec![
23378 (0, disable_breakpoint.clone()),
23379 (2, Breakpoint::new_standard()),
23380 (3, disable_breakpoint.clone()),
23381 ],
23382 );
23383
23384 editor.update_in(cx, |editor, window, cx| {
23385 editor.move_to_beginning(&MoveToBeginning, window, cx);
23386 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23387 editor.move_to_end(&MoveToEnd, window, cx);
23388 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23389 editor.move_up(&MoveUp, window, cx);
23390 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23391 });
23392
23393 let breakpoints = editor.update(cx, |editor, cx| {
23394 editor
23395 .breakpoint_store()
23396 .as_ref()
23397 .unwrap()
23398 .read(cx)
23399 .all_source_breakpoints(cx)
23400 });
23401
23402 assert_eq!(1, breakpoints.len());
23403 assert_breakpoint(
23404 &breakpoints,
23405 &abs_path,
23406 vec![
23407 (0, Breakpoint::new_standard()),
23408 (2, disable_breakpoint),
23409 (3, Breakpoint::new_standard()),
23410 ],
23411 );
23412}
23413
23414#[gpui::test]
23415async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23416 init_test(cx, |_| {});
23417 let capabilities = lsp::ServerCapabilities {
23418 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23419 prepare_provider: Some(true),
23420 work_done_progress_options: Default::default(),
23421 })),
23422 ..Default::default()
23423 };
23424 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23425
23426 cx.set_state(indoc! {"
23427 struct Fˇoo {}
23428 "});
23429
23430 cx.update_editor(|editor, _, cx| {
23431 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23432 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23433 editor.highlight_background::<DocumentHighlightRead>(
23434 &[highlight_range],
23435 |theme| theme.colors().editor_document_highlight_read_background,
23436 cx,
23437 );
23438 });
23439
23440 let mut prepare_rename_handler = cx
23441 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23442 move |_, _, _| async move {
23443 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23444 start: lsp::Position {
23445 line: 0,
23446 character: 7,
23447 },
23448 end: lsp::Position {
23449 line: 0,
23450 character: 10,
23451 },
23452 })))
23453 },
23454 );
23455 let prepare_rename_task = cx
23456 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23457 .expect("Prepare rename was not started");
23458 prepare_rename_handler.next().await.unwrap();
23459 prepare_rename_task.await.expect("Prepare rename failed");
23460
23461 let mut rename_handler =
23462 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23463 let edit = lsp::TextEdit {
23464 range: lsp::Range {
23465 start: lsp::Position {
23466 line: 0,
23467 character: 7,
23468 },
23469 end: lsp::Position {
23470 line: 0,
23471 character: 10,
23472 },
23473 },
23474 new_text: "FooRenamed".to_string(),
23475 };
23476 Ok(Some(lsp::WorkspaceEdit::new(
23477 // Specify the same edit twice
23478 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23479 )))
23480 });
23481 let rename_task = cx
23482 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23483 .expect("Confirm rename was not started");
23484 rename_handler.next().await.unwrap();
23485 rename_task.await.expect("Confirm rename failed");
23486 cx.run_until_parked();
23487
23488 // Despite two edits, only one is actually applied as those are identical
23489 cx.assert_editor_state(indoc! {"
23490 struct FooRenamedˇ {}
23491 "});
23492}
23493
23494#[gpui::test]
23495async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23496 init_test(cx, |_| {});
23497 // These capabilities indicate that the server does not support prepare rename.
23498 let capabilities = lsp::ServerCapabilities {
23499 rename_provider: Some(lsp::OneOf::Left(true)),
23500 ..Default::default()
23501 };
23502 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23503
23504 cx.set_state(indoc! {"
23505 struct Fˇoo {}
23506 "});
23507
23508 cx.update_editor(|editor, _window, cx| {
23509 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23510 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23511 editor.highlight_background::<DocumentHighlightRead>(
23512 &[highlight_range],
23513 |theme| theme.colors().editor_document_highlight_read_background,
23514 cx,
23515 );
23516 });
23517
23518 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23519 .expect("Prepare rename was not started")
23520 .await
23521 .expect("Prepare rename failed");
23522
23523 let mut rename_handler =
23524 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23525 let edit = lsp::TextEdit {
23526 range: lsp::Range {
23527 start: lsp::Position {
23528 line: 0,
23529 character: 7,
23530 },
23531 end: lsp::Position {
23532 line: 0,
23533 character: 10,
23534 },
23535 },
23536 new_text: "FooRenamed".to_string(),
23537 };
23538 Ok(Some(lsp::WorkspaceEdit::new(
23539 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23540 )))
23541 });
23542 let rename_task = cx
23543 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23544 .expect("Confirm rename was not started");
23545 rename_handler.next().await.unwrap();
23546 rename_task.await.expect("Confirm rename failed");
23547 cx.run_until_parked();
23548
23549 // Correct range is renamed, as `surrounding_word` is used to find it.
23550 cx.assert_editor_state(indoc! {"
23551 struct FooRenamedˇ {}
23552 "});
23553}
23554
23555#[gpui::test]
23556async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23557 init_test(cx, |_| {});
23558 let mut cx = EditorTestContext::new(cx).await;
23559
23560 let language = Arc::new(
23561 Language::new(
23562 LanguageConfig::default(),
23563 Some(tree_sitter_html::LANGUAGE.into()),
23564 )
23565 .with_brackets_query(
23566 r#"
23567 ("<" @open "/>" @close)
23568 ("</" @open ">" @close)
23569 ("<" @open ">" @close)
23570 ("\"" @open "\"" @close)
23571 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23572 "#,
23573 )
23574 .unwrap(),
23575 );
23576 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23577
23578 cx.set_state(indoc! {"
23579 <span>ˇ</span>
23580 "});
23581 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23582 cx.assert_editor_state(indoc! {"
23583 <span>
23584 ˇ
23585 </span>
23586 "});
23587
23588 cx.set_state(indoc! {"
23589 <span><span></span>ˇ</span>
23590 "});
23591 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23592 cx.assert_editor_state(indoc! {"
23593 <span><span></span>
23594 ˇ</span>
23595 "});
23596
23597 cx.set_state(indoc! {"
23598 <span>ˇ
23599 </span>
23600 "});
23601 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23602 cx.assert_editor_state(indoc! {"
23603 <span>
23604 ˇ
23605 </span>
23606 "});
23607}
23608
23609#[gpui::test(iterations = 10)]
23610async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23611 init_test(cx, |_| {});
23612
23613 let fs = FakeFs::new(cx.executor());
23614 fs.insert_tree(
23615 path!("/dir"),
23616 json!({
23617 "a.ts": "a",
23618 }),
23619 )
23620 .await;
23621
23622 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23623 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23624 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23625
23626 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23627 language_registry.add(Arc::new(Language::new(
23628 LanguageConfig {
23629 name: "TypeScript".into(),
23630 matcher: LanguageMatcher {
23631 path_suffixes: vec!["ts".to_string()],
23632 ..Default::default()
23633 },
23634 ..Default::default()
23635 },
23636 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23637 )));
23638 let mut fake_language_servers = language_registry.register_fake_lsp(
23639 "TypeScript",
23640 FakeLspAdapter {
23641 capabilities: lsp::ServerCapabilities {
23642 code_lens_provider: Some(lsp::CodeLensOptions {
23643 resolve_provider: Some(true),
23644 }),
23645 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23646 commands: vec!["_the/command".to_string()],
23647 ..lsp::ExecuteCommandOptions::default()
23648 }),
23649 ..lsp::ServerCapabilities::default()
23650 },
23651 ..FakeLspAdapter::default()
23652 },
23653 );
23654
23655 let editor = workspace
23656 .update(cx, |workspace, window, cx| {
23657 workspace.open_abs_path(
23658 PathBuf::from(path!("/dir/a.ts")),
23659 OpenOptions::default(),
23660 window,
23661 cx,
23662 )
23663 })
23664 .unwrap()
23665 .await
23666 .unwrap()
23667 .downcast::<Editor>()
23668 .unwrap();
23669 cx.executor().run_until_parked();
23670
23671 let fake_server = fake_language_servers.next().await.unwrap();
23672
23673 let buffer = editor.update(cx, |editor, cx| {
23674 editor
23675 .buffer()
23676 .read(cx)
23677 .as_singleton()
23678 .expect("have opened a single file by path")
23679 });
23680
23681 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23682 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23683 drop(buffer_snapshot);
23684 let actions = cx
23685 .update_window(*workspace, |_, window, cx| {
23686 project.code_actions(&buffer, anchor..anchor, window, cx)
23687 })
23688 .unwrap();
23689
23690 fake_server
23691 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23692 Ok(Some(vec![
23693 lsp::CodeLens {
23694 range: lsp::Range::default(),
23695 command: Some(lsp::Command {
23696 title: "Code lens command".to_owned(),
23697 command: "_the/command".to_owned(),
23698 arguments: None,
23699 }),
23700 data: None,
23701 },
23702 lsp::CodeLens {
23703 range: lsp::Range::default(),
23704 command: Some(lsp::Command {
23705 title: "Command not in capabilities".to_owned(),
23706 command: "not in capabilities".to_owned(),
23707 arguments: None,
23708 }),
23709 data: None,
23710 },
23711 lsp::CodeLens {
23712 range: lsp::Range {
23713 start: lsp::Position {
23714 line: 1,
23715 character: 1,
23716 },
23717 end: lsp::Position {
23718 line: 1,
23719 character: 1,
23720 },
23721 },
23722 command: Some(lsp::Command {
23723 title: "Command not in range".to_owned(),
23724 command: "_the/command".to_owned(),
23725 arguments: None,
23726 }),
23727 data: None,
23728 },
23729 ]))
23730 })
23731 .next()
23732 .await;
23733
23734 let actions = actions.await.unwrap();
23735 assert_eq!(
23736 actions.len(),
23737 1,
23738 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23739 );
23740 let action = actions[0].clone();
23741 let apply = project.update(cx, |project, cx| {
23742 project.apply_code_action(buffer.clone(), action, true, cx)
23743 });
23744
23745 // Resolving the code action does not populate its edits. In absence of
23746 // edits, we must execute the given command.
23747 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23748 |mut lens, _| async move {
23749 let lens_command = lens.command.as_mut().expect("should have a command");
23750 assert_eq!(lens_command.title, "Code lens command");
23751 lens_command.arguments = Some(vec![json!("the-argument")]);
23752 Ok(lens)
23753 },
23754 );
23755
23756 // While executing the command, the language server sends the editor
23757 // a `workspaceEdit` request.
23758 fake_server
23759 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23760 let fake = fake_server.clone();
23761 move |params, _| {
23762 assert_eq!(params.command, "_the/command");
23763 let fake = fake.clone();
23764 async move {
23765 fake.server
23766 .request::<lsp::request::ApplyWorkspaceEdit>(
23767 lsp::ApplyWorkspaceEditParams {
23768 label: None,
23769 edit: lsp::WorkspaceEdit {
23770 changes: Some(
23771 [(
23772 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23773 vec![lsp::TextEdit {
23774 range: lsp::Range::new(
23775 lsp::Position::new(0, 0),
23776 lsp::Position::new(0, 0),
23777 ),
23778 new_text: "X".into(),
23779 }],
23780 )]
23781 .into_iter()
23782 .collect(),
23783 ),
23784 ..lsp::WorkspaceEdit::default()
23785 },
23786 },
23787 )
23788 .await
23789 .into_response()
23790 .unwrap();
23791 Ok(Some(json!(null)))
23792 }
23793 }
23794 })
23795 .next()
23796 .await;
23797
23798 // Applying the code lens command returns a project transaction containing the edits
23799 // sent by the language server in its `workspaceEdit` request.
23800 let transaction = apply.await.unwrap();
23801 assert!(transaction.0.contains_key(&buffer));
23802 buffer.update(cx, |buffer, cx| {
23803 assert_eq!(buffer.text(), "Xa");
23804 buffer.undo(cx);
23805 assert_eq!(buffer.text(), "a");
23806 });
23807
23808 let actions_after_edits = cx
23809 .update_window(*workspace, |_, window, cx| {
23810 project.code_actions(&buffer, anchor..anchor, window, cx)
23811 })
23812 .unwrap()
23813 .await
23814 .unwrap();
23815 assert_eq!(
23816 actions, actions_after_edits,
23817 "For the same selection, same code lens actions should be returned"
23818 );
23819
23820 let _responses =
23821 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23822 panic!("No more code lens requests are expected");
23823 });
23824 editor.update_in(cx, |editor, window, cx| {
23825 editor.select_all(&SelectAll, window, cx);
23826 });
23827 cx.executor().run_until_parked();
23828 let new_actions = cx
23829 .update_window(*workspace, |_, window, cx| {
23830 project.code_actions(&buffer, anchor..anchor, window, cx)
23831 })
23832 .unwrap()
23833 .await
23834 .unwrap();
23835 assert_eq!(
23836 actions, new_actions,
23837 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23838 );
23839}
23840
23841#[gpui::test]
23842async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23843 init_test(cx, |_| {});
23844
23845 let fs = FakeFs::new(cx.executor());
23846 let main_text = r#"fn main() {
23847println!("1");
23848println!("2");
23849println!("3");
23850println!("4");
23851println!("5");
23852}"#;
23853 let lib_text = "mod foo {}";
23854 fs.insert_tree(
23855 path!("/a"),
23856 json!({
23857 "lib.rs": lib_text,
23858 "main.rs": main_text,
23859 }),
23860 )
23861 .await;
23862
23863 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23864 let (workspace, cx) =
23865 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23866 let worktree_id = workspace.update(cx, |workspace, cx| {
23867 workspace.project().update(cx, |project, cx| {
23868 project.worktrees(cx).next().unwrap().read(cx).id()
23869 })
23870 });
23871
23872 let expected_ranges = vec![
23873 Point::new(0, 0)..Point::new(0, 0),
23874 Point::new(1, 0)..Point::new(1, 1),
23875 Point::new(2, 0)..Point::new(2, 2),
23876 Point::new(3, 0)..Point::new(3, 3),
23877 ];
23878
23879 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23880 let editor_1 = workspace
23881 .update_in(cx, |workspace, window, cx| {
23882 workspace.open_path(
23883 (worktree_id, rel_path("main.rs")),
23884 Some(pane_1.downgrade()),
23885 true,
23886 window,
23887 cx,
23888 )
23889 })
23890 .unwrap()
23891 .await
23892 .downcast::<Editor>()
23893 .unwrap();
23894 pane_1.update(cx, |pane, cx| {
23895 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23896 open_editor.update(cx, |editor, cx| {
23897 assert_eq!(
23898 editor.display_text(cx),
23899 main_text,
23900 "Original main.rs text on initial open",
23901 );
23902 assert_eq!(
23903 editor
23904 .selections
23905 .all::<Point>(&editor.display_snapshot(cx))
23906 .into_iter()
23907 .map(|s| s.range())
23908 .collect::<Vec<_>>(),
23909 vec![Point::zero()..Point::zero()],
23910 "Default selections on initial open",
23911 );
23912 })
23913 });
23914 editor_1.update_in(cx, |editor, window, cx| {
23915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23916 s.select_ranges(expected_ranges.clone());
23917 });
23918 });
23919
23920 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23921 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23922 });
23923 let editor_2 = workspace
23924 .update_in(cx, |workspace, window, cx| {
23925 workspace.open_path(
23926 (worktree_id, rel_path("main.rs")),
23927 Some(pane_2.downgrade()),
23928 true,
23929 window,
23930 cx,
23931 )
23932 })
23933 .unwrap()
23934 .await
23935 .downcast::<Editor>()
23936 .unwrap();
23937 pane_2.update(cx, |pane, cx| {
23938 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23939 open_editor.update(cx, |editor, cx| {
23940 assert_eq!(
23941 editor.display_text(cx),
23942 main_text,
23943 "Original main.rs text on initial open in another panel",
23944 );
23945 assert_eq!(
23946 editor
23947 .selections
23948 .all::<Point>(&editor.display_snapshot(cx))
23949 .into_iter()
23950 .map(|s| s.range())
23951 .collect::<Vec<_>>(),
23952 vec![Point::zero()..Point::zero()],
23953 "Default selections on initial open in another panel",
23954 );
23955 })
23956 });
23957
23958 editor_2.update_in(cx, |editor, window, cx| {
23959 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23960 });
23961
23962 let _other_editor_1 = workspace
23963 .update_in(cx, |workspace, window, cx| {
23964 workspace.open_path(
23965 (worktree_id, rel_path("lib.rs")),
23966 Some(pane_1.downgrade()),
23967 true,
23968 window,
23969 cx,
23970 )
23971 })
23972 .unwrap()
23973 .await
23974 .downcast::<Editor>()
23975 .unwrap();
23976 pane_1
23977 .update_in(cx, |pane, window, cx| {
23978 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23979 })
23980 .await
23981 .unwrap();
23982 drop(editor_1);
23983 pane_1.update(cx, |pane, cx| {
23984 pane.active_item()
23985 .unwrap()
23986 .downcast::<Editor>()
23987 .unwrap()
23988 .update(cx, |editor, cx| {
23989 assert_eq!(
23990 editor.display_text(cx),
23991 lib_text,
23992 "Other file should be open and active",
23993 );
23994 });
23995 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23996 });
23997
23998 let _other_editor_2 = workspace
23999 .update_in(cx, |workspace, window, cx| {
24000 workspace.open_path(
24001 (worktree_id, rel_path("lib.rs")),
24002 Some(pane_2.downgrade()),
24003 true,
24004 window,
24005 cx,
24006 )
24007 })
24008 .unwrap()
24009 .await
24010 .downcast::<Editor>()
24011 .unwrap();
24012 pane_2
24013 .update_in(cx, |pane, window, cx| {
24014 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24015 })
24016 .await
24017 .unwrap();
24018 drop(editor_2);
24019 pane_2.update(cx, |pane, cx| {
24020 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24021 open_editor.update(cx, |editor, cx| {
24022 assert_eq!(
24023 editor.display_text(cx),
24024 lib_text,
24025 "Other file should be open and active in another panel too",
24026 );
24027 });
24028 assert_eq!(
24029 pane.items().count(),
24030 1,
24031 "No other editors should be open in another pane",
24032 );
24033 });
24034
24035 let _editor_1_reopened = workspace
24036 .update_in(cx, |workspace, window, cx| {
24037 workspace.open_path(
24038 (worktree_id, rel_path("main.rs")),
24039 Some(pane_1.downgrade()),
24040 true,
24041 window,
24042 cx,
24043 )
24044 })
24045 .unwrap()
24046 .await
24047 .downcast::<Editor>()
24048 .unwrap();
24049 let _editor_2_reopened = workspace
24050 .update_in(cx, |workspace, window, cx| {
24051 workspace.open_path(
24052 (worktree_id, rel_path("main.rs")),
24053 Some(pane_2.downgrade()),
24054 true,
24055 window,
24056 cx,
24057 )
24058 })
24059 .unwrap()
24060 .await
24061 .downcast::<Editor>()
24062 .unwrap();
24063 pane_1.update(cx, |pane, cx| {
24064 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24065 open_editor.update(cx, |editor, cx| {
24066 assert_eq!(
24067 editor.display_text(cx),
24068 main_text,
24069 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24070 );
24071 assert_eq!(
24072 editor
24073 .selections
24074 .all::<Point>(&editor.display_snapshot(cx))
24075 .into_iter()
24076 .map(|s| s.range())
24077 .collect::<Vec<_>>(),
24078 expected_ranges,
24079 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24080 );
24081 })
24082 });
24083 pane_2.update(cx, |pane, cx| {
24084 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24085 open_editor.update(cx, |editor, cx| {
24086 assert_eq!(
24087 editor.display_text(cx),
24088 r#"fn main() {
24089⋯rintln!("1");
24090⋯intln!("2");
24091⋯ntln!("3");
24092println!("4");
24093println!("5");
24094}"#,
24095 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24096 );
24097 assert_eq!(
24098 editor
24099 .selections
24100 .all::<Point>(&editor.display_snapshot(cx))
24101 .into_iter()
24102 .map(|s| s.range())
24103 .collect::<Vec<_>>(),
24104 vec![Point::zero()..Point::zero()],
24105 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24106 );
24107 })
24108 });
24109}
24110
24111#[gpui::test]
24112async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24113 init_test(cx, |_| {});
24114
24115 let fs = FakeFs::new(cx.executor());
24116 let main_text = r#"fn main() {
24117println!("1");
24118println!("2");
24119println!("3");
24120println!("4");
24121println!("5");
24122}"#;
24123 let lib_text = "mod foo {}";
24124 fs.insert_tree(
24125 path!("/a"),
24126 json!({
24127 "lib.rs": lib_text,
24128 "main.rs": main_text,
24129 }),
24130 )
24131 .await;
24132
24133 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24134 let (workspace, cx) =
24135 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24136 let worktree_id = workspace.update(cx, |workspace, cx| {
24137 workspace.project().update(cx, |project, cx| {
24138 project.worktrees(cx).next().unwrap().read(cx).id()
24139 })
24140 });
24141
24142 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24143 let editor = workspace
24144 .update_in(cx, |workspace, window, cx| {
24145 workspace.open_path(
24146 (worktree_id, rel_path("main.rs")),
24147 Some(pane.downgrade()),
24148 true,
24149 window,
24150 cx,
24151 )
24152 })
24153 .unwrap()
24154 .await
24155 .downcast::<Editor>()
24156 .unwrap();
24157 pane.update(cx, |pane, cx| {
24158 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24159 open_editor.update(cx, |editor, cx| {
24160 assert_eq!(
24161 editor.display_text(cx),
24162 main_text,
24163 "Original main.rs text on initial open",
24164 );
24165 })
24166 });
24167 editor.update_in(cx, |editor, window, cx| {
24168 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24169 });
24170
24171 cx.update_global(|store: &mut SettingsStore, cx| {
24172 store.update_user_settings(cx, |s| {
24173 s.workspace.restore_on_file_reopen = Some(false);
24174 });
24175 });
24176 editor.update_in(cx, |editor, window, cx| {
24177 editor.fold_ranges(
24178 vec![
24179 Point::new(1, 0)..Point::new(1, 1),
24180 Point::new(2, 0)..Point::new(2, 2),
24181 Point::new(3, 0)..Point::new(3, 3),
24182 ],
24183 false,
24184 window,
24185 cx,
24186 );
24187 });
24188 pane.update_in(cx, |pane, window, cx| {
24189 pane.close_all_items(&CloseAllItems::default(), window, cx)
24190 })
24191 .await
24192 .unwrap();
24193 pane.update(cx, |pane, _| {
24194 assert!(pane.active_item().is_none());
24195 });
24196 cx.update_global(|store: &mut SettingsStore, cx| {
24197 store.update_user_settings(cx, |s| {
24198 s.workspace.restore_on_file_reopen = Some(true);
24199 });
24200 });
24201
24202 let _editor_reopened = workspace
24203 .update_in(cx, |workspace, window, cx| {
24204 workspace.open_path(
24205 (worktree_id, rel_path("main.rs")),
24206 Some(pane.downgrade()),
24207 true,
24208 window,
24209 cx,
24210 )
24211 })
24212 .unwrap()
24213 .await
24214 .downcast::<Editor>()
24215 .unwrap();
24216 pane.update(cx, |pane, cx| {
24217 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24218 open_editor.update(cx, |editor, cx| {
24219 assert_eq!(
24220 editor.display_text(cx),
24221 main_text,
24222 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24223 );
24224 })
24225 });
24226}
24227
24228#[gpui::test]
24229async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24230 struct EmptyModalView {
24231 focus_handle: gpui::FocusHandle,
24232 }
24233 impl EventEmitter<DismissEvent> for EmptyModalView {}
24234 impl Render for EmptyModalView {
24235 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24236 div()
24237 }
24238 }
24239 impl Focusable for EmptyModalView {
24240 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24241 self.focus_handle.clone()
24242 }
24243 }
24244 impl workspace::ModalView for EmptyModalView {}
24245 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24246 EmptyModalView {
24247 focus_handle: cx.focus_handle(),
24248 }
24249 }
24250
24251 init_test(cx, |_| {});
24252
24253 let fs = FakeFs::new(cx.executor());
24254 let project = Project::test(fs, [], cx).await;
24255 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24256 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24257 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24258 let editor = cx.new_window_entity(|window, cx| {
24259 Editor::new(
24260 EditorMode::full(),
24261 buffer,
24262 Some(project.clone()),
24263 window,
24264 cx,
24265 )
24266 });
24267 workspace
24268 .update(cx, |workspace, window, cx| {
24269 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24270 })
24271 .unwrap();
24272 editor.update_in(cx, |editor, window, cx| {
24273 editor.open_context_menu(&OpenContextMenu, window, cx);
24274 assert!(editor.mouse_context_menu.is_some());
24275 });
24276 workspace
24277 .update(cx, |workspace, window, cx| {
24278 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24279 })
24280 .unwrap();
24281 cx.read(|cx| {
24282 assert!(editor.read(cx).mouse_context_menu.is_none());
24283 });
24284}
24285
24286fn set_linked_edit_ranges(
24287 opening: (Point, Point),
24288 closing: (Point, Point),
24289 editor: &mut Editor,
24290 cx: &mut Context<Editor>,
24291) {
24292 let Some((buffer, _)) = editor
24293 .buffer
24294 .read(cx)
24295 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24296 else {
24297 panic!("Failed to get buffer for selection position");
24298 };
24299 let buffer = buffer.read(cx);
24300 let buffer_id = buffer.remote_id();
24301 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24302 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24303 let mut linked_ranges = HashMap::default();
24304 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24305 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24306}
24307
24308#[gpui::test]
24309async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24310 init_test(cx, |_| {});
24311
24312 let fs = FakeFs::new(cx.executor());
24313 fs.insert_file(path!("/file.html"), Default::default())
24314 .await;
24315
24316 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24317
24318 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24319 let html_language = Arc::new(Language::new(
24320 LanguageConfig {
24321 name: "HTML".into(),
24322 matcher: LanguageMatcher {
24323 path_suffixes: vec!["html".to_string()],
24324 ..LanguageMatcher::default()
24325 },
24326 brackets: BracketPairConfig {
24327 pairs: vec![BracketPair {
24328 start: "<".into(),
24329 end: ">".into(),
24330 close: true,
24331 ..Default::default()
24332 }],
24333 ..Default::default()
24334 },
24335 ..Default::default()
24336 },
24337 Some(tree_sitter_html::LANGUAGE.into()),
24338 ));
24339 language_registry.add(html_language);
24340 let mut fake_servers = language_registry.register_fake_lsp(
24341 "HTML",
24342 FakeLspAdapter {
24343 capabilities: lsp::ServerCapabilities {
24344 completion_provider: Some(lsp::CompletionOptions {
24345 resolve_provider: Some(true),
24346 ..Default::default()
24347 }),
24348 ..Default::default()
24349 },
24350 ..Default::default()
24351 },
24352 );
24353
24354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24355 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24356
24357 let worktree_id = workspace
24358 .update(cx, |workspace, _window, cx| {
24359 workspace.project().update(cx, |project, cx| {
24360 project.worktrees(cx).next().unwrap().read(cx).id()
24361 })
24362 })
24363 .unwrap();
24364 project
24365 .update(cx, |project, cx| {
24366 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24367 })
24368 .await
24369 .unwrap();
24370 let editor = workspace
24371 .update(cx, |workspace, window, cx| {
24372 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24373 })
24374 .unwrap()
24375 .await
24376 .unwrap()
24377 .downcast::<Editor>()
24378 .unwrap();
24379
24380 let fake_server = fake_servers.next().await.unwrap();
24381 editor.update_in(cx, |editor, window, cx| {
24382 editor.set_text("<ad></ad>", window, cx);
24383 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24384 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24385 });
24386 set_linked_edit_ranges(
24387 (Point::new(0, 1), Point::new(0, 3)),
24388 (Point::new(0, 6), Point::new(0, 8)),
24389 editor,
24390 cx,
24391 );
24392 });
24393 let mut completion_handle =
24394 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24395 Ok(Some(lsp::CompletionResponse::Array(vec![
24396 lsp::CompletionItem {
24397 label: "head".to_string(),
24398 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24399 lsp::InsertReplaceEdit {
24400 new_text: "head".to_string(),
24401 insert: lsp::Range::new(
24402 lsp::Position::new(0, 1),
24403 lsp::Position::new(0, 3),
24404 ),
24405 replace: lsp::Range::new(
24406 lsp::Position::new(0, 1),
24407 lsp::Position::new(0, 3),
24408 ),
24409 },
24410 )),
24411 ..Default::default()
24412 },
24413 ])))
24414 });
24415 editor.update_in(cx, |editor, window, cx| {
24416 editor.show_completions(
24417 &ShowCompletions {
24418 trigger: None,
24419 snippets_only: false,
24420 },
24421 window,
24422 cx,
24423 );
24424 });
24425 cx.run_until_parked();
24426 completion_handle.next().await.unwrap();
24427 editor.update(cx, |editor, _| {
24428 assert!(
24429 editor.context_menu_visible(),
24430 "Completion menu should be visible"
24431 );
24432 });
24433 editor.update_in(cx, |editor, window, cx| {
24434 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24435 });
24436 cx.executor().run_until_parked();
24437 editor.update(cx, |editor, cx| {
24438 assert_eq!(editor.text(cx), "<head></head>");
24439 });
24440}
24441
24442#[gpui::test]
24443async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24444 init_test(cx, |_| {});
24445
24446 let mut cx = EditorTestContext::new(cx).await;
24447 let language = Arc::new(Language::new(
24448 LanguageConfig {
24449 name: "TSX".into(),
24450 matcher: LanguageMatcher {
24451 path_suffixes: vec!["tsx".to_string()],
24452 ..LanguageMatcher::default()
24453 },
24454 brackets: BracketPairConfig {
24455 pairs: vec![BracketPair {
24456 start: "<".into(),
24457 end: ">".into(),
24458 close: true,
24459 ..Default::default()
24460 }],
24461 ..Default::default()
24462 },
24463 linked_edit_characters: HashSet::from_iter(['.']),
24464 ..Default::default()
24465 },
24466 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24467 ));
24468 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24469
24470 // Test typing > does not extend linked pair
24471 cx.set_state("<divˇ<div></div>");
24472 cx.update_editor(|editor, _, cx| {
24473 set_linked_edit_ranges(
24474 (Point::new(0, 1), Point::new(0, 4)),
24475 (Point::new(0, 11), Point::new(0, 14)),
24476 editor,
24477 cx,
24478 );
24479 });
24480 cx.update_editor(|editor, window, cx| {
24481 editor.handle_input(">", window, cx);
24482 });
24483 cx.assert_editor_state("<div>ˇ<div></div>");
24484
24485 // Test typing . do extend linked pair
24486 cx.set_state("<Animatedˇ></Animated>");
24487 cx.update_editor(|editor, _, cx| {
24488 set_linked_edit_ranges(
24489 (Point::new(0, 1), Point::new(0, 9)),
24490 (Point::new(0, 12), Point::new(0, 20)),
24491 editor,
24492 cx,
24493 );
24494 });
24495 cx.update_editor(|editor, window, cx| {
24496 editor.handle_input(".", window, cx);
24497 });
24498 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24499 cx.update_editor(|editor, _, cx| {
24500 set_linked_edit_ranges(
24501 (Point::new(0, 1), Point::new(0, 10)),
24502 (Point::new(0, 13), Point::new(0, 21)),
24503 editor,
24504 cx,
24505 );
24506 });
24507 cx.update_editor(|editor, window, cx| {
24508 editor.handle_input("V", window, cx);
24509 });
24510 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24511}
24512
24513#[gpui::test]
24514async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24515 init_test(cx, |_| {});
24516
24517 let fs = FakeFs::new(cx.executor());
24518 fs.insert_tree(
24519 path!("/root"),
24520 json!({
24521 "a": {
24522 "main.rs": "fn main() {}",
24523 },
24524 "foo": {
24525 "bar": {
24526 "external_file.rs": "pub mod external {}",
24527 }
24528 }
24529 }),
24530 )
24531 .await;
24532
24533 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24534 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24535 language_registry.add(rust_lang());
24536 let _fake_servers = language_registry.register_fake_lsp(
24537 "Rust",
24538 FakeLspAdapter {
24539 ..FakeLspAdapter::default()
24540 },
24541 );
24542 let (workspace, cx) =
24543 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24544 let worktree_id = workspace.update(cx, |workspace, cx| {
24545 workspace.project().update(cx, |project, cx| {
24546 project.worktrees(cx).next().unwrap().read(cx).id()
24547 })
24548 });
24549
24550 let assert_language_servers_count =
24551 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24552 project.update(cx, |project, cx| {
24553 let current = project
24554 .lsp_store()
24555 .read(cx)
24556 .as_local()
24557 .unwrap()
24558 .language_servers
24559 .len();
24560 assert_eq!(expected, current, "{context}");
24561 });
24562 };
24563
24564 assert_language_servers_count(
24565 0,
24566 "No servers should be running before any file is open",
24567 cx,
24568 );
24569 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24570 let main_editor = workspace
24571 .update_in(cx, |workspace, window, cx| {
24572 workspace.open_path(
24573 (worktree_id, rel_path("main.rs")),
24574 Some(pane.downgrade()),
24575 true,
24576 window,
24577 cx,
24578 )
24579 })
24580 .unwrap()
24581 .await
24582 .downcast::<Editor>()
24583 .unwrap();
24584 pane.update(cx, |pane, cx| {
24585 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24586 open_editor.update(cx, |editor, cx| {
24587 assert_eq!(
24588 editor.display_text(cx),
24589 "fn main() {}",
24590 "Original main.rs text on initial open",
24591 );
24592 });
24593 assert_eq!(open_editor, main_editor);
24594 });
24595 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24596
24597 let external_editor = workspace
24598 .update_in(cx, |workspace, window, cx| {
24599 workspace.open_abs_path(
24600 PathBuf::from("/root/foo/bar/external_file.rs"),
24601 OpenOptions::default(),
24602 window,
24603 cx,
24604 )
24605 })
24606 .await
24607 .expect("opening external file")
24608 .downcast::<Editor>()
24609 .expect("downcasted external file's open element to editor");
24610 pane.update(cx, |pane, cx| {
24611 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24612 open_editor.update(cx, |editor, cx| {
24613 assert_eq!(
24614 editor.display_text(cx),
24615 "pub mod external {}",
24616 "External file is open now",
24617 );
24618 });
24619 assert_eq!(open_editor, external_editor);
24620 });
24621 assert_language_servers_count(
24622 1,
24623 "Second, external, *.rs file should join the existing server",
24624 cx,
24625 );
24626
24627 pane.update_in(cx, |pane, window, cx| {
24628 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24629 })
24630 .await
24631 .unwrap();
24632 pane.update_in(cx, |pane, window, cx| {
24633 pane.navigate_backward(&Default::default(), window, cx);
24634 });
24635 cx.run_until_parked();
24636 pane.update(cx, |pane, cx| {
24637 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24638 open_editor.update(cx, |editor, cx| {
24639 assert_eq!(
24640 editor.display_text(cx),
24641 "pub mod external {}",
24642 "External file is open now",
24643 );
24644 });
24645 });
24646 assert_language_servers_count(
24647 1,
24648 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24649 cx,
24650 );
24651
24652 cx.update(|_, cx| {
24653 workspace::reload(cx);
24654 });
24655 assert_language_servers_count(
24656 1,
24657 "After reloading the worktree with local and external files opened, only one project should be started",
24658 cx,
24659 );
24660}
24661
24662#[gpui::test]
24663async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24664 init_test(cx, |_| {});
24665
24666 let mut cx = EditorTestContext::new(cx).await;
24667 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24668 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24669
24670 // test cursor move to start of each line on tab
24671 // for `if`, `elif`, `else`, `while`, `with` and `for`
24672 cx.set_state(indoc! {"
24673 def main():
24674 ˇ for item in items:
24675 ˇ while item.active:
24676 ˇ if item.value > 10:
24677 ˇ continue
24678 ˇ elif item.value < 0:
24679 ˇ break
24680 ˇ else:
24681 ˇ with item.context() as ctx:
24682 ˇ yield count
24683 ˇ else:
24684 ˇ log('while else')
24685 ˇ else:
24686 ˇ log('for else')
24687 "});
24688 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24689 cx.assert_editor_state(indoc! {"
24690 def main():
24691 ˇfor item in items:
24692 ˇwhile item.active:
24693 ˇif item.value > 10:
24694 ˇcontinue
24695 ˇelif item.value < 0:
24696 ˇbreak
24697 ˇelse:
24698 ˇwith item.context() as ctx:
24699 ˇyield count
24700 ˇelse:
24701 ˇlog('while else')
24702 ˇelse:
24703 ˇlog('for else')
24704 "});
24705 // test relative indent is preserved when tab
24706 // for `if`, `elif`, `else`, `while`, `with` and `for`
24707 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24708 cx.assert_editor_state(indoc! {"
24709 def main():
24710 ˇfor item in items:
24711 ˇwhile item.active:
24712 ˇif item.value > 10:
24713 ˇcontinue
24714 ˇelif item.value < 0:
24715 ˇbreak
24716 ˇelse:
24717 ˇwith item.context() as ctx:
24718 ˇyield count
24719 ˇelse:
24720 ˇlog('while else')
24721 ˇelse:
24722 ˇlog('for else')
24723 "});
24724
24725 // test cursor move to start of each line on tab
24726 // for `try`, `except`, `else`, `finally`, `match` and `def`
24727 cx.set_state(indoc! {"
24728 def main():
24729 ˇ try:
24730 ˇ fetch()
24731 ˇ except ValueError:
24732 ˇ handle_error()
24733 ˇ else:
24734 ˇ match value:
24735 ˇ case _:
24736 ˇ finally:
24737 ˇ def status():
24738 ˇ return 0
24739 "});
24740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24741 cx.assert_editor_state(indoc! {"
24742 def main():
24743 ˇtry:
24744 ˇfetch()
24745 ˇexcept ValueError:
24746 ˇhandle_error()
24747 ˇelse:
24748 ˇmatch value:
24749 ˇcase _:
24750 ˇfinally:
24751 ˇdef status():
24752 ˇreturn 0
24753 "});
24754 // test relative indent is preserved when tab
24755 // for `try`, `except`, `else`, `finally`, `match` and `def`
24756 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24757 cx.assert_editor_state(indoc! {"
24758 def main():
24759 ˇtry:
24760 ˇfetch()
24761 ˇexcept ValueError:
24762 ˇhandle_error()
24763 ˇelse:
24764 ˇmatch value:
24765 ˇcase _:
24766 ˇfinally:
24767 ˇdef status():
24768 ˇreturn 0
24769 "});
24770}
24771
24772#[gpui::test]
24773async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24774 init_test(cx, |_| {});
24775
24776 let mut cx = EditorTestContext::new(cx).await;
24777 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24778 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24779
24780 // test `else` auto outdents when typed inside `if` block
24781 cx.set_state(indoc! {"
24782 def main():
24783 if i == 2:
24784 return
24785 ˇ
24786 "});
24787 cx.update_editor(|editor, window, cx| {
24788 editor.handle_input("else:", window, cx);
24789 });
24790 cx.assert_editor_state(indoc! {"
24791 def main():
24792 if i == 2:
24793 return
24794 else:ˇ
24795 "});
24796
24797 // test `except` auto outdents when typed inside `try` block
24798 cx.set_state(indoc! {"
24799 def main():
24800 try:
24801 i = 2
24802 ˇ
24803 "});
24804 cx.update_editor(|editor, window, cx| {
24805 editor.handle_input("except:", window, cx);
24806 });
24807 cx.assert_editor_state(indoc! {"
24808 def main():
24809 try:
24810 i = 2
24811 except:ˇ
24812 "});
24813
24814 // test `else` auto outdents when typed inside `except` block
24815 cx.set_state(indoc! {"
24816 def main():
24817 try:
24818 i = 2
24819 except:
24820 j = 2
24821 ˇ
24822 "});
24823 cx.update_editor(|editor, window, cx| {
24824 editor.handle_input("else:", window, cx);
24825 });
24826 cx.assert_editor_state(indoc! {"
24827 def main():
24828 try:
24829 i = 2
24830 except:
24831 j = 2
24832 else:ˇ
24833 "});
24834
24835 // test `finally` auto outdents when typed inside `else` block
24836 cx.set_state(indoc! {"
24837 def main():
24838 try:
24839 i = 2
24840 except:
24841 j = 2
24842 else:
24843 k = 2
24844 ˇ
24845 "});
24846 cx.update_editor(|editor, window, cx| {
24847 editor.handle_input("finally:", window, cx);
24848 });
24849 cx.assert_editor_state(indoc! {"
24850 def main():
24851 try:
24852 i = 2
24853 except:
24854 j = 2
24855 else:
24856 k = 2
24857 finally:ˇ
24858 "});
24859
24860 // test `else` does not outdents when typed inside `except` block right after for block
24861 cx.set_state(indoc! {"
24862 def main():
24863 try:
24864 i = 2
24865 except:
24866 for i in range(n):
24867 pass
24868 ˇ
24869 "});
24870 cx.update_editor(|editor, window, cx| {
24871 editor.handle_input("else:", window, cx);
24872 });
24873 cx.assert_editor_state(indoc! {"
24874 def main():
24875 try:
24876 i = 2
24877 except:
24878 for i in range(n):
24879 pass
24880 else:ˇ
24881 "});
24882
24883 // test `finally` auto outdents when typed inside `else` block right after for block
24884 cx.set_state(indoc! {"
24885 def main():
24886 try:
24887 i = 2
24888 except:
24889 j = 2
24890 else:
24891 for i in range(n):
24892 pass
24893 ˇ
24894 "});
24895 cx.update_editor(|editor, window, cx| {
24896 editor.handle_input("finally:", window, cx);
24897 });
24898 cx.assert_editor_state(indoc! {"
24899 def main():
24900 try:
24901 i = 2
24902 except:
24903 j = 2
24904 else:
24905 for i in range(n):
24906 pass
24907 finally:ˇ
24908 "});
24909
24910 // test `except` outdents to inner "try" block
24911 cx.set_state(indoc! {"
24912 def main():
24913 try:
24914 i = 2
24915 if i == 2:
24916 try:
24917 i = 3
24918 ˇ
24919 "});
24920 cx.update_editor(|editor, window, cx| {
24921 editor.handle_input("except:", window, cx);
24922 });
24923 cx.assert_editor_state(indoc! {"
24924 def main():
24925 try:
24926 i = 2
24927 if i == 2:
24928 try:
24929 i = 3
24930 except:ˇ
24931 "});
24932
24933 // test `except` outdents to outer "try" block
24934 cx.set_state(indoc! {"
24935 def main():
24936 try:
24937 i = 2
24938 if i == 2:
24939 try:
24940 i = 3
24941 ˇ
24942 "});
24943 cx.update_editor(|editor, window, cx| {
24944 editor.handle_input("except:", window, cx);
24945 });
24946 cx.assert_editor_state(indoc! {"
24947 def main():
24948 try:
24949 i = 2
24950 if i == 2:
24951 try:
24952 i = 3
24953 except:ˇ
24954 "});
24955
24956 // test `else` stays at correct indent when typed after `for` block
24957 cx.set_state(indoc! {"
24958 def main():
24959 for i in range(10):
24960 if i == 3:
24961 break
24962 ˇ
24963 "});
24964 cx.update_editor(|editor, window, cx| {
24965 editor.handle_input("else:", window, cx);
24966 });
24967 cx.assert_editor_state(indoc! {"
24968 def main():
24969 for i in range(10):
24970 if i == 3:
24971 break
24972 else:ˇ
24973 "});
24974
24975 // test does not outdent on typing after line with square brackets
24976 cx.set_state(indoc! {"
24977 def f() -> list[str]:
24978 ˇ
24979 "});
24980 cx.update_editor(|editor, window, cx| {
24981 editor.handle_input("a", window, cx);
24982 });
24983 cx.assert_editor_state(indoc! {"
24984 def f() -> list[str]:
24985 aˇ
24986 "});
24987
24988 // test does not outdent on typing : after case keyword
24989 cx.set_state(indoc! {"
24990 match 1:
24991 caseˇ
24992 "});
24993 cx.update_editor(|editor, window, cx| {
24994 editor.handle_input(":", window, cx);
24995 });
24996 cx.assert_editor_state(indoc! {"
24997 match 1:
24998 case:ˇ
24999 "});
25000}
25001
25002#[gpui::test]
25003async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25004 init_test(cx, |_| {});
25005 update_test_language_settings(cx, |settings| {
25006 settings.defaults.extend_comment_on_newline = Some(false);
25007 });
25008 let mut cx = EditorTestContext::new(cx).await;
25009 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25010 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25011
25012 // test correct indent after newline on comment
25013 cx.set_state(indoc! {"
25014 # COMMENT:ˇ
25015 "});
25016 cx.update_editor(|editor, window, cx| {
25017 editor.newline(&Newline, window, cx);
25018 });
25019 cx.assert_editor_state(indoc! {"
25020 # COMMENT:
25021 ˇ
25022 "});
25023
25024 // test correct indent after newline in brackets
25025 cx.set_state(indoc! {"
25026 {ˇ}
25027 "});
25028 cx.update_editor(|editor, window, cx| {
25029 editor.newline(&Newline, window, cx);
25030 });
25031 cx.run_until_parked();
25032 cx.assert_editor_state(indoc! {"
25033 {
25034 ˇ
25035 }
25036 "});
25037
25038 cx.set_state(indoc! {"
25039 (ˇ)
25040 "});
25041 cx.update_editor(|editor, window, cx| {
25042 editor.newline(&Newline, window, cx);
25043 });
25044 cx.run_until_parked();
25045 cx.assert_editor_state(indoc! {"
25046 (
25047 ˇ
25048 )
25049 "});
25050
25051 // do not indent after empty lists or dictionaries
25052 cx.set_state(indoc! {"
25053 a = []ˇ
25054 "});
25055 cx.update_editor(|editor, window, cx| {
25056 editor.newline(&Newline, window, cx);
25057 });
25058 cx.run_until_parked();
25059 cx.assert_editor_state(indoc! {"
25060 a = []
25061 ˇ
25062 "});
25063}
25064
25065#[gpui::test]
25066async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25067 init_test(cx, |_| {});
25068
25069 let mut cx = EditorTestContext::new(cx).await;
25070 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25071 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25072
25073 // test cursor move to start of each line on tab
25074 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25075 cx.set_state(indoc! {"
25076 function main() {
25077 ˇ for item in $items; do
25078 ˇ while [ -n \"$item\" ]; do
25079 ˇ if [ \"$value\" -gt 10 ]; then
25080 ˇ continue
25081 ˇ elif [ \"$value\" -lt 0 ]; then
25082 ˇ break
25083 ˇ else
25084 ˇ echo \"$item\"
25085 ˇ fi
25086 ˇ done
25087 ˇ done
25088 ˇ}
25089 "});
25090 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25091 cx.assert_editor_state(indoc! {"
25092 function main() {
25093 ˇfor item in $items; do
25094 ˇwhile [ -n \"$item\" ]; do
25095 ˇif [ \"$value\" -gt 10 ]; then
25096 ˇcontinue
25097 ˇelif [ \"$value\" -lt 0 ]; then
25098 ˇbreak
25099 ˇelse
25100 ˇecho \"$item\"
25101 ˇfi
25102 ˇdone
25103 ˇdone
25104 ˇ}
25105 "});
25106 // test relative indent is preserved when tab
25107 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25108 cx.assert_editor_state(indoc! {"
25109 function main() {
25110 ˇfor item in $items; do
25111 ˇwhile [ -n \"$item\" ]; do
25112 ˇif [ \"$value\" -gt 10 ]; then
25113 ˇcontinue
25114 ˇelif [ \"$value\" -lt 0 ]; then
25115 ˇbreak
25116 ˇelse
25117 ˇecho \"$item\"
25118 ˇfi
25119 ˇdone
25120 ˇdone
25121 ˇ}
25122 "});
25123
25124 // test cursor move to start of each line on tab
25125 // for `case` statement with patterns
25126 cx.set_state(indoc! {"
25127 function handle() {
25128 ˇ case \"$1\" in
25129 ˇ start)
25130 ˇ echo \"a\"
25131 ˇ ;;
25132 ˇ stop)
25133 ˇ echo \"b\"
25134 ˇ ;;
25135 ˇ *)
25136 ˇ echo \"c\"
25137 ˇ ;;
25138 ˇ esac
25139 ˇ}
25140 "});
25141 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25142 cx.assert_editor_state(indoc! {"
25143 function handle() {
25144 ˇcase \"$1\" in
25145 ˇstart)
25146 ˇecho \"a\"
25147 ˇ;;
25148 ˇstop)
25149 ˇecho \"b\"
25150 ˇ;;
25151 ˇ*)
25152 ˇecho \"c\"
25153 ˇ;;
25154 ˇesac
25155 ˇ}
25156 "});
25157}
25158
25159#[gpui::test]
25160async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25161 init_test(cx, |_| {});
25162
25163 let mut cx = EditorTestContext::new(cx).await;
25164 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25165 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25166
25167 // test indents on comment insert
25168 cx.set_state(indoc! {"
25169 function main() {
25170 ˇ for item in $items; do
25171 ˇ while [ -n \"$item\" ]; do
25172 ˇ if [ \"$value\" -gt 10 ]; then
25173 ˇ continue
25174 ˇ elif [ \"$value\" -lt 0 ]; then
25175 ˇ break
25176 ˇ else
25177 ˇ echo \"$item\"
25178 ˇ fi
25179 ˇ done
25180 ˇ done
25181 ˇ}
25182 "});
25183 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25184 cx.assert_editor_state(indoc! {"
25185 function main() {
25186 #ˇ for item in $items; do
25187 #ˇ while [ -n \"$item\" ]; do
25188 #ˇ if [ \"$value\" -gt 10 ]; then
25189 #ˇ continue
25190 #ˇ elif [ \"$value\" -lt 0 ]; then
25191 #ˇ break
25192 #ˇ else
25193 #ˇ echo \"$item\"
25194 #ˇ fi
25195 #ˇ done
25196 #ˇ done
25197 #ˇ}
25198 "});
25199}
25200
25201#[gpui::test]
25202async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25203 init_test(cx, |_| {});
25204
25205 let mut cx = EditorTestContext::new(cx).await;
25206 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25207 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25208
25209 // test `else` auto outdents when typed inside `if` block
25210 cx.set_state(indoc! {"
25211 if [ \"$1\" = \"test\" ]; then
25212 echo \"foo bar\"
25213 ˇ
25214 "});
25215 cx.update_editor(|editor, window, cx| {
25216 editor.handle_input("else", window, cx);
25217 });
25218 cx.assert_editor_state(indoc! {"
25219 if [ \"$1\" = \"test\" ]; then
25220 echo \"foo bar\"
25221 elseˇ
25222 "});
25223
25224 // test `elif` auto outdents when typed inside `if` block
25225 cx.set_state(indoc! {"
25226 if [ \"$1\" = \"test\" ]; then
25227 echo \"foo bar\"
25228 ˇ
25229 "});
25230 cx.update_editor(|editor, window, cx| {
25231 editor.handle_input("elif", window, cx);
25232 });
25233 cx.assert_editor_state(indoc! {"
25234 if [ \"$1\" = \"test\" ]; then
25235 echo \"foo bar\"
25236 elifˇ
25237 "});
25238
25239 // test `fi` auto outdents when typed inside `else` block
25240 cx.set_state(indoc! {"
25241 if [ \"$1\" = \"test\" ]; then
25242 echo \"foo bar\"
25243 else
25244 echo \"bar baz\"
25245 ˇ
25246 "});
25247 cx.update_editor(|editor, window, cx| {
25248 editor.handle_input("fi", window, cx);
25249 });
25250 cx.assert_editor_state(indoc! {"
25251 if [ \"$1\" = \"test\" ]; then
25252 echo \"foo bar\"
25253 else
25254 echo \"bar baz\"
25255 fiˇ
25256 "});
25257
25258 // test `done` auto outdents when typed inside `while` block
25259 cx.set_state(indoc! {"
25260 while read line; do
25261 echo \"$line\"
25262 ˇ
25263 "});
25264 cx.update_editor(|editor, window, cx| {
25265 editor.handle_input("done", window, cx);
25266 });
25267 cx.assert_editor_state(indoc! {"
25268 while read line; do
25269 echo \"$line\"
25270 doneˇ
25271 "});
25272
25273 // test `done` auto outdents when typed inside `for` block
25274 cx.set_state(indoc! {"
25275 for file in *.txt; do
25276 cat \"$file\"
25277 ˇ
25278 "});
25279 cx.update_editor(|editor, window, cx| {
25280 editor.handle_input("done", window, cx);
25281 });
25282 cx.assert_editor_state(indoc! {"
25283 for file in *.txt; do
25284 cat \"$file\"
25285 doneˇ
25286 "});
25287
25288 // test `esac` auto outdents when typed inside `case` block
25289 cx.set_state(indoc! {"
25290 case \"$1\" in
25291 start)
25292 echo \"foo bar\"
25293 ;;
25294 stop)
25295 echo \"bar baz\"
25296 ;;
25297 ˇ
25298 "});
25299 cx.update_editor(|editor, window, cx| {
25300 editor.handle_input("esac", window, cx);
25301 });
25302 cx.assert_editor_state(indoc! {"
25303 case \"$1\" in
25304 start)
25305 echo \"foo bar\"
25306 ;;
25307 stop)
25308 echo \"bar baz\"
25309 ;;
25310 esacˇ
25311 "});
25312
25313 // test `*)` auto outdents when typed inside `case` block
25314 cx.set_state(indoc! {"
25315 case \"$1\" in
25316 start)
25317 echo \"foo bar\"
25318 ;;
25319 ˇ
25320 "});
25321 cx.update_editor(|editor, window, cx| {
25322 editor.handle_input("*)", window, cx);
25323 });
25324 cx.assert_editor_state(indoc! {"
25325 case \"$1\" in
25326 start)
25327 echo \"foo bar\"
25328 ;;
25329 *)ˇ
25330 "});
25331
25332 // test `fi` outdents to correct level with nested if blocks
25333 cx.set_state(indoc! {"
25334 if [ \"$1\" = \"test\" ]; then
25335 echo \"outer if\"
25336 if [ \"$2\" = \"debug\" ]; then
25337 echo \"inner if\"
25338 ˇ
25339 "});
25340 cx.update_editor(|editor, window, cx| {
25341 editor.handle_input("fi", window, cx);
25342 });
25343 cx.assert_editor_state(indoc! {"
25344 if [ \"$1\" = \"test\" ]; then
25345 echo \"outer if\"
25346 if [ \"$2\" = \"debug\" ]; then
25347 echo \"inner if\"
25348 fiˇ
25349 "});
25350}
25351
25352#[gpui::test]
25353async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25354 init_test(cx, |_| {});
25355 update_test_language_settings(cx, |settings| {
25356 settings.defaults.extend_comment_on_newline = Some(false);
25357 });
25358 let mut cx = EditorTestContext::new(cx).await;
25359 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25360 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25361
25362 // test correct indent after newline on comment
25363 cx.set_state(indoc! {"
25364 # COMMENT:ˇ
25365 "});
25366 cx.update_editor(|editor, window, cx| {
25367 editor.newline(&Newline, window, cx);
25368 });
25369 cx.assert_editor_state(indoc! {"
25370 # COMMENT:
25371 ˇ
25372 "});
25373
25374 // test correct indent after newline after `then`
25375 cx.set_state(indoc! {"
25376
25377 if [ \"$1\" = \"test\" ]; thenˇ
25378 "});
25379 cx.update_editor(|editor, window, cx| {
25380 editor.newline(&Newline, window, cx);
25381 });
25382 cx.run_until_parked();
25383 cx.assert_editor_state(indoc! {"
25384
25385 if [ \"$1\" = \"test\" ]; then
25386 ˇ
25387 "});
25388
25389 // test correct indent after newline after `else`
25390 cx.set_state(indoc! {"
25391 if [ \"$1\" = \"test\" ]; then
25392 elseˇ
25393 "});
25394 cx.update_editor(|editor, window, cx| {
25395 editor.newline(&Newline, window, cx);
25396 });
25397 cx.run_until_parked();
25398 cx.assert_editor_state(indoc! {"
25399 if [ \"$1\" = \"test\" ]; then
25400 else
25401 ˇ
25402 "});
25403
25404 // test correct indent after newline after `elif`
25405 cx.set_state(indoc! {"
25406 if [ \"$1\" = \"test\" ]; then
25407 elifˇ
25408 "});
25409 cx.update_editor(|editor, window, cx| {
25410 editor.newline(&Newline, window, cx);
25411 });
25412 cx.run_until_parked();
25413 cx.assert_editor_state(indoc! {"
25414 if [ \"$1\" = \"test\" ]; then
25415 elif
25416 ˇ
25417 "});
25418
25419 // test correct indent after newline after `do`
25420 cx.set_state(indoc! {"
25421 for file in *.txt; doˇ
25422 "});
25423 cx.update_editor(|editor, window, cx| {
25424 editor.newline(&Newline, window, cx);
25425 });
25426 cx.run_until_parked();
25427 cx.assert_editor_state(indoc! {"
25428 for file in *.txt; do
25429 ˇ
25430 "});
25431
25432 // test correct indent after newline after case pattern
25433 cx.set_state(indoc! {"
25434 case \"$1\" in
25435 start)ˇ
25436 "});
25437 cx.update_editor(|editor, window, cx| {
25438 editor.newline(&Newline, window, cx);
25439 });
25440 cx.run_until_parked();
25441 cx.assert_editor_state(indoc! {"
25442 case \"$1\" in
25443 start)
25444 ˇ
25445 "});
25446
25447 // test correct indent after newline after case pattern
25448 cx.set_state(indoc! {"
25449 case \"$1\" in
25450 start)
25451 ;;
25452 *)ˇ
25453 "});
25454 cx.update_editor(|editor, window, cx| {
25455 editor.newline(&Newline, window, cx);
25456 });
25457 cx.run_until_parked();
25458 cx.assert_editor_state(indoc! {"
25459 case \"$1\" in
25460 start)
25461 ;;
25462 *)
25463 ˇ
25464 "});
25465
25466 // test correct indent after newline after function opening brace
25467 cx.set_state(indoc! {"
25468 function test() {ˇ}
25469 "});
25470 cx.update_editor(|editor, window, cx| {
25471 editor.newline(&Newline, window, cx);
25472 });
25473 cx.run_until_parked();
25474 cx.assert_editor_state(indoc! {"
25475 function test() {
25476 ˇ
25477 }
25478 "});
25479
25480 // test no extra indent after semicolon on same line
25481 cx.set_state(indoc! {"
25482 echo \"test\";ˇ
25483 "});
25484 cx.update_editor(|editor, window, cx| {
25485 editor.newline(&Newline, window, cx);
25486 });
25487 cx.run_until_parked();
25488 cx.assert_editor_state(indoc! {"
25489 echo \"test\";
25490 ˇ
25491 "});
25492}
25493
25494fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25495 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25496 point..point
25497}
25498
25499#[track_caller]
25500fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25501 let (text, ranges) = marked_text_ranges(marked_text, true);
25502 assert_eq!(editor.text(cx), text);
25503 assert_eq!(
25504 editor.selections.ranges(&editor.display_snapshot(cx)),
25505 ranges,
25506 "Assert selections are {}",
25507 marked_text
25508 );
25509}
25510
25511pub fn handle_signature_help_request(
25512 cx: &mut EditorLspTestContext,
25513 mocked_response: lsp::SignatureHelp,
25514) -> impl Future<Output = ()> + use<> {
25515 let mut request =
25516 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25517 let mocked_response = mocked_response.clone();
25518 async move { Ok(Some(mocked_response)) }
25519 });
25520
25521 async move {
25522 request.next().await;
25523 }
25524}
25525
25526#[track_caller]
25527pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25528 cx.update_editor(|editor, _, _| {
25529 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25530 let entries = menu.entries.borrow();
25531 let entries = entries
25532 .iter()
25533 .map(|entry| entry.string.as_str())
25534 .collect::<Vec<_>>();
25535 assert_eq!(entries, expected);
25536 } else {
25537 panic!("Expected completions menu");
25538 }
25539 });
25540}
25541
25542/// Handle completion request passing a marked string specifying where the completion
25543/// should be triggered from using '|' character, what range should be replaced, and what completions
25544/// should be returned using '<' and '>' to delimit the range.
25545///
25546/// Also see `handle_completion_request_with_insert_and_replace`.
25547#[track_caller]
25548pub fn handle_completion_request(
25549 marked_string: &str,
25550 completions: Vec<&'static str>,
25551 is_incomplete: bool,
25552 counter: Arc<AtomicUsize>,
25553 cx: &mut EditorLspTestContext,
25554) -> impl Future<Output = ()> {
25555 let complete_from_marker: TextRangeMarker = '|'.into();
25556 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25557 let (_, mut marked_ranges) = marked_text_ranges_by(
25558 marked_string,
25559 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25560 );
25561
25562 let complete_from_position =
25563 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25564 let replace_range =
25565 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25566
25567 let mut request =
25568 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25569 let completions = completions.clone();
25570 counter.fetch_add(1, atomic::Ordering::Release);
25571 async move {
25572 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25573 assert_eq!(
25574 params.text_document_position.position,
25575 complete_from_position
25576 );
25577 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25578 is_incomplete,
25579 item_defaults: None,
25580 items: completions
25581 .iter()
25582 .map(|completion_text| lsp::CompletionItem {
25583 label: completion_text.to_string(),
25584 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25585 range: replace_range,
25586 new_text: completion_text.to_string(),
25587 })),
25588 ..Default::default()
25589 })
25590 .collect(),
25591 })))
25592 }
25593 });
25594
25595 async move {
25596 request.next().await;
25597 }
25598}
25599
25600/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25601/// given instead, which also contains an `insert` range.
25602///
25603/// This function uses markers to define ranges:
25604/// - `|` marks the cursor position
25605/// - `<>` marks the replace range
25606/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25607pub fn handle_completion_request_with_insert_and_replace(
25608 cx: &mut EditorLspTestContext,
25609 marked_string: &str,
25610 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25611 counter: Arc<AtomicUsize>,
25612) -> impl Future<Output = ()> {
25613 let complete_from_marker: TextRangeMarker = '|'.into();
25614 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25615 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25616
25617 let (_, mut marked_ranges) = marked_text_ranges_by(
25618 marked_string,
25619 vec![
25620 complete_from_marker.clone(),
25621 replace_range_marker.clone(),
25622 insert_range_marker.clone(),
25623 ],
25624 );
25625
25626 let complete_from_position =
25627 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25628 let replace_range =
25629 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25630
25631 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25632 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25633 _ => lsp::Range {
25634 start: replace_range.start,
25635 end: complete_from_position,
25636 },
25637 };
25638
25639 let mut request =
25640 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25641 let completions = completions.clone();
25642 counter.fetch_add(1, atomic::Ordering::Release);
25643 async move {
25644 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25645 assert_eq!(
25646 params.text_document_position.position, complete_from_position,
25647 "marker `|` position doesn't match",
25648 );
25649 Ok(Some(lsp::CompletionResponse::Array(
25650 completions
25651 .iter()
25652 .map(|(label, new_text)| lsp::CompletionItem {
25653 label: label.to_string(),
25654 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25655 lsp::InsertReplaceEdit {
25656 insert: insert_range,
25657 replace: replace_range,
25658 new_text: new_text.to_string(),
25659 },
25660 )),
25661 ..Default::default()
25662 })
25663 .collect(),
25664 )))
25665 }
25666 });
25667
25668 async move {
25669 request.next().await;
25670 }
25671}
25672
25673fn handle_resolve_completion_request(
25674 cx: &mut EditorLspTestContext,
25675 edits: Option<Vec<(&'static str, &'static str)>>,
25676) -> impl Future<Output = ()> {
25677 let edits = edits.map(|edits| {
25678 edits
25679 .iter()
25680 .map(|(marked_string, new_text)| {
25681 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25682 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25683 lsp::TextEdit::new(replace_range, new_text.to_string())
25684 })
25685 .collect::<Vec<_>>()
25686 });
25687
25688 let mut request =
25689 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25690 let edits = edits.clone();
25691 async move {
25692 Ok(lsp::CompletionItem {
25693 additional_text_edits: edits,
25694 ..Default::default()
25695 })
25696 }
25697 });
25698
25699 async move {
25700 request.next().await;
25701 }
25702}
25703
25704pub(crate) fn update_test_language_settings(
25705 cx: &mut TestAppContext,
25706 f: impl Fn(&mut AllLanguageSettingsContent),
25707) {
25708 cx.update(|cx| {
25709 SettingsStore::update_global(cx, |store, cx| {
25710 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25711 });
25712 });
25713}
25714
25715pub(crate) fn update_test_project_settings(
25716 cx: &mut TestAppContext,
25717 f: impl Fn(&mut ProjectSettingsContent),
25718) {
25719 cx.update(|cx| {
25720 SettingsStore::update_global(cx, |store, cx| {
25721 store.update_user_settings(cx, |settings| f(&mut settings.project));
25722 });
25723 });
25724}
25725
25726pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25727 cx.update(|cx| {
25728 assets::Assets.load_test_fonts(cx);
25729 let store = SettingsStore::test(cx);
25730 cx.set_global(store);
25731 theme::init(theme::LoadThemes::JustBase, cx);
25732 release_channel::init(SemanticVersion::default(), cx);
25733 client::init_settings(cx);
25734 language::init(cx);
25735 Project::init_settings(cx);
25736 workspace::init_settings(cx);
25737 crate::init(cx);
25738 });
25739 zlog::init_test();
25740 update_test_language_settings(cx, f);
25741}
25742
25743#[track_caller]
25744fn assert_hunk_revert(
25745 not_reverted_text_with_selections: &str,
25746 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25747 expected_reverted_text_with_selections: &str,
25748 base_text: &str,
25749 cx: &mut EditorLspTestContext,
25750) {
25751 cx.set_state(not_reverted_text_with_selections);
25752 cx.set_head_text(base_text);
25753 cx.executor().run_until_parked();
25754
25755 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25756 let snapshot = editor.snapshot(window, cx);
25757 let reverted_hunk_statuses = snapshot
25758 .buffer_snapshot()
25759 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25760 .map(|hunk| hunk.status().kind)
25761 .collect::<Vec<_>>();
25762
25763 editor.git_restore(&Default::default(), window, cx);
25764 reverted_hunk_statuses
25765 });
25766 cx.executor().run_until_parked();
25767 cx.assert_editor_state(expected_reverted_text_with_selections);
25768 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25769}
25770
25771#[gpui::test(iterations = 10)]
25772async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25773 init_test(cx, |_| {});
25774
25775 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25776 let counter = diagnostic_requests.clone();
25777
25778 let fs = FakeFs::new(cx.executor());
25779 fs.insert_tree(
25780 path!("/a"),
25781 json!({
25782 "first.rs": "fn main() { let a = 5; }",
25783 "second.rs": "// Test file",
25784 }),
25785 )
25786 .await;
25787
25788 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25789 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25790 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25791
25792 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25793 language_registry.add(rust_lang());
25794 let mut fake_servers = language_registry.register_fake_lsp(
25795 "Rust",
25796 FakeLspAdapter {
25797 capabilities: lsp::ServerCapabilities {
25798 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25799 lsp::DiagnosticOptions {
25800 identifier: None,
25801 inter_file_dependencies: true,
25802 workspace_diagnostics: true,
25803 work_done_progress_options: Default::default(),
25804 },
25805 )),
25806 ..Default::default()
25807 },
25808 ..Default::default()
25809 },
25810 );
25811
25812 let editor = workspace
25813 .update(cx, |workspace, window, cx| {
25814 workspace.open_abs_path(
25815 PathBuf::from(path!("/a/first.rs")),
25816 OpenOptions::default(),
25817 window,
25818 cx,
25819 )
25820 })
25821 .unwrap()
25822 .await
25823 .unwrap()
25824 .downcast::<Editor>()
25825 .unwrap();
25826 let fake_server = fake_servers.next().await.unwrap();
25827 let server_id = fake_server.server.server_id();
25828 let mut first_request = fake_server
25829 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25830 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25831 let result_id = Some(new_result_id.to_string());
25832 assert_eq!(
25833 params.text_document.uri,
25834 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25835 );
25836 async move {
25837 Ok(lsp::DocumentDiagnosticReportResult::Report(
25838 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25839 related_documents: None,
25840 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25841 items: Vec::new(),
25842 result_id,
25843 },
25844 }),
25845 ))
25846 }
25847 });
25848
25849 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25850 project.update(cx, |project, cx| {
25851 let buffer_id = editor
25852 .read(cx)
25853 .buffer()
25854 .read(cx)
25855 .as_singleton()
25856 .expect("created a singleton buffer")
25857 .read(cx)
25858 .remote_id();
25859 let buffer_result_id = project
25860 .lsp_store()
25861 .read(cx)
25862 .result_id(server_id, buffer_id, cx);
25863 assert_eq!(expected, buffer_result_id);
25864 });
25865 };
25866
25867 ensure_result_id(None, cx);
25868 cx.executor().advance_clock(Duration::from_millis(60));
25869 cx.executor().run_until_parked();
25870 assert_eq!(
25871 diagnostic_requests.load(atomic::Ordering::Acquire),
25872 1,
25873 "Opening file should trigger diagnostic request"
25874 );
25875 first_request
25876 .next()
25877 .await
25878 .expect("should have sent the first diagnostics pull request");
25879 ensure_result_id(Some("1".to_string()), cx);
25880
25881 // Editing should trigger diagnostics
25882 editor.update_in(cx, |editor, window, cx| {
25883 editor.handle_input("2", window, cx)
25884 });
25885 cx.executor().advance_clock(Duration::from_millis(60));
25886 cx.executor().run_until_parked();
25887 assert_eq!(
25888 diagnostic_requests.load(atomic::Ordering::Acquire),
25889 2,
25890 "Editing should trigger diagnostic request"
25891 );
25892 ensure_result_id(Some("2".to_string()), cx);
25893
25894 // Moving cursor should not trigger diagnostic request
25895 editor.update_in(cx, |editor, window, cx| {
25896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25897 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25898 });
25899 });
25900 cx.executor().advance_clock(Duration::from_millis(60));
25901 cx.executor().run_until_parked();
25902 assert_eq!(
25903 diagnostic_requests.load(atomic::Ordering::Acquire),
25904 2,
25905 "Cursor movement should not trigger diagnostic request"
25906 );
25907 ensure_result_id(Some("2".to_string()), cx);
25908 // Multiple rapid edits should be debounced
25909 for _ in 0..5 {
25910 editor.update_in(cx, |editor, window, cx| {
25911 editor.handle_input("x", window, cx)
25912 });
25913 }
25914 cx.executor().advance_clock(Duration::from_millis(60));
25915 cx.executor().run_until_parked();
25916
25917 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25918 assert!(
25919 final_requests <= 4,
25920 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25921 );
25922 ensure_result_id(Some(final_requests.to_string()), cx);
25923}
25924
25925#[gpui::test]
25926async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25927 // Regression test for issue #11671
25928 // Previously, adding a cursor after moving multiple cursors would reset
25929 // the cursor count instead of adding to the existing cursors.
25930 init_test(cx, |_| {});
25931 let mut cx = EditorTestContext::new(cx).await;
25932
25933 // Create a simple buffer with cursor at start
25934 cx.set_state(indoc! {"
25935 ˇaaaa
25936 bbbb
25937 cccc
25938 dddd
25939 eeee
25940 ffff
25941 gggg
25942 hhhh"});
25943
25944 // Add 2 cursors below (so we have 3 total)
25945 cx.update_editor(|editor, window, cx| {
25946 editor.add_selection_below(&Default::default(), window, cx);
25947 editor.add_selection_below(&Default::default(), window, cx);
25948 });
25949
25950 // Verify we have 3 cursors
25951 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25952 assert_eq!(
25953 initial_count, 3,
25954 "Should have 3 cursors after adding 2 below"
25955 );
25956
25957 // Move down one line
25958 cx.update_editor(|editor, window, cx| {
25959 editor.move_down(&MoveDown, window, cx);
25960 });
25961
25962 // Add another cursor below
25963 cx.update_editor(|editor, window, cx| {
25964 editor.add_selection_below(&Default::default(), window, cx);
25965 });
25966
25967 // Should now have 4 cursors (3 original + 1 new)
25968 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25969 assert_eq!(
25970 final_count, 4,
25971 "Should have 4 cursors after moving and adding another"
25972 );
25973}
25974
25975#[gpui::test]
25976async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25977 init_test(cx, |_| {});
25978
25979 let mut cx = EditorTestContext::new(cx).await;
25980
25981 cx.set_state(indoc!(
25982 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25983 Second line here"#
25984 ));
25985
25986 cx.update_editor(|editor, window, cx| {
25987 // Enable soft wrapping with a narrow width to force soft wrapping and
25988 // confirm that more than 2 rows are being displayed.
25989 editor.set_wrap_width(Some(100.0.into()), cx);
25990 assert!(editor.display_text(cx).lines().count() > 2);
25991
25992 editor.add_selection_below(
25993 &AddSelectionBelow {
25994 skip_soft_wrap: true,
25995 },
25996 window,
25997 cx,
25998 );
25999
26000 assert_eq!(
26001 editor.selections.display_ranges(cx),
26002 &[
26003 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26004 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26005 ]
26006 );
26007
26008 editor.add_selection_above(
26009 &AddSelectionAbove {
26010 skip_soft_wrap: true,
26011 },
26012 window,
26013 cx,
26014 );
26015
26016 assert_eq!(
26017 editor.selections.display_ranges(cx),
26018 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26019 );
26020
26021 editor.add_selection_below(
26022 &AddSelectionBelow {
26023 skip_soft_wrap: false,
26024 },
26025 window,
26026 cx,
26027 );
26028
26029 assert_eq!(
26030 editor.selections.display_ranges(cx),
26031 &[
26032 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26033 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26034 ]
26035 );
26036
26037 editor.add_selection_above(
26038 &AddSelectionAbove {
26039 skip_soft_wrap: false,
26040 },
26041 window,
26042 cx,
26043 );
26044
26045 assert_eq!(
26046 editor.selections.display_ranges(cx),
26047 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26048 );
26049 });
26050}
26051
26052#[gpui::test(iterations = 10)]
26053async fn test_document_colors(cx: &mut TestAppContext) {
26054 let expected_color = Rgba {
26055 r: 0.33,
26056 g: 0.33,
26057 b: 0.33,
26058 a: 0.33,
26059 };
26060
26061 init_test(cx, |_| {});
26062
26063 let fs = FakeFs::new(cx.executor());
26064 fs.insert_tree(
26065 path!("/a"),
26066 json!({
26067 "first.rs": "fn main() { let a = 5; }",
26068 }),
26069 )
26070 .await;
26071
26072 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26073 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26074 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26075
26076 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26077 language_registry.add(rust_lang());
26078 let mut fake_servers = language_registry.register_fake_lsp(
26079 "Rust",
26080 FakeLspAdapter {
26081 capabilities: lsp::ServerCapabilities {
26082 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26083 ..lsp::ServerCapabilities::default()
26084 },
26085 name: "rust-analyzer",
26086 ..FakeLspAdapter::default()
26087 },
26088 );
26089 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26090 "Rust",
26091 FakeLspAdapter {
26092 capabilities: lsp::ServerCapabilities {
26093 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26094 ..lsp::ServerCapabilities::default()
26095 },
26096 name: "not-rust-analyzer",
26097 ..FakeLspAdapter::default()
26098 },
26099 );
26100
26101 let editor = workspace
26102 .update(cx, |workspace, window, cx| {
26103 workspace.open_abs_path(
26104 PathBuf::from(path!("/a/first.rs")),
26105 OpenOptions::default(),
26106 window,
26107 cx,
26108 )
26109 })
26110 .unwrap()
26111 .await
26112 .unwrap()
26113 .downcast::<Editor>()
26114 .unwrap();
26115 let fake_language_server = fake_servers.next().await.unwrap();
26116 let fake_language_server_without_capabilities =
26117 fake_servers_without_capabilities.next().await.unwrap();
26118 let requests_made = Arc::new(AtomicUsize::new(0));
26119 let closure_requests_made = Arc::clone(&requests_made);
26120 let mut color_request_handle = fake_language_server
26121 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26122 let requests_made = Arc::clone(&closure_requests_made);
26123 async move {
26124 assert_eq!(
26125 params.text_document.uri,
26126 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26127 );
26128 requests_made.fetch_add(1, atomic::Ordering::Release);
26129 Ok(vec![
26130 lsp::ColorInformation {
26131 range: lsp::Range {
26132 start: lsp::Position {
26133 line: 0,
26134 character: 0,
26135 },
26136 end: lsp::Position {
26137 line: 0,
26138 character: 1,
26139 },
26140 },
26141 color: lsp::Color {
26142 red: 0.33,
26143 green: 0.33,
26144 blue: 0.33,
26145 alpha: 0.33,
26146 },
26147 },
26148 lsp::ColorInformation {
26149 range: lsp::Range {
26150 start: lsp::Position {
26151 line: 0,
26152 character: 0,
26153 },
26154 end: lsp::Position {
26155 line: 0,
26156 character: 1,
26157 },
26158 },
26159 color: lsp::Color {
26160 red: 0.33,
26161 green: 0.33,
26162 blue: 0.33,
26163 alpha: 0.33,
26164 },
26165 },
26166 ])
26167 }
26168 });
26169
26170 let _handle = fake_language_server_without_capabilities
26171 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26172 panic!("Should not be called");
26173 });
26174 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26175 color_request_handle.next().await.unwrap();
26176 cx.run_until_parked();
26177 assert_eq!(
26178 1,
26179 requests_made.load(atomic::Ordering::Acquire),
26180 "Should query for colors once per editor open"
26181 );
26182 editor.update_in(cx, |editor, _, cx| {
26183 assert_eq!(
26184 vec![expected_color],
26185 extract_color_inlays(editor, cx),
26186 "Should have an initial inlay"
26187 );
26188 });
26189
26190 // opening another file in a split should not influence the LSP query counter
26191 workspace
26192 .update(cx, |workspace, window, cx| {
26193 assert_eq!(
26194 workspace.panes().len(),
26195 1,
26196 "Should have one pane with one editor"
26197 );
26198 workspace.move_item_to_pane_in_direction(
26199 &MoveItemToPaneInDirection {
26200 direction: SplitDirection::Right,
26201 focus: false,
26202 clone: true,
26203 },
26204 window,
26205 cx,
26206 );
26207 })
26208 .unwrap();
26209 cx.run_until_parked();
26210 workspace
26211 .update(cx, |workspace, _, cx| {
26212 let panes = workspace.panes();
26213 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26214 for pane in panes {
26215 let editor = pane
26216 .read(cx)
26217 .active_item()
26218 .and_then(|item| item.downcast::<Editor>())
26219 .expect("Should have opened an editor in each split");
26220 let editor_file = editor
26221 .read(cx)
26222 .buffer()
26223 .read(cx)
26224 .as_singleton()
26225 .expect("test deals with singleton buffers")
26226 .read(cx)
26227 .file()
26228 .expect("test buffese should have a file")
26229 .path();
26230 assert_eq!(
26231 editor_file.as_ref(),
26232 rel_path("first.rs"),
26233 "Both editors should be opened for the same file"
26234 )
26235 }
26236 })
26237 .unwrap();
26238
26239 cx.executor().advance_clock(Duration::from_millis(500));
26240 let save = editor.update_in(cx, |editor, window, cx| {
26241 editor.move_to_end(&MoveToEnd, window, cx);
26242 editor.handle_input("dirty", window, cx);
26243 editor.save(
26244 SaveOptions {
26245 format: true,
26246 autosave: true,
26247 },
26248 project.clone(),
26249 window,
26250 cx,
26251 )
26252 });
26253 save.await.unwrap();
26254
26255 color_request_handle.next().await.unwrap();
26256 cx.run_until_parked();
26257 assert_eq!(
26258 2,
26259 requests_made.load(atomic::Ordering::Acquire),
26260 "Should query for colors once per save (deduplicated) and once per formatting after save"
26261 );
26262
26263 drop(editor);
26264 let close = workspace
26265 .update(cx, |workspace, window, cx| {
26266 workspace.active_pane().update(cx, |pane, cx| {
26267 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26268 })
26269 })
26270 .unwrap();
26271 close.await.unwrap();
26272 let close = workspace
26273 .update(cx, |workspace, window, cx| {
26274 workspace.active_pane().update(cx, |pane, cx| {
26275 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26276 })
26277 })
26278 .unwrap();
26279 close.await.unwrap();
26280 assert_eq!(
26281 2,
26282 requests_made.load(atomic::Ordering::Acquire),
26283 "After saving and closing all editors, no extra requests should be made"
26284 );
26285 workspace
26286 .update(cx, |workspace, _, cx| {
26287 assert!(
26288 workspace.active_item(cx).is_none(),
26289 "Should close all editors"
26290 )
26291 })
26292 .unwrap();
26293
26294 workspace
26295 .update(cx, |workspace, window, cx| {
26296 workspace.active_pane().update(cx, |pane, cx| {
26297 pane.navigate_backward(&workspace::GoBack, window, cx);
26298 })
26299 })
26300 .unwrap();
26301 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26302 cx.run_until_parked();
26303 let editor = workspace
26304 .update(cx, |workspace, _, cx| {
26305 workspace
26306 .active_item(cx)
26307 .expect("Should have reopened the editor again after navigating back")
26308 .downcast::<Editor>()
26309 .expect("Should be an editor")
26310 })
26311 .unwrap();
26312
26313 assert_eq!(
26314 2,
26315 requests_made.load(atomic::Ordering::Acquire),
26316 "Cache should be reused on buffer close and reopen"
26317 );
26318 editor.update(cx, |editor, cx| {
26319 assert_eq!(
26320 vec![expected_color],
26321 extract_color_inlays(editor, cx),
26322 "Should have an initial inlay"
26323 );
26324 });
26325
26326 drop(color_request_handle);
26327 let closure_requests_made = Arc::clone(&requests_made);
26328 let mut empty_color_request_handle = fake_language_server
26329 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26330 let requests_made = Arc::clone(&closure_requests_made);
26331 async move {
26332 assert_eq!(
26333 params.text_document.uri,
26334 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26335 );
26336 requests_made.fetch_add(1, atomic::Ordering::Release);
26337 Ok(Vec::new())
26338 }
26339 });
26340 let save = editor.update_in(cx, |editor, window, cx| {
26341 editor.move_to_end(&MoveToEnd, window, cx);
26342 editor.handle_input("dirty_again", window, cx);
26343 editor.save(
26344 SaveOptions {
26345 format: false,
26346 autosave: true,
26347 },
26348 project.clone(),
26349 window,
26350 cx,
26351 )
26352 });
26353 save.await.unwrap();
26354
26355 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26356 empty_color_request_handle.next().await.unwrap();
26357 cx.run_until_parked();
26358 assert_eq!(
26359 3,
26360 requests_made.load(atomic::Ordering::Acquire),
26361 "Should query for colors once per save only, as formatting was not requested"
26362 );
26363 editor.update(cx, |editor, cx| {
26364 assert_eq!(
26365 Vec::<Rgba>::new(),
26366 extract_color_inlays(editor, cx),
26367 "Should clear all colors when the server returns an empty response"
26368 );
26369 });
26370}
26371
26372#[gpui::test]
26373async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26374 init_test(cx, |_| {});
26375 let (editor, cx) = cx.add_window_view(Editor::single_line);
26376 editor.update_in(cx, |editor, window, cx| {
26377 editor.set_text("oops\n\nwow\n", window, cx)
26378 });
26379 cx.run_until_parked();
26380 editor.update(cx, |editor, cx| {
26381 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26382 });
26383 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26384 cx.run_until_parked();
26385 editor.update(cx, |editor, cx| {
26386 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26387 });
26388}
26389
26390#[gpui::test]
26391async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26392 init_test(cx, |_| {});
26393
26394 cx.update(|cx| {
26395 register_project_item::<Editor>(cx);
26396 });
26397
26398 let fs = FakeFs::new(cx.executor());
26399 fs.insert_tree("/root1", json!({})).await;
26400 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26401 .await;
26402
26403 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26404 let (workspace, cx) =
26405 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26406
26407 let worktree_id = project.update(cx, |project, cx| {
26408 project.worktrees(cx).next().unwrap().read(cx).id()
26409 });
26410
26411 let handle = workspace
26412 .update_in(cx, |workspace, window, cx| {
26413 let project_path = (worktree_id, rel_path("one.pdf"));
26414 workspace.open_path(project_path, None, true, window, cx)
26415 })
26416 .await
26417 .unwrap();
26418
26419 assert_eq!(
26420 handle.to_any().entity_type(),
26421 TypeId::of::<InvalidItemView>()
26422 );
26423}
26424
26425#[gpui::test]
26426async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26427 init_test(cx, |_| {});
26428
26429 let language = Arc::new(Language::new(
26430 LanguageConfig::default(),
26431 Some(tree_sitter_rust::LANGUAGE.into()),
26432 ));
26433
26434 // Test hierarchical sibling navigation
26435 let text = r#"
26436 fn outer() {
26437 if condition {
26438 let a = 1;
26439 }
26440 let b = 2;
26441 }
26442
26443 fn another() {
26444 let c = 3;
26445 }
26446 "#;
26447
26448 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26449 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26450 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26451
26452 // Wait for parsing to complete
26453 editor
26454 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26455 .await;
26456
26457 editor.update_in(cx, |editor, window, cx| {
26458 // Start by selecting "let a = 1;" inside the if block
26459 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26460 s.select_display_ranges([
26461 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26462 ]);
26463 });
26464
26465 let initial_selection = editor.selections.display_ranges(cx);
26466 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26467
26468 // Test select next sibling - should move up levels to find the next sibling
26469 // Since "let a = 1;" has no siblings in the if block, it should move up
26470 // to find "let b = 2;" which is a sibling of the if block
26471 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26472 let next_selection = editor.selections.display_ranges(cx);
26473
26474 // Should have a selection and it should be different from the initial
26475 assert_eq!(
26476 next_selection.len(),
26477 1,
26478 "Should have one selection after next"
26479 );
26480 assert_ne!(
26481 next_selection[0], initial_selection[0],
26482 "Next sibling selection should be different"
26483 );
26484
26485 // Test hierarchical navigation by going to the end of the current function
26486 // and trying to navigate to the next function
26487 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26488 s.select_display_ranges([
26489 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26490 ]);
26491 });
26492
26493 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26494 let function_next_selection = editor.selections.display_ranges(cx);
26495
26496 // Should move to the next function
26497 assert_eq!(
26498 function_next_selection.len(),
26499 1,
26500 "Should have one selection after function next"
26501 );
26502
26503 // Test select previous sibling navigation
26504 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26505 let prev_selection = editor.selections.display_ranges(cx);
26506
26507 // Should have a selection and it should be different
26508 assert_eq!(
26509 prev_selection.len(),
26510 1,
26511 "Should have one selection after prev"
26512 );
26513 assert_ne!(
26514 prev_selection[0], function_next_selection[0],
26515 "Previous sibling selection should be different from next"
26516 );
26517 });
26518}
26519
26520#[gpui::test]
26521async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26522 init_test(cx, |_| {});
26523
26524 let mut cx = EditorTestContext::new(cx).await;
26525 cx.set_state(
26526 "let ˇvariable = 42;
26527let another = variable + 1;
26528let result = variable * 2;",
26529 );
26530
26531 // Set up document highlights manually (simulating LSP response)
26532 cx.update_editor(|editor, _window, cx| {
26533 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26534
26535 // Create highlights for "variable" occurrences
26536 let highlight_ranges = [
26537 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26538 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26539 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26540 ];
26541
26542 let anchor_ranges: Vec<_> = highlight_ranges
26543 .iter()
26544 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26545 .collect();
26546
26547 editor.highlight_background::<DocumentHighlightRead>(
26548 &anchor_ranges,
26549 |theme| theme.colors().editor_document_highlight_read_background,
26550 cx,
26551 );
26552 });
26553
26554 // Go to next highlight - should move to second "variable"
26555 cx.update_editor(|editor, window, cx| {
26556 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26557 });
26558 cx.assert_editor_state(
26559 "let variable = 42;
26560let another = ˇvariable + 1;
26561let result = variable * 2;",
26562 );
26563
26564 // Go to next highlight - should move to third "variable"
26565 cx.update_editor(|editor, window, cx| {
26566 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26567 });
26568 cx.assert_editor_state(
26569 "let variable = 42;
26570let another = variable + 1;
26571let result = ˇvariable * 2;",
26572 );
26573
26574 // Go to next highlight - should stay at third "variable" (no wrap-around)
26575 cx.update_editor(|editor, window, cx| {
26576 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26577 });
26578 cx.assert_editor_state(
26579 "let variable = 42;
26580let another = variable + 1;
26581let result = ˇvariable * 2;",
26582 );
26583
26584 // Now test going backwards from third position
26585 cx.update_editor(|editor, window, cx| {
26586 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26587 });
26588 cx.assert_editor_state(
26589 "let variable = 42;
26590let another = ˇvariable + 1;
26591let result = variable * 2;",
26592 );
26593
26594 // Go to previous highlight - should move to first "variable"
26595 cx.update_editor(|editor, window, cx| {
26596 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26597 });
26598 cx.assert_editor_state(
26599 "let ˇvariable = 42;
26600let another = variable + 1;
26601let result = variable * 2;",
26602 );
26603
26604 // Go to previous highlight - should stay on first "variable"
26605 cx.update_editor(|editor, window, cx| {
26606 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26607 });
26608 cx.assert_editor_state(
26609 "let ˇvariable = 42;
26610let another = variable + 1;
26611let result = variable * 2;",
26612 );
26613}
26614
26615#[gpui::test]
26616async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26617 cx: &mut gpui::TestAppContext,
26618) {
26619 init_test(cx, |_| {});
26620
26621 let url = "https://zed.dev";
26622
26623 let markdown_language = Arc::new(Language::new(
26624 LanguageConfig {
26625 name: "Markdown".into(),
26626 ..LanguageConfig::default()
26627 },
26628 None,
26629 ));
26630
26631 let mut cx = EditorTestContext::new(cx).await;
26632 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26633 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26634
26635 cx.update_editor(|editor, window, cx| {
26636 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26637 editor.paste(&Paste, window, cx);
26638 });
26639
26640 cx.assert_editor_state(&format!(
26641 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26642 ));
26643}
26644
26645#[gpui::test]
26646async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26647 cx: &mut gpui::TestAppContext,
26648) {
26649 init_test(cx, |_| {});
26650
26651 let url = "https://zed.dev";
26652
26653 let markdown_language = Arc::new(Language::new(
26654 LanguageConfig {
26655 name: "Markdown".into(),
26656 ..LanguageConfig::default()
26657 },
26658 None,
26659 ));
26660
26661 let mut cx = EditorTestContext::new(cx).await;
26662 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26663 cx.set_state(&format!(
26664 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26665 ));
26666
26667 cx.update_editor(|editor, window, cx| {
26668 editor.copy(&Copy, window, cx);
26669 });
26670
26671 cx.set_state(&format!(
26672 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26673 ));
26674
26675 cx.update_editor(|editor, window, cx| {
26676 editor.paste(&Paste, window, cx);
26677 });
26678
26679 cx.assert_editor_state(&format!(
26680 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26681 ));
26682}
26683
26684#[gpui::test]
26685async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26686 cx: &mut gpui::TestAppContext,
26687) {
26688 init_test(cx, |_| {});
26689
26690 let url = "https://zed.dev";
26691
26692 let markdown_language = Arc::new(Language::new(
26693 LanguageConfig {
26694 name: "Markdown".into(),
26695 ..LanguageConfig::default()
26696 },
26697 None,
26698 ));
26699
26700 let mut cx = EditorTestContext::new(cx).await;
26701 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26702 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26703
26704 cx.update_editor(|editor, window, cx| {
26705 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26706 editor.paste(&Paste, window, cx);
26707 });
26708
26709 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26710}
26711
26712#[gpui::test]
26713async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26714 cx: &mut gpui::TestAppContext,
26715) {
26716 init_test(cx, |_| {});
26717
26718 let text = "Awesome";
26719
26720 let markdown_language = Arc::new(Language::new(
26721 LanguageConfig {
26722 name: "Markdown".into(),
26723 ..LanguageConfig::default()
26724 },
26725 None,
26726 ));
26727
26728 let mut cx = EditorTestContext::new(cx).await;
26729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26730 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26731
26732 cx.update_editor(|editor, window, cx| {
26733 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26734 editor.paste(&Paste, window, cx);
26735 });
26736
26737 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26738}
26739
26740#[gpui::test]
26741async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26742 cx: &mut gpui::TestAppContext,
26743) {
26744 init_test(cx, |_| {});
26745
26746 let url = "https://zed.dev";
26747
26748 let markdown_language = Arc::new(Language::new(
26749 LanguageConfig {
26750 name: "Rust".into(),
26751 ..LanguageConfig::default()
26752 },
26753 None,
26754 ));
26755
26756 let mut cx = EditorTestContext::new(cx).await;
26757 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26758 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26759
26760 cx.update_editor(|editor, window, cx| {
26761 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26762 editor.paste(&Paste, window, cx);
26763 });
26764
26765 cx.assert_editor_state(&format!(
26766 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26767 ));
26768}
26769
26770#[gpui::test]
26771async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26772 cx: &mut TestAppContext,
26773) {
26774 init_test(cx, |_| {});
26775
26776 let url = "https://zed.dev";
26777
26778 let markdown_language = Arc::new(Language::new(
26779 LanguageConfig {
26780 name: "Markdown".into(),
26781 ..LanguageConfig::default()
26782 },
26783 None,
26784 ));
26785
26786 let (editor, cx) = cx.add_window_view(|window, cx| {
26787 let multi_buffer = MultiBuffer::build_multi(
26788 [
26789 ("this will embed -> link", vec![Point::row_range(0..1)]),
26790 ("this will replace -> link", vec![Point::row_range(0..1)]),
26791 ],
26792 cx,
26793 );
26794 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26796 s.select_ranges(vec![
26797 Point::new(0, 19)..Point::new(0, 23),
26798 Point::new(1, 21)..Point::new(1, 25),
26799 ])
26800 });
26801 let first_buffer_id = multi_buffer
26802 .read(cx)
26803 .excerpt_buffer_ids()
26804 .into_iter()
26805 .next()
26806 .unwrap();
26807 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26808 first_buffer.update(cx, |buffer, cx| {
26809 buffer.set_language(Some(markdown_language.clone()), cx);
26810 });
26811
26812 editor
26813 });
26814 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26815
26816 cx.update_editor(|editor, window, cx| {
26817 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26818 editor.paste(&Paste, window, cx);
26819 });
26820
26821 cx.assert_editor_state(&format!(
26822 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26823 ));
26824}
26825
26826#[gpui::test]
26827async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26828 init_test(cx, |_| {});
26829
26830 let fs = FakeFs::new(cx.executor());
26831 fs.insert_tree(
26832 path!("/project"),
26833 json!({
26834 "first.rs": "# First Document\nSome content here.",
26835 "second.rs": "Plain text content for second file.",
26836 }),
26837 )
26838 .await;
26839
26840 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26841 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26842 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26843
26844 let language = rust_lang();
26845 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26846 language_registry.add(language.clone());
26847 let mut fake_servers = language_registry.register_fake_lsp(
26848 "Rust",
26849 FakeLspAdapter {
26850 ..FakeLspAdapter::default()
26851 },
26852 );
26853
26854 let buffer1 = project
26855 .update(cx, |project, cx| {
26856 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26857 })
26858 .await
26859 .unwrap();
26860 let buffer2 = project
26861 .update(cx, |project, cx| {
26862 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26863 })
26864 .await
26865 .unwrap();
26866
26867 let multi_buffer = cx.new(|cx| {
26868 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26869 multi_buffer.set_excerpts_for_path(
26870 PathKey::for_buffer(&buffer1, cx),
26871 buffer1.clone(),
26872 [Point::zero()..buffer1.read(cx).max_point()],
26873 3,
26874 cx,
26875 );
26876 multi_buffer.set_excerpts_for_path(
26877 PathKey::for_buffer(&buffer2, cx),
26878 buffer2.clone(),
26879 [Point::zero()..buffer1.read(cx).max_point()],
26880 3,
26881 cx,
26882 );
26883 multi_buffer
26884 });
26885
26886 let (editor, cx) = cx.add_window_view(|window, cx| {
26887 Editor::new(
26888 EditorMode::full(),
26889 multi_buffer,
26890 Some(project.clone()),
26891 window,
26892 cx,
26893 )
26894 });
26895
26896 let fake_language_server = fake_servers.next().await.unwrap();
26897
26898 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26899
26900 let save = editor.update_in(cx, |editor, window, cx| {
26901 assert!(editor.is_dirty(cx));
26902
26903 editor.save(
26904 SaveOptions {
26905 format: true,
26906 autosave: true,
26907 },
26908 project,
26909 window,
26910 cx,
26911 )
26912 });
26913 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26914 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26915 let mut done_edit_rx = Some(done_edit_rx);
26916 let mut start_edit_tx = Some(start_edit_tx);
26917
26918 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26919 start_edit_tx.take().unwrap().send(()).unwrap();
26920 let done_edit_rx = done_edit_rx.take().unwrap();
26921 async move {
26922 done_edit_rx.await.unwrap();
26923 Ok(None)
26924 }
26925 });
26926
26927 start_edit_rx.await.unwrap();
26928 buffer2
26929 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26930 .unwrap();
26931
26932 done_edit_tx.send(()).unwrap();
26933
26934 save.await.unwrap();
26935 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26936}
26937
26938#[track_caller]
26939fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26940 editor
26941 .all_inlays(cx)
26942 .into_iter()
26943 .filter_map(|inlay| inlay.get_color())
26944 .map(Rgba::from)
26945 .collect()
26946}
26947
26948#[gpui::test]
26949fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26950 init_test(cx, |_| {});
26951
26952 let editor = cx.add_window(|window, cx| {
26953 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26954 build_editor(buffer, window, cx)
26955 });
26956
26957 editor
26958 .update(cx, |editor, window, cx| {
26959 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26960 s.select_display_ranges([
26961 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26962 ])
26963 });
26964
26965 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26966
26967 assert_eq!(
26968 editor.display_text(cx),
26969 "line1\nline2\nline2",
26970 "Duplicating last line upward should create duplicate above, not on same line"
26971 );
26972
26973 assert_eq!(
26974 editor.selections.display_ranges(cx),
26975 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26976 "Selection should remain on the original line"
26977 );
26978 })
26979 .unwrap();
26980}
26981
26982#[gpui::test]
26983async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26984 init_test(cx, |_| {});
26985
26986 let mut cx = EditorTestContext::new(cx).await;
26987
26988 cx.set_state("line1\nline2ˇ");
26989
26990 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26991
26992 let clipboard_text = cx
26993 .read_from_clipboard()
26994 .and_then(|item| item.text().as_deref().map(str::to_string));
26995
26996 assert_eq!(
26997 clipboard_text,
26998 Some("line2\n".to_string()),
26999 "Copying a line without trailing newline should include a newline"
27000 );
27001
27002 cx.set_state("line1\nˇ");
27003
27004 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27005
27006 cx.assert_editor_state("line1\nline2\nˇ");
27007}
27008
27009#[gpui::test]
27010async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27011 init_test(cx, |_| {});
27012
27013 let mut cx = EditorTestContext::new(cx).await;
27014
27015 cx.set_state("line1\nline2ˇ");
27016 cx.update_editor(|e, window, cx| {
27017 e.set_mode(EditorMode::SingleLine);
27018 assert!(e.key_context(window, cx).contains("end_of_input"));
27019 });
27020 cx.set_state("ˇline1\nline2");
27021 cx.update_editor(|e, window, cx| {
27022 assert!(!e.key_context(window, cx).contains("end_of_input"));
27023 });
27024 cx.set_state("line1ˇ\nline2");
27025 cx.update_editor(|e, window, cx| {
27026 assert!(!e.key_context(window, cx).contains("end_of_input"));
27027 });
27028}