1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use languages::rust_lang;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::build_editor_with_project;
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_item_view::InvalidItemView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(
224 editor.selections.ranges(&editor.display_snapshot(cx)),
225 vec![4..4]
226 );
227
228 editor.start_transaction_at(now, window, cx);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([4..5])
231 });
232 editor.insert("e", window, cx);
233 editor.end_transaction_at(now, cx);
234 assert_eq!(editor.text(cx), "12cde6");
235 assert_eq!(
236 editor.selections.ranges(&editor.display_snapshot(cx)),
237 vec![5..5]
238 );
239
240 now += group_interval + Duration::from_millis(1);
241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
242 s.select_ranges([2..2])
243 });
244
245 // Simulate an edit in another editor
246 buffer.update(cx, |buffer, cx| {
247 buffer.start_transaction_at(now, cx);
248 buffer.edit([(0..1, "a")], None, cx);
249 buffer.edit([(1..1, "b")], None, cx);
250 buffer.end_transaction_at(now, cx);
251 });
252
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(
255 editor.selections.ranges(&editor.display_snapshot(cx)),
256 vec![3..3]
257 );
258
259 // Last transaction happened past the group interval in a different editor.
260 // Undo it individually and don't restore selections.
261 editor.undo(&Undo, window, cx);
262 assert_eq!(editor.text(cx), "12cde6");
263 assert_eq!(
264 editor.selections.ranges(&editor.display_snapshot(cx)),
265 vec![2..2]
266 );
267
268 // First two transactions happened within the group interval in this editor.
269 // Undo them together and restore selections.
270 editor.undo(&Undo, window, cx);
271 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
272 assert_eq!(editor.text(cx), "123456");
273 assert_eq!(
274 editor.selections.ranges(&editor.display_snapshot(cx)),
275 vec![0..0]
276 );
277
278 // Redo the first two transactions together.
279 editor.redo(&Redo, window, cx);
280 assert_eq!(editor.text(cx), "12cde6");
281 assert_eq!(
282 editor.selections.ranges(&editor.display_snapshot(cx)),
283 vec![5..5]
284 );
285
286 // Redo the last transaction on its own.
287 editor.redo(&Redo, window, cx);
288 assert_eq!(editor.text(cx), "ab2cde6");
289 assert_eq!(
290 editor.selections.ranges(&editor.display_snapshot(cx)),
291 vec![6..6]
292 );
293
294 // Test empty transactions.
295 editor.start_transaction_at(now, window, cx);
296 editor.end_transaction_at(now, cx);
297 editor.undo(&Undo, window, cx);
298 assert_eq!(editor.text(cx), "12cde6");
299 });
300}
301
302#[gpui::test]
303fn test_ime_composition(cx: &mut TestAppContext) {
304 init_test(cx, |_| {});
305
306 let buffer = cx.new(|cx| {
307 let mut buffer = language::Buffer::local("abcde", cx);
308 // Ensure automatic grouping doesn't occur.
309 buffer.set_group_interval(Duration::ZERO);
310 buffer
311 });
312
313 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
314 cx.add_window(|window, cx| {
315 let mut editor = build_editor(buffer.clone(), window, cx);
316
317 // Start a new IME composition.
318 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
319 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
320 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
321 assert_eq!(editor.text(cx), "äbcde");
322 assert_eq!(
323 editor.marked_text_ranges(cx),
324 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
325 );
326
327 // Finalize IME composition.
328 editor.replace_text_in_range(None, "ā", window, cx);
329 assert_eq!(editor.text(cx), "ābcde");
330 assert_eq!(editor.marked_text_ranges(cx), None);
331
332 // IME composition edits are grouped and are undone/redone at once.
333 editor.undo(&Default::default(), window, cx);
334 assert_eq!(editor.text(cx), "abcde");
335 assert_eq!(editor.marked_text_ranges(cx), None);
336 editor.redo(&Default::default(), window, cx);
337 assert_eq!(editor.text(cx), "ābcde");
338 assert_eq!(editor.marked_text_ranges(cx), None);
339
340 // Start a new IME composition.
341 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
342 assert_eq!(
343 editor.marked_text_ranges(cx),
344 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
345 );
346
347 // Undoing during an IME composition cancels it.
348 editor.undo(&Default::default(), window, cx);
349 assert_eq!(editor.text(cx), "ābcde");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
353 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
354 assert_eq!(editor.text(cx), "ābcdè");
355 assert_eq!(
356 editor.marked_text_ranges(cx),
357 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
358 );
359
360 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
361 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
362 assert_eq!(editor.text(cx), "ābcdę");
363 assert_eq!(editor.marked_text_ranges(cx), None);
364
365 // Start a new IME composition with multiple cursors.
366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
367 s.select_ranges([
368 OffsetUtf16(1)..OffsetUtf16(1),
369 OffsetUtf16(3)..OffsetUtf16(3),
370 OffsetUtf16(5)..OffsetUtf16(5),
371 ])
372 });
373 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
374 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
375 assert_eq!(
376 editor.marked_text_ranges(cx),
377 Some(vec![
378 OffsetUtf16(0)..OffsetUtf16(3),
379 OffsetUtf16(4)..OffsetUtf16(7),
380 OffsetUtf16(8)..OffsetUtf16(11)
381 ])
382 );
383
384 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
385 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
386 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
387 assert_eq!(
388 editor.marked_text_ranges(cx),
389 Some(vec![
390 OffsetUtf16(1)..OffsetUtf16(2),
391 OffsetUtf16(5)..OffsetUtf16(6),
392 OffsetUtf16(9)..OffsetUtf16(10)
393 ])
394 );
395
396 // Finalize IME composition with multiple cursors.
397 editor.replace_text_in_range(Some(9..10), "2", window, cx);
398 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
399 assert_eq!(editor.marked_text_ranges(cx), None);
400
401 editor
402 });
403}
404
405#[gpui::test]
406fn test_selection_with_mouse(cx: &mut TestAppContext) {
407 init_test(cx, |_| {});
408
409 let editor = cx.add_window(|window, cx| {
410 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
411 build_editor(buffer, window, cx)
412 });
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
416 });
417 assert_eq!(
418 editor
419 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
420 .unwrap(),
421 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
422 );
423
424 _ = editor.update(cx, |editor, window, cx| {
425 editor.update_selection(
426 DisplayPoint::new(DisplayRow(3), 3),
427 0,
428 gpui::Point::<f32>::default(),
429 window,
430 cx,
431 );
432 });
433
434 assert_eq!(
435 editor
436 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
437 .unwrap(),
438 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
439 );
440
441 _ = editor.update(cx, |editor, window, cx| {
442 editor.update_selection(
443 DisplayPoint::new(DisplayRow(1), 1),
444 0,
445 gpui::Point::<f32>::default(),
446 window,
447 cx,
448 );
449 });
450
451 assert_eq!(
452 editor
453 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
454 .unwrap(),
455 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
456 );
457
458 _ = editor.update(cx, |editor, window, cx| {
459 editor.end_selection(window, cx);
460 editor.update_selection(
461 DisplayPoint::new(DisplayRow(3), 3),
462 0,
463 gpui::Point::<f32>::default(),
464 window,
465 cx,
466 );
467 });
468
469 assert_eq!(
470 editor
471 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
472 .unwrap(),
473 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
478 editor.update_selection(
479 DisplayPoint::new(DisplayRow(0), 0),
480 0,
481 gpui::Point::<f32>::default(),
482 window,
483 cx,
484 );
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
493 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.end_selection(window, cx);
499 });
500
501 assert_eq!(
502 editor
503 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
504 .unwrap(),
505 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
506 );
507}
508
509#[gpui::test]
510fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
511 init_test(cx, |_| {});
512
513 let editor = cx.add_window(|window, cx| {
514 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
515 build_editor(buffer, window, cx)
516 });
517
518 _ = editor.update(cx, |editor, window, cx| {
519 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.end_selection(window, cx);
532 });
533
534 assert_eq!(
535 editor
536 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
537 .unwrap(),
538 [
539 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
540 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
541 ]
542 );
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.end_selection(window, cx);
550 });
551
552 assert_eq!(
553 editor
554 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
555 .unwrap(),
556 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
557 );
558}
559
560#[gpui::test]
561fn test_canceling_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575 });
576
577 _ = editor.update(cx, |editor, window, cx| {
578 editor.update_selection(
579 DisplayPoint::new(DisplayRow(3), 3),
580 0,
581 gpui::Point::<f32>::default(),
582 window,
583 cx,
584 );
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
588 );
589 });
590
591 _ = editor.update(cx, |editor, window, cx| {
592 editor.cancel(&Cancel, window, cx);
593 editor.update_selection(
594 DisplayPoint::new(DisplayRow(1), 1),
595 0,
596 gpui::Point::<f32>::default(),
597 window,
598 cx,
599 );
600 assert_eq!(
601 editor.selections.display_ranges(cx),
602 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
603 );
604 });
605}
606
607#[gpui::test]
608fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
609 init_test(cx, |_| {});
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
618 assert_eq!(
619 editor.selections.display_ranges(cx),
620 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
621 );
622
623 editor.move_down(&Default::default(), window, cx);
624 assert_eq!(
625 editor.selections.display_ranges(cx),
626 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
627 );
628
629 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
630 assert_eq!(
631 editor.selections.display_ranges(cx),
632 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
633 );
634
635 editor.move_up(&Default::default(), window, cx);
636 assert_eq!(
637 editor.selections.display_ranges(cx),
638 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
639 );
640 });
641}
642
643#[gpui::test]
644fn test_extending_selection(cx: &mut TestAppContext) {
645 init_test(cx, |_| {});
646
647 let editor = cx.add_window(|window, cx| {
648 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
649 build_editor(buffer, window, cx)
650 });
651
652 _ = editor.update(cx, |editor, window, cx| {
653 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
654 editor.end_selection(window, cx);
655 assert_eq!(
656 editor.selections.display_ranges(cx),
657 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
658 );
659
660 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 editor.selections.display_ranges(cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
670 assert_eq!(
671 editor.selections.display_ranges(cx),
672 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
673 );
674
675 editor.update_selection(
676 DisplayPoint::new(DisplayRow(0), 1),
677 0,
678 gpui::Point::<f32>::default(),
679 window,
680 cx,
681 );
682 editor.end_selection(window, cx);
683 assert_eq!(
684 editor.selections.display_ranges(cx),
685 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
686 );
687
688 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
689 editor.end_selection(window, cx);
690 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
691 editor.end_selection(window, cx);
692 assert_eq!(
693 editor.selections.display_ranges(cx),
694 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
695 );
696
697 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
698 assert_eq!(
699 editor.selections.display_ranges(cx),
700 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
701 );
702
703 editor.update_selection(
704 DisplayPoint::new(DisplayRow(0), 6),
705 0,
706 gpui::Point::<f32>::default(),
707 window,
708 cx,
709 );
710 assert_eq!(
711 editor.selections.display_ranges(cx),
712 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
713 );
714
715 editor.update_selection(
716 DisplayPoint::new(DisplayRow(0), 1),
717 0,
718 gpui::Point::<f32>::default(),
719 window,
720 cx,
721 );
722 editor.end_selection(window, cx);
723 assert_eq!(
724 editor.selections.display_ranges(cx),
725 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
726 );
727 });
728}
729
730#[gpui::test]
731fn test_clone(cx: &mut TestAppContext) {
732 init_test(cx, |_| {});
733
734 let (text, selection_ranges) = marked_text_ranges(
735 indoc! {"
736 one
737 two
738 threeˇ
739 four
740 fiveˇ
741 "},
742 true,
743 );
744
745 let editor = cx.add_window(|window, cx| {
746 let buffer = MultiBuffer::build_simple(&text, cx);
747 build_editor(buffer, window, cx)
748 });
749
750 _ = editor.update(cx, |editor, window, cx| {
751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
752 s.select_ranges(selection_ranges.clone())
753 });
754 editor.fold_creases(
755 vec![
756 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
757 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
758 ],
759 true,
760 window,
761 cx,
762 );
763 });
764
765 let cloned_editor = editor
766 .update(cx, |editor, _, cx| {
767 cx.open_window(Default::default(), |window, cx| {
768 cx.new(|cx| editor.clone(window, cx))
769 })
770 })
771 .unwrap()
772 .unwrap();
773
774 let snapshot = editor
775 .update(cx, |e, window, cx| e.snapshot(window, cx))
776 .unwrap();
777 let cloned_snapshot = cloned_editor
778 .update(cx, |e, window, cx| e.snapshot(window, cx))
779 .unwrap();
780
781 assert_eq!(
782 cloned_editor
783 .update(cx, |e, _, cx| e.display_text(cx))
784 .unwrap(),
785 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
786 );
787 assert_eq!(
788 cloned_snapshot
789 .folds_in_range(0..text.len())
790 .collect::<Vec<_>>(),
791 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
792 );
793 assert_set_eq!(
794 cloned_editor
795 .update(cx, |editor, _, cx| editor
796 .selections
797 .ranges::<Point>(&editor.display_snapshot(cx)))
798 .unwrap(),
799 editor
800 .update(cx, |editor, _, cx| editor
801 .selections
802 .ranges(&editor.display_snapshot(cx)))
803 .unwrap()
804 );
805 assert_set_eq!(
806 cloned_editor
807 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
808 .unwrap(),
809 editor
810 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
811 .unwrap()
812 );
813}
814
815#[gpui::test]
816async fn test_navigation_history(cx: &mut TestAppContext) {
817 init_test(cx, |_| {});
818
819 use workspace::item::Item;
820
821 let fs = FakeFs::new(cx.executor());
822 let project = Project::test(fs, [], cx).await;
823 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
824 let pane = workspace
825 .update(cx, |workspace, _, _| workspace.active_pane().clone())
826 .unwrap();
827
828 _ = workspace.update(cx, |_v, window, cx| {
829 cx.new(|cx| {
830 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
831 let mut editor = build_editor(buffer, window, cx);
832 let handle = cx.entity();
833 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
834
835 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
836 editor.nav_history.as_mut().unwrap().pop_backward(cx)
837 }
838
839 // Move the cursor a small distance.
840 // Nothing is added to the navigation history.
841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
842 s.select_display_ranges([
843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
844 ])
845 });
846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
847 s.select_display_ranges([
848 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
849 ])
850 });
851 assert!(pop_history(&mut editor, cx).is_none());
852
853 // Move the cursor a large distance.
854 // The history can jump back to the previous position.
855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
856 s.select_display_ranges([
857 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
858 ])
859 });
860 let nav_entry = pop_history(&mut editor, cx).unwrap();
861 editor.navigate(nav_entry.data.unwrap(), window, cx);
862 assert_eq!(nav_entry.item.id(), cx.entity_id());
863 assert_eq!(
864 editor.selections.display_ranges(cx),
865 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
866 );
867 assert!(pop_history(&mut editor, cx).is_none());
868
869 // Move the cursor a small distance via the mouse.
870 // Nothing is added to the navigation history.
871 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
872 editor.end_selection(window, cx);
873 assert_eq!(
874 editor.selections.display_ranges(cx),
875 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
876 );
877 assert!(pop_history(&mut editor, cx).is_none());
878
879 // Move the cursor a large distance via the mouse.
880 // The history can jump back to the previous position.
881 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
882 editor.end_selection(window, cx);
883 assert_eq!(
884 editor.selections.display_ranges(cx),
885 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
886 );
887 let nav_entry = pop_history(&mut editor, cx).unwrap();
888 editor.navigate(nav_entry.data.unwrap(), window, cx);
889 assert_eq!(nav_entry.item.id(), cx.entity_id());
890 assert_eq!(
891 editor.selections.display_ranges(cx),
892 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
893 );
894 assert!(pop_history(&mut editor, cx).is_none());
895
896 // Set scroll position to check later
897 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
898 let original_scroll_position = editor.scroll_manager.anchor();
899
900 // Jump to the end of the document and adjust scroll
901 editor.move_to_end(&MoveToEnd, window, cx);
902 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
903 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
904
905 let nav_entry = pop_history(&mut editor, cx).unwrap();
906 editor.navigate(nav_entry.data.unwrap(), window, cx);
907 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
908
909 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
910 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
911 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
912 let invalid_point = Point::new(9999, 0);
913 editor.navigate(
914 Box::new(NavigationData {
915 cursor_anchor: invalid_anchor,
916 cursor_position: invalid_point,
917 scroll_anchor: ScrollAnchor {
918 anchor: invalid_anchor,
919 offset: Default::default(),
920 },
921 scroll_top_row: invalid_point.row,
922 }),
923 window,
924 cx,
925 );
926 assert_eq!(
927 editor.selections.display_ranges(cx),
928 &[editor.max_point(cx)..editor.max_point(cx)]
929 );
930 assert_eq!(
931 editor.scroll_position(cx),
932 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
933 );
934
935 editor
936 })
937 });
938}
939
940#[gpui::test]
941fn test_cancel(cx: &mut TestAppContext) {
942 init_test(cx, |_| {});
943
944 let editor = cx.add_window(|window, cx| {
945 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
946 build_editor(buffer, window, cx)
947 });
948
949 _ = editor.update(cx, |editor, window, cx| {
950 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
951 editor.update_selection(
952 DisplayPoint::new(DisplayRow(1), 1),
953 0,
954 gpui::Point::<f32>::default(),
955 window,
956 cx,
957 );
958 editor.end_selection(window, cx);
959
960 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
961 editor.update_selection(
962 DisplayPoint::new(DisplayRow(0), 3),
963 0,
964 gpui::Point::<f32>::default(),
965 window,
966 cx,
967 );
968 editor.end_selection(window, cx);
969 assert_eq!(
970 editor.selections.display_ranges(cx),
971 [
972 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
973 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
974 ]
975 );
976 });
977
978 _ = editor.update(cx, |editor, window, cx| {
979 editor.cancel(&Cancel, window, cx);
980 assert_eq!(
981 editor.selections.display_ranges(cx),
982 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
983 );
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.cancel(&Cancel, window, cx);
988 assert_eq!(
989 editor.selections.display_ranges(cx),
990 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
991 );
992 });
993}
994
995#[gpui::test]
996fn test_fold_action(cx: &mut TestAppContext) {
997 init_test(cx, |_| {});
998
999 let editor = cx.add_window(|window, cx| {
1000 let buffer = MultiBuffer::build_simple(
1001 &"
1002 impl Foo {
1003 // Hello!
1004
1005 fn a() {
1006 1
1007 }
1008
1009 fn b() {
1010 2
1011 }
1012
1013 fn c() {
1014 3
1015 }
1016 }
1017 "
1018 .unindent(),
1019 cx,
1020 );
1021 build_editor(buffer, window, cx)
1022 });
1023
1024 _ = editor.update(cx, |editor, window, cx| {
1025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1026 s.select_display_ranges([
1027 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1028 ]);
1029 });
1030 editor.fold(&Fold, window, cx);
1031 assert_eq!(
1032 editor.display_text(cx),
1033 "
1034 impl Foo {
1035 // Hello!
1036
1037 fn a() {
1038 1
1039 }
1040
1041 fn b() {⋯
1042 }
1043
1044 fn c() {⋯
1045 }
1046 }
1047 "
1048 .unindent(),
1049 );
1050
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {⋯
1056 }
1057 "
1058 .unindent(),
1059 );
1060
1061 editor.unfold_lines(&UnfoldLines, window, cx);
1062 assert_eq!(
1063 editor.display_text(cx),
1064 "
1065 impl Foo {
1066 // Hello!
1067
1068 fn a() {
1069 1
1070 }
1071
1072 fn b() {⋯
1073 }
1074
1075 fn c() {⋯
1076 }
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 editor.buffer.read(cx).read(cx).text()
1086 );
1087 });
1088}
1089
1090#[gpui::test]
1091fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1092 init_test(cx, |_| {});
1093
1094 let editor = cx.add_window(|window, cx| {
1095 let buffer = MultiBuffer::build_simple(
1096 &"
1097 class Foo:
1098 # Hello!
1099
1100 def a():
1101 print(1)
1102
1103 def b():
1104 print(2)
1105
1106 def c():
1107 print(3)
1108 "
1109 .unindent(),
1110 cx,
1111 );
1112 build_editor(buffer, window, cx)
1113 });
1114
1115 _ = editor.update(cx, |editor, window, cx| {
1116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1117 s.select_display_ranges([
1118 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1119 ]);
1120 });
1121 editor.fold(&Fold, window, cx);
1122 assert_eq!(
1123 editor.display_text(cx),
1124 "
1125 class Foo:
1126 # Hello!
1127
1128 def a():
1129 print(1)
1130
1131 def b():⋯
1132
1133 def c():⋯
1134 "
1135 .unindent(),
1136 );
1137
1138 editor.fold(&Fold, window, cx);
1139 assert_eq!(
1140 editor.display_text(cx),
1141 "
1142 class Foo:⋯
1143 "
1144 .unindent(),
1145 );
1146
1147 editor.unfold_lines(&UnfoldLines, window, cx);
1148 assert_eq!(
1149 editor.display_text(cx),
1150 "
1151 class Foo:
1152 # Hello!
1153
1154 def a():
1155 print(1)
1156
1157 def b():⋯
1158
1159 def c():⋯
1160 "
1161 .unindent(),
1162 );
1163
1164 editor.unfold_lines(&UnfoldLines, window, cx);
1165 assert_eq!(
1166 editor.display_text(cx),
1167 editor.buffer.read(cx).read(cx).text()
1168 );
1169 });
1170}
1171
1172#[gpui::test]
1173fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1174 init_test(cx, |_| {});
1175
1176 let editor = cx.add_window(|window, cx| {
1177 let buffer = MultiBuffer::build_simple(
1178 &"
1179 class Foo:
1180 # Hello!
1181
1182 def a():
1183 print(1)
1184
1185 def b():
1186 print(2)
1187
1188
1189 def c():
1190 print(3)
1191
1192
1193 "
1194 .unindent(),
1195 cx,
1196 );
1197 build_editor(buffer, window, cx)
1198 });
1199
1200 _ = editor.update(cx, |editor, window, cx| {
1201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1202 s.select_display_ranges([
1203 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1204 ]);
1205 });
1206 editor.fold(&Fold, window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():⋯
1217
1218
1219 def c():⋯
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.fold(&Fold, window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:⋯
1231
1232
1233 "
1234 .unindent(),
1235 );
1236
1237 editor.unfold_lines(&UnfoldLines, window, cx);
1238 assert_eq!(
1239 editor.display_text(cx),
1240 "
1241 class Foo:
1242 # Hello!
1243
1244 def a():
1245 print(1)
1246
1247 def b():⋯
1248
1249
1250 def c():⋯
1251
1252
1253 "
1254 .unindent(),
1255 );
1256
1257 editor.unfold_lines(&UnfoldLines, window, cx);
1258 assert_eq!(
1259 editor.display_text(cx),
1260 editor.buffer.read(cx).read(cx).text()
1261 );
1262 });
1263}
1264
1265#[gpui::test]
1266fn test_fold_at_level(cx: &mut TestAppContext) {
1267 init_test(cx, |_| {});
1268
1269 let editor = cx.add_window(|window, cx| {
1270 let buffer = MultiBuffer::build_simple(
1271 &"
1272 class Foo:
1273 # Hello!
1274
1275 def a():
1276 print(1)
1277
1278 def b():
1279 print(2)
1280
1281
1282 class Bar:
1283 # World!
1284
1285 def a():
1286 print(1)
1287
1288 def b():
1289 print(2)
1290
1291
1292 "
1293 .unindent(),
1294 cx,
1295 );
1296 build_editor(buffer, window, cx)
1297 });
1298
1299 _ = editor.update(cx, |editor, window, cx| {
1300 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1301 assert_eq!(
1302 editor.display_text(cx),
1303 "
1304 class Foo:
1305 # Hello!
1306
1307 def a():⋯
1308
1309 def b():⋯
1310
1311
1312 class Bar:
1313 # World!
1314
1315 def a():⋯
1316
1317 def b():⋯
1318
1319
1320 "
1321 .unindent(),
1322 );
1323
1324 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1325 assert_eq!(
1326 editor.display_text(cx),
1327 "
1328 class Foo:⋯
1329
1330
1331 class Bar:⋯
1332
1333
1334 "
1335 .unindent(),
1336 );
1337
1338 editor.unfold_all(&UnfoldAll, window, cx);
1339 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1340 assert_eq!(
1341 editor.display_text(cx),
1342 "
1343 class Foo:
1344 # Hello!
1345
1346 def a():
1347 print(1)
1348
1349 def b():
1350 print(2)
1351
1352
1353 class Bar:
1354 # World!
1355
1356 def a():
1357 print(1)
1358
1359 def b():
1360 print(2)
1361
1362
1363 "
1364 .unindent(),
1365 );
1366
1367 assert_eq!(
1368 editor.display_text(cx),
1369 editor.buffer.read(cx).read(cx).text()
1370 );
1371 let (_, positions) = marked_text_ranges(
1372 &"
1373 class Foo:
1374 # Hello!
1375
1376 def a():
1377 print(1)
1378
1379 def b():
1380 p«riˇ»nt(2)
1381
1382
1383 class Bar:
1384 # World!
1385
1386 def a():
1387 «ˇprint(1)
1388
1389 def b():
1390 print(2)»
1391
1392
1393 "
1394 .unindent(),
1395 true,
1396 );
1397
1398 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1399 s.select_ranges(positions)
1400 });
1401
1402 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1403 assert_eq!(
1404 editor.display_text(cx),
1405 "
1406 class Foo:
1407 # Hello!
1408
1409 def a():⋯
1410
1411 def b():
1412 print(2)
1413
1414
1415 class Bar:
1416 # World!
1417
1418 def a():
1419 print(1)
1420
1421 def b():
1422 print(2)
1423
1424
1425 "
1426 .unindent(),
1427 );
1428 });
1429}
1430
1431#[gpui::test]
1432fn test_move_cursor(cx: &mut TestAppContext) {
1433 init_test(cx, |_| {});
1434
1435 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1436 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1437
1438 buffer.update(cx, |buffer, cx| {
1439 buffer.edit(
1440 vec![
1441 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1442 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1443 ],
1444 None,
1445 cx,
1446 );
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1458 );
1459
1460 editor.move_right(&MoveRight, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1464 );
1465
1466 editor.move_left(&MoveLeft, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1470 );
1471
1472 editor.move_up(&MoveUp, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1476 );
1477
1478 editor.move_to_end(&MoveToEnd, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1482 );
1483
1484 editor.move_to_beginning(&MoveToBeginning, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1488 );
1489
1490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1491 s.select_display_ranges([
1492 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1493 ]);
1494 });
1495 editor.select_to_beginning(&SelectToBeginning, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1499 );
1500
1501 editor.select_to_end(&SelectToEnd, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1505 );
1506 });
1507}
1508
1509#[gpui::test]
1510fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1511 init_test(cx, |_| {});
1512
1513 let editor = cx.add_window(|window, cx| {
1514 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1515 build_editor(buffer, window, cx)
1516 });
1517
1518 assert_eq!('🟥'.len_utf8(), 4);
1519 assert_eq!('α'.len_utf8(), 2);
1520
1521 _ = editor.update(cx, |editor, window, cx| {
1522 editor.fold_creases(
1523 vec![
1524 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1525 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1526 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1527 ],
1528 true,
1529 window,
1530 cx,
1531 );
1532 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1533
1534 editor.move_right(&MoveRight, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(0, "🟥".len())]
1538 );
1539 editor.move_right(&MoveRight, window, cx);
1540 assert_eq!(
1541 editor.selections.display_ranges(cx),
1542 &[empty_range(0, "🟥🟧".len())]
1543 );
1544 editor.move_right(&MoveRight, window, cx);
1545 assert_eq!(
1546 editor.selections.display_ranges(cx),
1547 &[empty_range(0, "🟥🟧⋯".len())]
1548 );
1549
1550 editor.move_down(&MoveDown, window, cx);
1551 assert_eq!(
1552 editor.selections.display_ranges(cx),
1553 &[empty_range(1, "ab⋯e".len())]
1554 );
1555 editor.move_left(&MoveLeft, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[empty_range(1, "ab⋯".len())]
1559 );
1560 editor.move_left(&MoveLeft, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(1, "ab".len())]
1564 );
1565 editor.move_left(&MoveLeft, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[empty_range(1, "a".len())]
1569 );
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[empty_range(2, "α".len())]
1575 );
1576 editor.move_right(&MoveRight, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[empty_range(2, "αβ".len())]
1580 );
1581 editor.move_right(&MoveRight, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[empty_range(2, "αβ⋯".len())]
1585 );
1586 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(
1588 editor.selections.display_ranges(cx),
1589 &[empty_range(2, "αβ⋯ε".len())]
1590 );
1591
1592 editor.move_up(&MoveUp, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[empty_range(1, "ab⋯e".len())]
1596 );
1597 editor.move_down(&MoveDown, window, cx);
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[empty_range(2, "αβ⋯ε".len())]
1601 );
1602 editor.move_up(&MoveUp, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[empty_range(1, "ab⋯e".len())]
1606 );
1607
1608 editor.move_up(&MoveUp, window, cx);
1609 assert_eq!(
1610 editor.selections.display_ranges(cx),
1611 &[empty_range(0, "🟥🟧".len())]
1612 );
1613 editor.move_left(&MoveLeft, window, cx);
1614 assert_eq!(
1615 editor.selections.display_ranges(cx),
1616 &[empty_range(0, "🟥".len())]
1617 );
1618 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(
1620 editor.selections.display_ranges(cx),
1621 &[empty_range(0, "".len())]
1622 );
1623 });
1624}
1625
1626#[gpui::test]
1627fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1628 init_test(cx, |_| {});
1629
1630 let editor = cx.add_window(|window, cx| {
1631 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1632 build_editor(buffer, window, cx)
1633 });
1634 _ = editor.update(cx, |editor, window, cx| {
1635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1636 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1637 });
1638
1639 // moving above start of document should move selection to start of document,
1640 // but the next move down should still be at the original goal_x
1641 editor.move_up(&MoveUp, window, cx);
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[empty_range(0, "".len())]
1645 );
1646
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[empty_range(1, "abcd".len())]
1651 );
1652
1653 editor.move_down(&MoveDown, window, cx);
1654 assert_eq!(
1655 editor.selections.display_ranges(cx),
1656 &[empty_range(2, "αβγ".len())]
1657 );
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[empty_range(3, "abcd".len())]
1663 );
1664
1665 editor.move_down(&MoveDown, window, cx);
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1669 );
1670
1671 // moving past end of document should not change goal_x
1672 editor.move_down(&MoveDown, window, cx);
1673 assert_eq!(
1674 editor.selections.display_ranges(cx),
1675 &[empty_range(5, "".len())]
1676 );
1677
1678 editor.move_down(&MoveDown, window, cx);
1679 assert_eq!(
1680 editor.selections.display_ranges(cx),
1681 &[empty_range(5, "".len())]
1682 );
1683
1684 editor.move_up(&MoveUp, window, cx);
1685 assert_eq!(
1686 editor.selections.display_ranges(cx),
1687 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1688 );
1689
1690 editor.move_up(&MoveUp, window, cx);
1691 assert_eq!(
1692 editor.selections.display_ranges(cx),
1693 &[empty_range(3, "abcd".len())]
1694 );
1695
1696 editor.move_up(&MoveUp, window, cx);
1697 assert_eq!(
1698 editor.selections.display_ranges(cx),
1699 &[empty_range(2, "αβγ".len())]
1700 );
1701 });
1702}
1703
1704#[gpui::test]
1705fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1706 init_test(cx, |_| {});
1707 let move_to_beg = MoveToBeginningOfLine {
1708 stop_at_soft_wraps: true,
1709 stop_at_indent: true,
1710 };
1711
1712 let delete_to_beg = DeleteToBeginningOfLine {
1713 stop_at_indent: false,
1714 };
1715
1716 let move_to_end = MoveToEndOfLine {
1717 stop_at_soft_wraps: true,
1718 };
1719
1720 let editor = cx.add_window(|window, cx| {
1721 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1722 build_editor(buffer, window, cx)
1723 });
1724 _ = editor.update(cx, |editor, window, cx| {
1725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1726 s.select_display_ranges([
1727 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1728 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1729 ]);
1730 });
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1735 assert_eq!(
1736 editor.selections.display_ranges(cx),
1737 &[
1738 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1740 ]
1741 );
1742 });
1743
1744 _ = editor.update(cx, |editor, window, cx| {
1745 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1746 assert_eq!(
1747 editor.selections.display_ranges(cx),
1748 &[
1749 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1750 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1751 ]
1752 );
1753 });
1754
1755 _ = editor.update(cx, |editor, window, cx| {
1756 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1757 assert_eq!(
1758 editor.selections.display_ranges(cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.move_to_end_of_line(&move_to_end, window, cx);
1768 assert_eq!(
1769 editor.selections.display_ranges(cx),
1770 &[
1771 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1772 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1773 ]
1774 );
1775 });
1776
1777 // Moving to the end of line again is a no-op.
1778 _ = editor.update(cx, |editor, window, cx| {
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 editor.selections.display_ranges(cx),
1782 &[
1783 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1784 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1785 ]
1786 );
1787 });
1788
1789 _ = editor.update(cx, |editor, window, cx| {
1790 editor.move_left(&MoveLeft, window, cx);
1791 editor.select_to_beginning_of_line(
1792 &SelectToBeginningOfLine {
1793 stop_at_soft_wraps: true,
1794 stop_at_indent: true,
1795 },
1796 window,
1797 cx,
1798 );
1799 assert_eq!(
1800 editor.selections.display_ranges(cx),
1801 &[
1802 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1803 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1804 ]
1805 );
1806 });
1807
1808 _ = editor.update(cx, |editor, window, cx| {
1809 editor.select_to_beginning_of_line(
1810 &SelectToBeginningOfLine {
1811 stop_at_soft_wraps: true,
1812 stop_at_indent: true,
1813 },
1814 window,
1815 cx,
1816 );
1817 assert_eq!(
1818 editor.selections.display_ranges(cx),
1819 &[
1820 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1822 ]
1823 );
1824 });
1825
1826 _ = editor.update(cx, |editor, window, cx| {
1827 editor.select_to_beginning_of_line(
1828 &SelectToBeginningOfLine {
1829 stop_at_soft_wraps: true,
1830 stop_at_indent: true,
1831 },
1832 window,
1833 cx,
1834 );
1835 assert_eq!(
1836 editor.selections.display_ranges(cx),
1837 &[
1838 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1839 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1840 ]
1841 );
1842 });
1843
1844 _ = editor.update(cx, |editor, window, cx| {
1845 editor.select_to_end_of_line(
1846 &SelectToEndOfLine {
1847 stop_at_soft_wraps: true,
1848 },
1849 window,
1850 cx,
1851 );
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1856 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1857 ]
1858 );
1859 });
1860
1861 _ = editor.update(cx, |editor, window, cx| {
1862 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1863 assert_eq!(editor.display_text(cx), "ab\n de");
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1869 ]
1870 );
1871 });
1872
1873 _ = editor.update(cx, |editor, window, cx| {
1874 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1875 assert_eq!(editor.display_text(cx), "\n");
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883 });
1884}
1885
1886#[gpui::test]
1887fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1888 init_test(cx, |_| {});
1889 let move_to_beg = MoveToBeginningOfLine {
1890 stop_at_soft_wraps: false,
1891 stop_at_indent: false,
1892 };
1893
1894 let move_to_end = MoveToEndOfLine {
1895 stop_at_soft_wraps: false,
1896 };
1897
1898 let editor = cx.add_window(|window, cx| {
1899 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1900 build_editor(buffer, window, cx)
1901 });
1902
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.set_wrap_width(Some(140.0.into()), cx);
1905
1906 // We expect the following lines after wrapping
1907 // ```
1908 // thequickbrownfox
1909 // jumpedoverthelazydo
1910 // gs
1911 // ```
1912 // The final `gs` was soft-wrapped onto a new line.
1913 assert_eq!(
1914 "thequickbrownfox\njumpedoverthelaz\nydogs",
1915 editor.display_text(cx),
1916 );
1917
1918 // First, let's assert behavior on the first line, that was not soft-wrapped.
1919 // Start the cursor at the `k` on the first line
1920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1921 s.select_display_ranges([
1922 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1923 ]);
1924 });
1925
1926 // Moving to the beginning of the line should put us at the beginning of the line.
1927 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1928 assert_eq!(
1929 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1930 editor.selections.display_ranges(cx)
1931 );
1932
1933 // Moving to the end of the line should put us at the end of the line.
1934 editor.move_to_end_of_line(&move_to_end, window, cx);
1935 assert_eq!(
1936 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1937 editor.selections.display_ranges(cx)
1938 );
1939
1940 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1941 // Start the cursor at the last line (`y` that was wrapped to a new line)
1942 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1943 s.select_display_ranges([
1944 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1945 ]);
1946 });
1947
1948 // Moving to the beginning of the line should put us at the start of the second line of
1949 // display text, i.e., the `j`.
1950 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1951 assert_eq!(
1952 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1953 editor.selections.display_ranges(cx)
1954 );
1955
1956 // Moving to the beginning of the line again should be a no-op.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1960 editor.selections.display_ranges(cx)
1961 );
1962
1963 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1964 // next display line.
1965 editor.move_to_end_of_line(&move_to_end, window, cx);
1966 assert_eq!(
1967 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1968 editor.selections.display_ranges(cx)
1969 );
1970
1971 // Moving to the end of the line again should be a no-op.
1972 editor.move_to_end_of_line(&move_to_end, window, cx);
1973 assert_eq!(
1974 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1975 editor.selections.display_ranges(cx)
1976 );
1977 });
1978}
1979
1980#[gpui::test]
1981fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1982 init_test(cx, |_| {});
1983
1984 let move_to_beg = MoveToBeginningOfLine {
1985 stop_at_soft_wraps: true,
1986 stop_at_indent: true,
1987 };
1988
1989 let select_to_beg = SelectToBeginningOfLine {
1990 stop_at_soft_wraps: true,
1991 stop_at_indent: true,
1992 };
1993
1994 let delete_to_beg = DeleteToBeginningOfLine {
1995 stop_at_indent: true,
1996 };
1997
1998 let move_to_end = MoveToEndOfLine {
1999 stop_at_soft_wraps: false,
2000 };
2001
2002 let editor = cx.add_window(|window, cx| {
2003 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2004 build_editor(buffer, window, cx)
2005 });
2006
2007 _ = editor.update(cx, |editor, window, cx| {
2008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2009 s.select_display_ranges([
2010 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2011 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2012 ]);
2013 });
2014
2015 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2016 // and the second cursor at the first non-whitespace character in the line.
2017 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2018 assert_eq!(
2019 editor.selections.display_ranges(cx),
2020 &[
2021 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2022 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2023 ]
2024 );
2025
2026 // Moving to the beginning of the line again should be a no-op for the first cursor,
2027 // and should move the second cursor to the beginning of the line.
2028 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2029 assert_eq!(
2030 editor.selections.display_ranges(cx),
2031 &[
2032 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2033 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2034 ]
2035 );
2036
2037 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2038 // and should move the second cursor back to the first non-whitespace character in the line.
2039 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2040 assert_eq!(
2041 editor.selections.display_ranges(cx),
2042 &[
2043 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2044 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2045 ]
2046 );
2047
2048 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2049 // and to the first non-whitespace character in the line for the second cursor.
2050 editor.move_to_end_of_line(&move_to_end, window, cx);
2051 editor.move_left(&MoveLeft, window, cx);
2052 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2053 assert_eq!(
2054 editor.selections.display_ranges(cx),
2055 &[
2056 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2057 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2058 ]
2059 );
2060
2061 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2062 // and should select to the beginning of the line for the second cursor.
2063 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2064 assert_eq!(
2065 editor.selections.display_ranges(cx),
2066 &[
2067 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2068 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2069 ]
2070 );
2071
2072 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2073 // and should delete to the first non-whitespace character in the line for the second cursor.
2074 editor.move_to_end_of_line(&move_to_end, window, cx);
2075 editor.move_left(&MoveLeft, window, cx);
2076 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2077 assert_eq!(editor.text(cx), "c\n f");
2078 });
2079}
2080
2081#[gpui::test]
2082fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2083 init_test(cx, |_| {});
2084
2085 let move_to_beg = MoveToBeginningOfLine {
2086 stop_at_soft_wraps: true,
2087 stop_at_indent: true,
2088 };
2089
2090 let editor = cx.add_window(|window, cx| {
2091 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2092 build_editor(buffer, window, cx)
2093 });
2094
2095 _ = editor.update(cx, |editor, window, cx| {
2096 // test cursor between line_start and indent_start
2097 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2098 s.select_display_ranges([
2099 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2100 ]);
2101 });
2102
2103 // cursor should move to line_start
2104 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2105 assert_eq!(
2106 editor.selections.display_ranges(cx),
2107 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2108 );
2109
2110 // cursor should move to indent_start
2111 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2112 assert_eq!(
2113 editor.selections.display_ranges(cx),
2114 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2115 );
2116
2117 // cursor should move to back to line_start
2118 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2119 assert_eq!(
2120 editor.selections.display_ranges(cx),
2121 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2122 );
2123 });
2124}
2125
2126#[gpui::test]
2127fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2128 init_test(cx, |_| {});
2129
2130 let editor = cx.add_window(|window, cx| {
2131 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2132 build_editor(buffer, window, cx)
2133 });
2134 _ = editor.update(cx, |editor, window, cx| {
2135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2136 s.select_display_ranges([
2137 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2138 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2139 ])
2140 });
2141 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2142 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2143
2144 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2145 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2146
2147 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2148 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2149
2150 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2151 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2152
2153 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2154 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2155
2156 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2157 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2158
2159 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2160 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2161
2162 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2163 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2164
2165 editor.move_right(&MoveRight, window, cx);
2166 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2167 assert_selection_ranges(
2168 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2169 editor,
2170 cx,
2171 );
2172
2173 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2174 assert_selection_ranges(
2175 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2176 editor,
2177 cx,
2178 );
2179
2180 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2181 assert_selection_ranges(
2182 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2183 editor,
2184 cx,
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192
2193 let editor = cx.add_window(|window, cx| {
2194 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2195 build_editor(buffer, window, cx)
2196 });
2197
2198 _ = editor.update(cx, |editor, window, cx| {
2199 editor.set_wrap_width(Some(140.0.into()), cx);
2200 assert_eq!(
2201 editor.display_text(cx),
2202 "use one::{\n two::three::\n four::five\n};"
2203 );
2204
2205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2206 s.select_display_ranges([
2207 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2208 ]);
2209 });
2210
2211 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2212 assert_eq!(
2213 editor.selections.display_ranges(cx),
2214 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2215 );
2216
2217 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2218 assert_eq!(
2219 editor.selections.display_ranges(cx),
2220 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2221 );
2222
2223 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2224 assert_eq!(
2225 editor.selections.display_ranges(cx),
2226 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2227 );
2228
2229 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2230 assert_eq!(
2231 editor.selections.display_ranges(cx),
2232 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2233 );
2234
2235 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2236 assert_eq!(
2237 editor.selections.display_ranges(cx),
2238 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2239 );
2240
2241 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2242 assert_eq!(
2243 editor.selections.display_ranges(cx),
2244 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2245 );
2246 });
2247}
2248
2249#[gpui::test]
2250async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2251 init_test(cx, |_| {});
2252 let mut cx = EditorTestContext::new(cx).await;
2253
2254 let line_height = cx.editor(|editor, window, _| {
2255 editor
2256 .style()
2257 .unwrap()
2258 .text
2259 .line_height_in_pixels(window.rem_size())
2260 });
2261 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2262
2263 cx.set_state(
2264 &r#"ˇone
2265 two
2266
2267 three
2268 fourˇ
2269 five
2270
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285 ˇ
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"one
2295 two
2296
2297 three
2298 four
2299 five
2300 ˇ
2301 sixˇ"#
2302 .unindent(),
2303 );
2304
2305 cx.update_editor(|editor, window, cx| {
2306 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2307 });
2308 cx.assert_editor_state(
2309 &r#"one
2310 two
2311
2312 three
2313 four
2314 five
2315
2316 sixˇ"#
2317 .unindent(),
2318 );
2319
2320 cx.update_editor(|editor, window, cx| {
2321 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2322 });
2323 cx.assert_editor_state(
2324 &r#"one
2325 two
2326
2327 three
2328 four
2329 five
2330 ˇ
2331 six"#
2332 .unindent(),
2333 );
2334
2335 cx.update_editor(|editor, window, cx| {
2336 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2337 });
2338 cx.assert_editor_state(
2339 &r#"one
2340 two
2341 ˇ
2342 three
2343 four
2344 five
2345
2346 six"#
2347 .unindent(),
2348 );
2349
2350 cx.update_editor(|editor, window, cx| {
2351 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2352 });
2353 cx.assert_editor_state(
2354 &r#"ˇone
2355 two
2356
2357 three
2358 four
2359 five
2360
2361 six"#
2362 .unindent(),
2363 );
2364}
2365
2366#[gpui::test]
2367async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2368 init_test(cx, |_| {});
2369 let mut cx = EditorTestContext::new(cx).await;
2370 let line_height = cx.editor(|editor, window, _| {
2371 editor
2372 .style()
2373 .unwrap()
2374 .text
2375 .line_height_in_pixels(window.rem_size())
2376 });
2377 let window = cx.window;
2378 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2379
2380 cx.set_state(
2381 r#"ˇone
2382 two
2383 three
2384 four
2385 five
2386 six
2387 seven
2388 eight
2389 nine
2390 ten
2391 "#,
2392 );
2393
2394 cx.update_editor(|editor, window, cx| {
2395 assert_eq!(
2396 editor.snapshot(window, cx).scroll_position(),
2397 gpui::Point::new(0., 0.)
2398 );
2399 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2400 assert_eq!(
2401 editor.snapshot(window, cx).scroll_position(),
2402 gpui::Point::new(0., 3.)
2403 );
2404 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2405 assert_eq!(
2406 editor.snapshot(window, cx).scroll_position(),
2407 gpui::Point::new(0., 6.)
2408 );
2409 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2410 assert_eq!(
2411 editor.snapshot(window, cx).scroll_position(),
2412 gpui::Point::new(0., 3.)
2413 );
2414
2415 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 1.)
2419 );
2420 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2421 assert_eq!(
2422 editor.snapshot(window, cx).scroll_position(),
2423 gpui::Point::new(0., 3.)
2424 );
2425 });
2426}
2427
2428#[gpui::test]
2429async fn test_autoscroll(cx: &mut TestAppContext) {
2430 init_test(cx, |_| {});
2431 let mut cx = EditorTestContext::new(cx).await;
2432
2433 let line_height = cx.update_editor(|editor, window, cx| {
2434 editor.set_vertical_scroll_margin(2, cx);
2435 editor
2436 .style()
2437 .unwrap()
2438 .text
2439 .line_height_in_pixels(window.rem_size())
2440 });
2441 let window = cx.window;
2442 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2443
2444 cx.set_state(
2445 r#"ˇone
2446 two
2447 three
2448 four
2449 five
2450 six
2451 seven
2452 eight
2453 nine
2454 ten
2455 "#,
2456 );
2457 cx.update_editor(|editor, window, cx| {
2458 assert_eq!(
2459 editor.snapshot(window, cx).scroll_position(),
2460 gpui::Point::new(0., 0.0)
2461 );
2462 });
2463
2464 // Add a cursor below the visible area. Since both cursors cannot fit
2465 // on screen, the editor autoscrolls to reveal the newest cursor, and
2466 // allows the vertical scroll margin below that cursor.
2467 cx.update_editor(|editor, window, cx| {
2468 editor.change_selections(Default::default(), window, cx, |selections| {
2469 selections.select_ranges([
2470 Point::new(0, 0)..Point::new(0, 0),
2471 Point::new(6, 0)..Point::new(6, 0),
2472 ]);
2473 })
2474 });
2475 cx.update_editor(|editor, window, cx| {
2476 assert_eq!(
2477 editor.snapshot(window, cx).scroll_position(),
2478 gpui::Point::new(0., 3.0)
2479 );
2480 });
2481
2482 // Move down. The editor cursor scrolls down to track the newest cursor.
2483 cx.update_editor(|editor, window, cx| {
2484 editor.move_down(&Default::default(), window, cx);
2485 });
2486 cx.update_editor(|editor, window, cx| {
2487 assert_eq!(
2488 editor.snapshot(window, cx).scroll_position(),
2489 gpui::Point::new(0., 4.0)
2490 );
2491 });
2492
2493 // Add a cursor above the visible area. Since both cursors fit on screen,
2494 // the editor scrolls to show both.
2495 cx.update_editor(|editor, window, cx| {
2496 editor.change_selections(Default::default(), window, cx, |selections| {
2497 selections.select_ranges([
2498 Point::new(1, 0)..Point::new(1, 0),
2499 Point::new(6, 0)..Point::new(6, 0),
2500 ]);
2501 })
2502 });
2503 cx.update_editor(|editor, window, cx| {
2504 assert_eq!(
2505 editor.snapshot(window, cx).scroll_position(),
2506 gpui::Point::new(0., 1.0)
2507 );
2508 });
2509}
2510
2511#[gpui::test]
2512async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2513 init_test(cx, |_| {});
2514 let mut cx = EditorTestContext::new(cx).await;
2515
2516 let line_height = cx.editor(|editor, window, _cx| {
2517 editor
2518 .style()
2519 .unwrap()
2520 .text
2521 .line_height_in_pixels(window.rem_size())
2522 });
2523 let window = cx.window;
2524 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2525 cx.set_state(
2526 &r#"
2527 ˇone
2528 two
2529 threeˇ
2530 four
2531 five
2532 six
2533 seven
2534 eight
2535 nine
2536 ten
2537 "#
2538 .unindent(),
2539 );
2540
2541 cx.update_editor(|editor, window, cx| {
2542 editor.move_page_down(&MovePageDown::default(), window, cx)
2543 });
2544 cx.assert_editor_state(
2545 &r#"
2546 one
2547 two
2548 three
2549 ˇfour
2550 five
2551 sixˇ
2552 seven
2553 eight
2554 nine
2555 ten
2556 "#
2557 .unindent(),
2558 );
2559
2560 cx.update_editor(|editor, window, cx| {
2561 editor.move_page_down(&MovePageDown::default(), window, cx)
2562 });
2563 cx.assert_editor_state(
2564 &r#"
2565 one
2566 two
2567 three
2568 four
2569 five
2570 six
2571 ˇseven
2572 eight
2573 nineˇ
2574 ten
2575 "#
2576 .unindent(),
2577 );
2578
2579 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2580 cx.assert_editor_state(
2581 &r#"
2582 one
2583 two
2584 three
2585 ˇfour
2586 five
2587 sixˇ
2588 seven
2589 eight
2590 nine
2591 ten
2592 "#
2593 .unindent(),
2594 );
2595
2596 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2597 cx.assert_editor_state(
2598 &r#"
2599 ˇone
2600 two
2601 threeˇ
2602 four
2603 five
2604 six
2605 seven
2606 eight
2607 nine
2608 ten
2609 "#
2610 .unindent(),
2611 );
2612
2613 // Test select collapsing
2614 cx.update_editor(|editor, window, cx| {
2615 editor.move_page_down(&MovePageDown::default(), window, cx);
2616 editor.move_page_down(&MovePageDown::default(), window, cx);
2617 editor.move_page_down(&MovePageDown::default(), window, cx);
2618 });
2619 cx.assert_editor_state(
2620 &r#"
2621 one
2622 two
2623 three
2624 four
2625 five
2626 six
2627 seven
2628 eight
2629 nine
2630 ˇten
2631 ˇ"#
2632 .unindent(),
2633 );
2634}
2635
2636#[gpui::test]
2637async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2638 init_test(cx, |_| {});
2639 let mut cx = EditorTestContext::new(cx).await;
2640 cx.set_state("one «two threeˇ» four");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_beginning_of_line(
2643 &DeleteToBeginningOfLine {
2644 stop_at_indent: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 assert_eq!(editor.text(cx), " four");
2650 });
2651}
2652
2653#[gpui::test]
2654async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 // For an empty selection, the preceding word fragment is deleted.
2660 // For non-empty selections, only selected characters are deleted.
2661 cx.set_state("onˇe two t«hreˇ»e four");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_previous_word_start(
2664 &DeleteToPreviousWordStart {
2665 ignore_newlines: false,
2666 ignore_brackets: false,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 cx.assert_editor_state("ˇe two tˇe four");
2673
2674 cx.set_state("e tˇwo te «fˇ»our");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: false,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("e tˇ te ˇour");
2686}
2687
2688#[gpui::test]
2689async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2690 init_test(cx, |_| {});
2691
2692 let mut cx = EditorTestContext::new(cx).await;
2693
2694 cx.set_state("here is some text ˇwith a space");
2695 cx.update_editor(|editor, window, cx| {
2696 editor.delete_to_previous_word_start(
2697 &DeleteToPreviousWordStart {
2698 ignore_newlines: false,
2699 ignore_brackets: true,
2700 },
2701 window,
2702 cx,
2703 );
2704 });
2705 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2706 cx.assert_editor_state("here is some textˇwith a space");
2707
2708 cx.set_state("here is some text ˇwith a space");
2709 cx.update_editor(|editor, window, cx| {
2710 editor.delete_to_previous_word_start(
2711 &DeleteToPreviousWordStart {
2712 ignore_newlines: false,
2713 ignore_brackets: false,
2714 },
2715 window,
2716 cx,
2717 );
2718 });
2719 cx.assert_editor_state("here is some textˇwith a space");
2720
2721 cx.set_state("here is some textˇ with a space");
2722 cx.update_editor(|editor, window, cx| {
2723 editor.delete_to_next_word_end(
2724 &DeleteToNextWordEnd {
2725 ignore_newlines: false,
2726 ignore_brackets: true,
2727 },
2728 window,
2729 cx,
2730 );
2731 });
2732 // Same happens in the other direction.
2733 cx.assert_editor_state("here is some textˇwith a space");
2734
2735 cx.set_state("here is some textˇ with a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_next_word_end(
2738 &DeleteToNextWordEnd {
2739 ignore_newlines: false,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 cx.assert_editor_state("here is some textˇwith a space");
2747
2748 cx.set_state("here is some textˇ with a space");
2749 cx.update_editor(|editor, window, cx| {
2750 editor.delete_to_next_word_end(
2751 &DeleteToNextWordEnd {
2752 ignore_newlines: true,
2753 ignore_brackets: false,
2754 },
2755 window,
2756 cx,
2757 );
2758 });
2759 cx.assert_editor_state("here is some textˇwith a space");
2760 cx.update_editor(|editor, window, cx| {
2761 editor.delete_to_previous_word_start(
2762 &DeleteToPreviousWordStart {
2763 ignore_newlines: true,
2764 ignore_brackets: false,
2765 },
2766 window,
2767 cx,
2768 );
2769 });
2770 cx.assert_editor_state("here is some ˇwith a space");
2771 cx.update_editor(|editor, window, cx| {
2772 editor.delete_to_previous_word_start(
2773 &DeleteToPreviousWordStart {
2774 ignore_newlines: true,
2775 ignore_brackets: false,
2776 },
2777 window,
2778 cx,
2779 );
2780 });
2781 // Single whitespaces are removed with the word behind them.
2782 cx.assert_editor_state("here is ˇwith a space");
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_previous_word_start(
2785 &DeleteToPreviousWordStart {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 cx.assert_editor_state("here ˇwith a space");
2794 cx.update_editor(|editor, window, cx| {
2795 editor.delete_to_previous_word_start(
2796 &DeleteToPreviousWordStart {
2797 ignore_newlines: true,
2798 ignore_brackets: false,
2799 },
2800 window,
2801 cx,
2802 );
2803 });
2804 cx.assert_editor_state("ˇwith a space");
2805 cx.update_editor(|editor, window, cx| {
2806 editor.delete_to_previous_word_start(
2807 &DeleteToPreviousWordStart {
2808 ignore_newlines: true,
2809 ignore_brackets: false,
2810 },
2811 window,
2812 cx,
2813 );
2814 });
2815 cx.assert_editor_state("ˇwith a space");
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 // Same happens in the other direction.
2827 cx.assert_editor_state("ˇ a space");
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state("ˇ space");
2839 cx.update_editor(|editor, window, cx| {
2840 editor.delete_to_next_word_end(
2841 &DeleteToNextWordEnd {
2842 ignore_newlines: true,
2843 ignore_brackets: false,
2844 },
2845 window,
2846 cx,
2847 );
2848 });
2849 cx.assert_editor_state("ˇ");
2850 cx.update_editor(|editor, window, cx| {
2851 editor.delete_to_next_word_end(
2852 &DeleteToNextWordEnd {
2853 ignore_newlines: true,
2854 ignore_brackets: false,
2855 },
2856 window,
2857 cx,
2858 );
2859 });
2860 cx.assert_editor_state("ˇ");
2861 cx.update_editor(|editor, window, cx| {
2862 editor.delete_to_previous_word_start(
2863 &DeleteToPreviousWordStart {
2864 ignore_newlines: true,
2865 ignore_brackets: false,
2866 },
2867 window,
2868 cx,
2869 );
2870 });
2871 cx.assert_editor_state("ˇ");
2872}
2873
2874#[gpui::test]
2875async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2876 init_test(cx, |_| {});
2877
2878 let language = Arc::new(
2879 Language::new(
2880 LanguageConfig {
2881 brackets: BracketPairConfig {
2882 pairs: vec![
2883 BracketPair {
2884 start: "\"".to_string(),
2885 end: "\"".to_string(),
2886 close: true,
2887 surround: true,
2888 newline: false,
2889 },
2890 BracketPair {
2891 start: "(".to_string(),
2892 end: ")".to_string(),
2893 close: true,
2894 surround: true,
2895 newline: true,
2896 },
2897 ],
2898 ..BracketPairConfig::default()
2899 },
2900 ..LanguageConfig::default()
2901 },
2902 Some(tree_sitter_rust::LANGUAGE.into()),
2903 )
2904 .with_brackets_query(
2905 r#"
2906 ("(" @open ")" @close)
2907 ("\"" @open "\"" @close)
2908 "#,
2909 )
2910 .unwrap(),
2911 );
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2915
2916 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_previous_word_start(
2919 &DeleteToPreviousWordStart {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 // Deletion stops before brackets if asked to not ignore them.
2928 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2929 cx.update_editor(|editor, window, cx| {
2930 editor.delete_to_previous_word_start(
2931 &DeleteToPreviousWordStart {
2932 ignore_newlines: true,
2933 ignore_brackets: false,
2934 },
2935 window,
2936 cx,
2937 );
2938 });
2939 // Deletion has to remove a single bracket and then stop again.
2940 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2941
2942 cx.update_editor(|editor, window, cx| {
2943 editor.delete_to_previous_word_start(
2944 &DeleteToPreviousWordStart {
2945 ignore_newlines: true,
2946 ignore_brackets: false,
2947 },
2948 window,
2949 cx,
2950 );
2951 });
2952 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2953
2954 cx.update_editor(|editor, window, cx| {
2955 editor.delete_to_previous_word_start(
2956 &DeleteToPreviousWordStart {
2957 ignore_newlines: true,
2958 ignore_brackets: false,
2959 },
2960 window,
2961 cx,
2962 );
2963 });
2964 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2965
2966 cx.update_editor(|editor, window, cx| {
2967 editor.delete_to_previous_word_start(
2968 &DeleteToPreviousWordStart {
2969 ignore_newlines: true,
2970 ignore_brackets: false,
2971 },
2972 window,
2973 cx,
2974 );
2975 });
2976 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2977
2978 cx.update_editor(|editor, window, cx| {
2979 editor.delete_to_next_word_end(
2980 &DeleteToNextWordEnd {
2981 ignore_newlines: true,
2982 ignore_brackets: false,
2983 },
2984 window,
2985 cx,
2986 );
2987 });
2988 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2989 cx.assert_editor_state(r#"ˇ");"#);
2990
2991 cx.update_editor(|editor, window, cx| {
2992 editor.delete_to_next_word_end(
2993 &DeleteToNextWordEnd {
2994 ignore_newlines: true,
2995 ignore_brackets: false,
2996 },
2997 window,
2998 cx,
2999 );
3000 });
3001 cx.assert_editor_state(r#"ˇ"#);
3002
3003 cx.update_editor(|editor, window, cx| {
3004 editor.delete_to_next_word_end(
3005 &DeleteToNextWordEnd {
3006 ignore_newlines: true,
3007 ignore_brackets: false,
3008 },
3009 window,
3010 cx,
3011 );
3012 });
3013 cx.assert_editor_state(r#"ˇ"#);
3014
3015 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
3016 cx.update_editor(|editor, window, cx| {
3017 editor.delete_to_previous_word_start(
3018 &DeleteToPreviousWordStart {
3019 ignore_newlines: true,
3020 ignore_brackets: true,
3021 },
3022 window,
3023 cx,
3024 );
3025 });
3026 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3027}
3028
3029#[gpui::test]
3030fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3031 init_test(cx, |_| {});
3032
3033 let editor = cx.add_window(|window, cx| {
3034 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3035 build_editor(buffer, window, cx)
3036 });
3037 let del_to_prev_word_start = DeleteToPreviousWordStart {
3038 ignore_newlines: false,
3039 ignore_brackets: false,
3040 };
3041 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3042 ignore_newlines: true,
3043 ignore_brackets: false,
3044 };
3045
3046 _ = editor.update(cx, |editor, window, cx| {
3047 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3048 s.select_display_ranges([
3049 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3050 ])
3051 });
3052 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3053 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3054 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3055 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3056 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3057 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3058 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3059 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3060 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3061 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3062 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3063 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3064 });
3065}
3066
3067#[gpui::test]
3068fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3069 init_test(cx, |_| {});
3070
3071 let editor = cx.add_window(|window, cx| {
3072 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3073 build_editor(buffer, window, cx)
3074 });
3075 let del_to_next_word_end = DeleteToNextWordEnd {
3076 ignore_newlines: false,
3077 ignore_brackets: false,
3078 };
3079 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3080 ignore_newlines: true,
3081 ignore_brackets: false,
3082 };
3083
3084 _ = editor.update(cx, |editor, window, cx| {
3085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3086 s.select_display_ranges([
3087 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3088 ])
3089 });
3090 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3091 assert_eq!(
3092 editor.buffer.read(cx).read(cx).text(),
3093 "one\n two\nthree\n four"
3094 );
3095 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3096 assert_eq!(
3097 editor.buffer.read(cx).read(cx).text(),
3098 "\n two\nthree\n four"
3099 );
3100 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3101 assert_eq!(
3102 editor.buffer.read(cx).read(cx).text(),
3103 "two\nthree\n four"
3104 );
3105 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3106 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3107 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3108 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3109 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3110 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3111 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3112 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3113 });
3114}
3115
3116#[gpui::test]
3117fn test_newline(cx: &mut TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let editor = cx.add_window(|window, cx| {
3121 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3122 build_editor(buffer, window, cx)
3123 });
3124
3125 _ = editor.update(cx, |editor, window, cx| {
3126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3127 s.select_display_ranges([
3128 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3129 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3130 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3131 ])
3132 });
3133
3134 editor.newline(&Newline, window, cx);
3135 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3136 });
3137}
3138
3139#[gpui::test]
3140fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3141 init_test(cx, |_| {});
3142
3143 let editor = cx.add_window(|window, cx| {
3144 let buffer = MultiBuffer::build_simple(
3145 "
3146 a
3147 b(
3148 X
3149 )
3150 c(
3151 X
3152 )
3153 "
3154 .unindent()
3155 .as_str(),
3156 cx,
3157 );
3158 let mut editor = build_editor(buffer, window, cx);
3159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3160 s.select_ranges([
3161 Point::new(2, 4)..Point::new(2, 5),
3162 Point::new(5, 4)..Point::new(5, 5),
3163 ])
3164 });
3165 editor
3166 });
3167
3168 _ = editor.update(cx, |editor, window, cx| {
3169 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3170 editor.buffer.update(cx, |buffer, cx| {
3171 buffer.edit(
3172 [
3173 (Point::new(1, 2)..Point::new(3, 0), ""),
3174 (Point::new(4, 2)..Point::new(6, 0), ""),
3175 ],
3176 None,
3177 cx,
3178 );
3179 assert_eq!(
3180 buffer.read(cx).text(),
3181 "
3182 a
3183 b()
3184 c()
3185 "
3186 .unindent()
3187 );
3188 });
3189 assert_eq!(
3190 editor.selections.ranges(&editor.display_snapshot(cx)),
3191 &[
3192 Point::new(1, 2)..Point::new(1, 2),
3193 Point::new(2, 2)..Point::new(2, 2),
3194 ],
3195 );
3196
3197 editor.newline(&Newline, window, cx);
3198 assert_eq!(
3199 editor.text(cx),
3200 "
3201 a
3202 b(
3203 )
3204 c(
3205 )
3206 "
3207 .unindent()
3208 );
3209
3210 // The selections are moved after the inserted newlines
3211 assert_eq!(
3212 editor.selections.ranges(&editor.display_snapshot(cx)),
3213 &[
3214 Point::new(2, 0)..Point::new(2, 0),
3215 Point::new(4, 0)..Point::new(4, 0),
3216 ],
3217 );
3218 });
3219}
3220
3221#[gpui::test]
3222async fn test_newline_above(cx: &mut TestAppContext) {
3223 init_test(cx, |settings| {
3224 settings.defaults.tab_size = NonZeroU32::new(4)
3225 });
3226
3227 let language = Arc::new(
3228 Language::new(
3229 LanguageConfig::default(),
3230 Some(tree_sitter_rust::LANGUAGE.into()),
3231 )
3232 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3233 .unwrap(),
3234 );
3235
3236 let mut cx = EditorTestContext::new(cx).await;
3237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3238 cx.set_state(indoc! {"
3239 const a: ˇA = (
3240 (ˇ
3241 «const_functionˇ»(ˇ),
3242 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3243 )ˇ
3244 ˇ);ˇ
3245 "});
3246
3247 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3248 cx.assert_editor_state(indoc! {"
3249 ˇ
3250 const a: A = (
3251 ˇ
3252 (
3253 ˇ
3254 ˇ
3255 const_function(),
3256 ˇ
3257 ˇ
3258 ˇ
3259 ˇ
3260 something_else,
3261 ˇ
3262 )
3263 ˇ
3264 ˇ
3265 );
3266 "});
3267}
3268
3269#[gpui::test]
3270async fn test_newline_below(cx: &mut TestAppContext) {
3271 init_test(cx, |settings| {
3272 settings.defaults.tab_size = NonZeroU32::new(4)
3273 });
3274
3275 let language = Arc::new(
3276 Language::new(
3277 LanguageConfig::default(),
3278 Some(tree_sitter_rust::LANGUAGE.into()),
3279 )
3280 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3281 .unwrap(),
3282 );
3283
3284 let mut cx = EditorTestContext::new(cx).await;
3285 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3286 cx.set_state(indoc! {"
3287 const a: ˇA = (
3288 (ˇ
3289 «const_functionˇ»(ˇ),
3290 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3291 )ˇ
3292 ˇ);ˇ
3293 "});
3294
3295 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: A = (
3298 ˇ
3299 (
3300 ˇ
3301 const_function(),
3302 ˇ
3303 ˇ
3304 something_else,
3305 ˇ
3306 ˇ
3307 ˇ
3308 ˇ
3309 )
3310 ˇ
3311 );
3312 ˇ
3313 ˇ
3314 "});
3315}
3316
3317#[gpui::test]
3318async fn test_newline_comments(cx: &mut TestAppContext) {
3319 init_test(cx, |settings| {
3320 settings.defaults.tab_size = NonZeroU32::new(4)
3321 });
3322
3323 let language = Arc::new(Language::new(
3324 LanguageConfig {
3325 line_comments: vec!["// ".into()],
3326 ..LanguageConfig::default()
3327 },
3328 None,
3329 ));
3330 {
3331 let mut cx = EditorTestContext::new(cx).await;
3332 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3333 cx.set_state(indoc! {"
3334 // Fooˇ
3335 "});
3336
3337 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3338 cx.assert_editor_state(indoc! {"
3339 // Foo
3340 // ˇ
3341 "});
3342 // Ensure that we add comment prefix when existing line contains space
3343 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3344 cx.assert_editor_state(
3345 indoc! {"
3346 // Foo
3347 //s
3348 // ˇ
3349 "}
3350 .replace("s", " ") // s is used as space placeholder to prevent format on save
3351 .as_str(),
3352 );
3353 // Ensure that we add comment prefix when existing line does not contain space
3354 cx.set_state(indoc! {"
3355 // Foo
3356 //ˇ
3357 "});
3358 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3359 cx.assert_editor_state(indoc! {"
3360 // Foo
3361 //
3362 // ˇ
3363 "});
3364 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3365 cx.set_state(indoc! {"
3366 ˇ// Foo
3367 "});
3368 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3369 cx.assert_editor_state(indoc! {"
3370
3371 ˇ// Foo
3372 "});
3373 }
3374 // Ensure that comment continuations can be disabled.
3375 update_test_language_settings(cx, |settings| {
3376 settings.defaults.extend_comment_on_newline = Some(false);
3377 });
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.set_state(indoc! {"
3380 // Fooˇ
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 // Foo
3385 ˇ
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4)
3393 });
3394
3395 let language = Arc::new(Language::new(
3396 LanguageConfig {
3397 line_comments: vec!["// ".into(), "/// ".into()],
3398 ..LanguageConfig::default()
3399 },
3400 None,
3401 ));
3402 {
3403 let mut cx = EditorTestContext::new(cx).await;
3404 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3405 cx.set_state(indoc! {"
3406 //ˇ
3407 "});
3408 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 //
3411 // ˇ
3412 "});
3413
3414 cx.set_state(indoc! {"
3415 ///ˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 ///
3420 /// ˇ
3421 "});
3422 }
3423}
3424
3425#[gpui::test]
3426async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3427 init_test(cx, |settings| {
3428 settings.defaults.tab_size = NonZeroU32::new(4)
3429 });
3430
3431 let language = Arc::new(
3432 Language::new(
3433 LanguageConfig {
3434 documentation_comment: Some(language::BlockCommentConfig {
3435 start: "/**".into(),
3436 end: "*/".into(),
3437 prefix: "* ".into(),
3438 tab_size: 1,
3439 }),
3440
3441 ..LanguageConfig::default()
3442 },
3443 Some(tree_sitter_rust::LANGUAGE.into()),
3444 )
3445 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3446 .unwrap(),
3447 );
3448
3449 {
3450 let mut cx = EditorTestContext::new(cx).await;
3451 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3452 cx.set_state(indoc! {"
3453 /**ˇ
3454 "});
3455
3456 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3457 cx.assert_editor_state(indoc! {"
3458 /**
3459 * ˇ
3460 "});
3461 // Ensure that if cursor is before the comment start,
3462 // we do not actually insert a comment prefix.
3463 cx.set_state(indoc! {"
3464 ˇ/**
3465 "});
3466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3467 cx.assert_editor_state(indoc! {"
3468
3469 ˇ/**
3470 "});
3471 // Ensure that if cursor is between it doesn't add comment prefix.
3472 cx.set_state(indoc! {"
3473 /*ˇ*
3474 "});
3475 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 /*
3478 ˇ*
3479 "});
3480 // Ensure that if suffix exists on same line after cursor it adds new line.
3481 cx.set_state(indoc! {"
3482 /**ˇ*/
3483 "});
3484 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3485 cx.assert_editor_state(indoc! {"
3486 /**
3487 * ˇ
3488 */
3489 "});
3490 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3491 cx.set_state(indoc! {"
3492 /**ˇ */
3493 "});
3494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 /**
3497 * ˇ
3498 */
3499 "});
3500 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3501 cx.set_state(indoc! {"
3502 /** ˇ*/
3503 "});
3504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3505 cx.assert_editor_state(
3506 indoc! {"
3507 /**s
3508 * ˇ
3509 */
3510 "}
3511 .replace("s", " ") // s is used as space placeholder to prevent format on save
3512 .as_str(),
3513 );
3514 // Ensure that delimiter space is preserved when newline on already
3515 // spaced delimiter.
3516 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3517 cx.assert_editor_state(
3518 indoc! {"
3519 /**s
3520 *s
3521 * ˇ
3522 */
3523 "}
3524 .replace("s", " ") // s is used as space placeholder to prevent format on save
3525 .as_str(),
3526 );
3527 // Ensure that delimiter space is preserved when space is not
3528 // on existing delimiter.
3529 cx.set_state(indoc! {"
3530 /**
3531 *ˇ
3532 */
3533 "});
3534 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3535 cx.assert_editor_state(indoc! {"
3536 /**
3537 *
3538 * ˇ
3539 */
3540 "});
3541 // Ensure that if suffix exists on same line after cursor it
3542 // doesn't add extra new line if prefix is not on same line.
3543 cx.set_state(indoc! {"
3544 /**
3545 ˇ*/
3546 "});
3547 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3548 cx.assert_editor_state(indoc! {"
3549 /**
3550
3551 ˇ*/
3552 "});
3553 // Ensure that it detects suffix after existing prefix.
3554 cx.set_state(indoc! {"
3555 /**ˇ/
3556 "});
3557 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3558 cx.assert_editor_state(indoc! {"
3559 /**
3560 ˇ/
3561 "});
3562 // Ensure that if suffix exists on same line before
3563 // cursor it does not add comment prefix.
3564 cx.set_state(indoc! {"
3565 /** */ˇ
3566 "});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 /** */
3570 ˇ
3571 "});
3572 // Ensure that if suffix exists on same line before
3573 // cursor it does not add comment prefix.
3574 cx.set_state(indoc! {"
3575 /**
3576 *
3577 */ˇ
3578 "});
3579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 /**
3582 *
3583 */
3584 ˇ
3585 "});
3586
3587 // Ensure that inline comment followed by code
3588 // doesn't add comment prefix on newline
3589 cx.set_state(indoc! {"
3590 /** */ textˇ
3591 "});
3592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 /** */ text
3595 ˇ
3596 "});
3597
3598 // Ensure that text after comment end tag
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /**
3602 *
3603 */ˇtext
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /**
3608 *
3609 */
3610 ˇtext
3611 "});
3612
3613 // Ensure if not comment block it doesn't
3614 // add comment prefix on newline
3615 cx.set_state(indoc! {"
3616 * textˇ
3617 "});
3618 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3619 cx.assert_editor_state(indoc! {"
3620 * text
3621 ˇ
3622 "});
3623 }
3624 // Ensure that comment continuations can be disabled.
3625 update_test_language_settings(cx, |settings| {
3626 settings.defaults.extend_comment_on_newline = Some(false);
3627 });
3628 let mut cx = EditorTestContext::new(cx).await;
3629 cx.set_state(indoc! {"
3630 /**ˇ
3631 "});
3632 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 /**
3635 ˇ
3636 "});
3637}
3638
3639#[gpui::test]
3640async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3641 init_test(cx, |settings| {
3642 settings.defaults.tab_size = NonZeroU32::new(4)
3643 });
3644
3645 let lua_language = Arc::new(Language::new(
3646 LanguageConfig {
3647 line_comments: vec!["--".into()],
3648 block_comment: Some(language::BlockCommentConfig {
3649 start: "--[[".into(),
3650 prefix: "".into(),
3651 end: "]]".into(),
3652 tab_size: 0,
3653 }),
3654 ..LanguageConfig::default()
3655 },
3656 None,
3657 ));
3658
3659 let mut cx = EditorTestContext::new(cx).await;
3660 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3661
3662 // Line with line comment should extend
3663 cx.set_state(indoc! {"
3664 --ˇ
3665 "});
3666 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3667 cx.assert_editor_state(indoc! {"
3668 --
3669 --ˇ
3670 "});
3671
3672 // Line with block comment that matches line comment should not extend
3673 cx.set_state(indoc! {"
3674 --[[ˇ
3675 "});
3676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 --[[
3679 ˇ
3680 "});
3681}
3682
3683#[gpui::test]
3684fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3685 init_test(cx, |_| {});
3686
3687 let editor = cx.add_window(|window, cx| {
3688 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3689 let mut editor = build_editor(buffer, window, cx);
3690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3691 s.select_ranges([3..4, 11..12, 19..20])
3692 });
3693 editor
3694 });
3695
3696 _ = editor.update(cx, |editor, window, cx| {
3697 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3698 editor.buffer.update(cx, |buffer, cx| {
3699 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3700 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3701 });
3702 assert_eq!(
3703 editor.selections.ranges(&editor.display_snapshot(cx)),
3704 &[2..2, 7..7, 12..12],
3705 );
3706
3707 editor.insert("Z", window, cx);
3708 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3709
3710 // The selections are moved after the inserted characters
3711 assert_eq!(
3712 editor.selections.ranges(&editor.display_snapshot(cx)),
3713 &[3..3, 9..9, 15..15],
3714 );
3715 });
3716}
3717
3718#[gpui::test]
3719async fn test_tab(cx: &mut TestAppContext) {
3720 init_test(cx, |settings| {
3721 settings.defaults.tab_size = NonZeroU32::new(3)
3722 });
3723
3724 let mut cx = EditorTestContext::new(cx).await;
3725 cx.set_state(indoc! {"
3726 ˇabˇc
3727 ˇ🏀ˇ🏀ˇefg
3728 dˇ
3729 "});
3730 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3731 cx.assert_editor_state(indoc! {"
3732 ˇab ˇc
3733 ˇ🏀 ˇ🏀 ˇefg
3734 d ˇ
3735 "});
3736
3737 cx.set_state(indoc! {"
3738 a
3739 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 a
3744 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3745 "});
3746}
3747
3748#[gpui::test]
3749async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3750 init_test(cx, |_| {});
3751
3752 let mut cx = EditorTestContext::new(cx).await;
3753 let language = Arc::new(
3754 Language::new(
3755 LanguageConfig::default(),
3756 Some(tree_sitter_rust::LANGUAGE.into()),
3757 )
3758 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3759 .unwrap(),
3760 );
3761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3762
3763 // test when all cursors are not at suggested indent
3764 // then simply move to their suggested indent location
3765 cx.set_state(indoc! {"
3766 const a: B = (
3767 c(
3768 ˇ
3769 ˇ )
3770 );
3771 "});
3772 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3773 cx.assert_editor_state(indoc! {"
3774 const a: B = (
3775 c(
3776 ˇ
3777 ˇ)
3778 );
3779 "});
3780
3781 // test cursor already at suggested indent not moving when
3782 // other cursors are yet to reach their suggested indents
3783 cx.set_state(indoc! {"
3784 ˇ
3785 const a: B = (
3786 c(
3787 d(
3788 ˇ
3789 )
3790 ˇ
3791 ˇ )
3792 );
3793 "});
3794 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3795 cx.assert_editor_state(indoc! {"
3796 ˇ
3797 const a: B = (
3798 c(
3799 d(
3800 ˇ
3801 )
3802 ˇ
3803 ˇ)
3804 );
3805 "});
3806 // test when all cursors are at suggested indent then tab is inserted
3807 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 ˇ
3810 const a: B = (
3811 c(
3812 d(
3813 ˇ
3814 )
3815 ˇ
3816 ˇ)
3817 );
3818 "});
3819
3820 // test when current indent is less than suggested indent,
3821 // we adjust line to match suggested indent and move cursor to it
3822 //
3823 // when no other cursor is at word boundary, all of them should move
3824 cx.set_state(indoc! {"
3825 const a: B = (
3826 c(
3827 d(
3828 ˇ
3829 ˇ )
3830 ˇ )
3831 );
3832 "});
3833 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3834 cx.assert_editor_state(indoc! {"
3835 const a: B = (
3836 c(
3837 d(
3838 ˇ
3839 ˇ)
3840 ˇ)
3841 );
3842 "});
3843
3844 // test when current indent is less than suggested indent,
3845 // we adjust line to match suggested indent and move cursor to it
3846 //
3847 // when some other cursor is at word boundary, it should not move
3848 cx.set_state(indoc! {"
3849 const a: B = (
3850 c(
3851 d(
3852 ˇ
3853 ˇ )
3854 ˇ)
3855 );
3856 "});
3857 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3858 cx.assert_editor_state(indoc! {"
3859 const a: B = (
3860 c(
3861 d(
3862 ˇ
3863 ˇ)
3864 ˇ)
3865 );
3866 "});
3867
3868 // test when current indent is more than suggested indent,
3869 // we just move cursor to current indent instead of suggested indent
3870 //
3871 // when no other cursor is at word boundary, all of them should move
3872 cx.set_state(indoc! {"
3873 const a: B = (
3874 c(
3875 d(
3876 ˇ
3877 ˇ )
3878 ˇ )
3879 );
3880 "});
3881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3882 cx.assert_editor_state(indoc! {"
3883 const a: B = (
3884 c(
3885 d(
3886 ˇ
3887 ˇ)
3888 ˇ)
3889 );
3890 "});
3891 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3892 cx.assert_editor_state(indoc! {"
3893 const a: B = (
3894 c(
3895 d(
3896 ˇ
3897 ˇ)
3898 ˇ)
3899 );
3900 "});
3901
3902 // test when current indent is more than suggested indent,
3903 // we just move cursor to current indent instead of suggested indent
3904 //
3905 // when some other cursor is at word boundary, it doesn't move
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 d(
3910 ˇ
3911 ˇ )
3912 ˇ)
3913 );
3914 "});
3915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3916 cx.assert_editor_state(indoc! {"
3917 const a: B = (
3918 c(
3919 d(
3920 ˇ
3921 ˇ)
3922 ˇ)
3923 );
3924 "});
3925
3926 // handle auto-indent when there are multiple cursors on the same line
3927 cx.set_state(indoc! {"
3928 const a: B = (
3929 c(
3930 ˇ ˇ
3931 ˇ )
3932 );
3933 "});
3934 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3935 cx.assert_editor_state(indoc! {"
3936 const a: B = (
3937 c(
3938 ˇ
3939 ˇ)
3940 );
3941 "});
3942}
3943
3944#[gpui::test]
3945async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3946 init_test(cx, |settings| {
3947 settings.defaults.tab_size = NonZeroU32::new(3)
3948 });
3949
3950 let mut cx = EditorTestContext::new(cx).await;
3951 cx.set_state(indoc! {"
3952 ˇ
3953 \t ˇ
3954 \t ˇ
3955 \t ˇ
3956 \t \t\t \t \t\t \t\t \t \t ˇ
3957 "});
3958
3959 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 ˇ
3962 \t ˇ
3963 \t ˇ
3964 \t ˇ
3965 \t \t\t \t \t\t \t\t \t \t ˇ
3966 "});
3967}
3968
3969#[gpui::test]
3970async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3971 init_test(cx, |settings| {
3972 settings.defaults.tab_size = NonZeroU32::new(4)
3973 });
3974
3975 let language = Arc::new(
3976 Language::new(
3977 LanguageConfig::default(),
3978 Some(tree_sitter_rust::LANGUAGE.into()),
3979 )
3980 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3981 .unwrap(),
3982 );
3983
3984 let mut cx = EditorTestContext::new(cx).await;
3985 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3986 cx.set_state(indoc! {"
3987 fn a() {
3988 if b {
3989 \t ˇc
3990 }
3991 }
3992 "});
3993
3994 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3995 cx.assert_editor_state(indoc! {"
3996 fn a() {
3997 if b {
3998 ˇc
3999 }
4000 }
4001 "});
4002}
4003
4004#[gpui::test]
4005async fn test_indent_outdent(cx: &mut TestAppContext) {
4006 init_test(cx, |settings| {
4007 settings.defaults.tab_size = NonZeroU32::new(4);
4008 });
4009
4010 let mut cx = EditorTestContext::new(cx).await;
4011
4012 cx.set_state(indoc! {"
4013 «oneˇ» «twoˇ»
4014 three
4015 four
4016 "});
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 «oneˇ» «twoˇ»
4020 three
4021 four
4022 "});
4023
4024 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4025 cx.assert_editor_state(indoc! {"
4026 «oneˇ» «twoˇ»
4027 three
4028 four
4029 "});
4030
4031 // select across line ending
4032 cx.set_state(indoc! {"
4033 one two
4034 t«hree
4035 ˇ» four
4036 "});
4037 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 t«hree
4041 ˇ» four
4042 "});
4043
4044 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4045 cx.assert_editor_state(indoc! {"
4046 one two
4047 t«hree
4048 ˇ» four
4049 "});
4050
4051 // Ensure that indenting/outdenting works when the cursor is at column 0.
4052 cx.set_state(indoc! {"
4053 one two
4054 ˇthree
4055 four
4056 "});
4057 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4058 cx.assert_editor_state(indoc! {"
4059 one two
4060 ˇthree
4061 four
4062 "});
4063
4064 cx.set_state(indoc! {"
4065 one two
4066 ˇ three
4067 four
4068 "});
4069 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4070 cx.assert_editor_state(indoc! {"
4071 one two
4072 ˇthree
4073 four
4074 "});
4075}
4076
4077#[gpui::test]
4078async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4079 // This is a regression test for issue #33761
4080 init_test(cx, |_| {});
4081
4082 let mut cx = EditorTestContext::new(cx).await;
4083 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4085
4086 cx.set_state(
4087 r#"ˇ# ingress:
4088ˇ# api:
4089ˇ# enabled: false
4090ˇ# pathType: Prefix
4091ˇ# console:
4092ˇ# enabled: false
4093ˇ# pathType: Prefix
4094"#,
4095 );
4096
4097 // Press tab to indent all lines
4098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4099
4100 cx.assert_editor_state(
4101 r#" ˇ# ingress:
4102 ˇ# api:
4103 ˇ# enabled: false
4104 ˇ# pathType: Prefix
4105 ˇ# console:
4106 ˇ# enabled: false
4107 ˇ# pathType: Prefix
4108"#,
4109 );
4110}
4111
4112#[gpui::test]
4113async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4114 // This is a test to make sure our fix for issue #33761 didn't break anything
4115 init_test(cx, |_| {});
4116
4117 let mut cx = EditorTestContext::new(cx).await;
4118 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4119 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4120
4121 cx.set_state(
4122 r#"ˇingress:
4123ˇ api:
4124ˇ enabled: false
4125ˇ pathType: Prefix
4126"#,
4127 );
4128
4129 // Press tab to indent all lines
4130 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4131
4132 cx.assert_editor_state(
4133 r#"ˇingress:
4134 ˇapi:
4135 ˇenabled: false
4136 ˇpathType: Prefix
4137"#,
4138 );
4139}
4140
4141#[gpui::test]
4142async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4143 init_test(cx, |settings| {
4144 settings.defaults.hard_tabs = Some(true);
4145 });
4146
4147 let mut cx = EditorTestContext::new(cx).await;
4148
4149 // select two ranges on one line
4150 cx.set_state(indoc! {"
4151 «oneˇ» «twoˇ»
4152 three
4153 four
4154 "});
4155 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4156 cx.assert_editor_state(indoc! {"
4157 \t«oneˇ» «twoˇ»
4158 three
4159 four
4160 "});
4161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4162 cx.assert_editor_state(indoc! {"
4163 \t\t«oneˇ» «twoˇ»
4164 three
4165 four
4166 "});
4167 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4168 cx.assert_editor_state(indoc! {"
4169 \t«oneˇ» «twoˇ»
4170 three
4171 four
4172 "});
4173 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4174 cx.assert_editor_state(indoc! {"
4175 «oneˇ» «twoˇ»
4176 three
4177 four
4178 "});
4179
4180 // select across a line ending
4181 cx.set_state(indoc! {"
4182 one two
4183 t«hree
4184 ˇ»four
4185 "});
4186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4187 cx.assert_editor_state(indoc! {"
4188 one two
4189 \tt«hree
4190 ˇ»four
4191 "});
4192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4193 cx.assert_editor_state(indoc! {"
4194 one two
4195 \t\tt«hree
4196 ˇ»four
4197 "});
4198 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 \tt«hree
4202 ˇ»four
4203 "});
4204 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4205 cx.assert_editor_state(indoc! {"
4206 one two
4207 t«hree
4208 ˇ»four
4209 "});
4210
4211 // Ensure that indenting/outdenting works when the cursor is at column 0.
4212 cx.set_state(indoc! {"
4213 one two
4214 ˇthree
4215 four
4216 "});
4217 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 one two
4220 ˇthree
4221 four
4222 "});
4223 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4224 cx.assert_editor_state(indoc! {"
4225 one two
4226 \tˇthree
4227 four
4228 "});
4229 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4230 cx.assert_editor_state(indoc! {"
4231 one two
4232 ˇthree
4233 four
4234 "});
4235}
4236
4237#[gpui::test]
4238fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4239 init_test(cx, |settings| {
4240 settings.languages.0.extend([
4241 (
4242 "TOML".into(),
4243 LanguageSettingsContent {
4244 tab_size: NonZeroU32::new(2),
4245 ..Default::default()
4246 },
4247 ),
4248 (
4249 "Rust".into(),
4250 LanguageSettingsContent {
4251 tab_size: NonZeroU32::new(4),
4252 ..Default::default()
4253 },
4254 ),
4255 ]);
4256 });
4257
4258 let toml_language = Arc::new(Language::new(
4259 LanguageConfig {
4260 name: "TOML".into(),
4261 ..Default::default()
4262 },
4263 None,
4264 ));
4265 let rust_language = Arc::new(Language::new(
4266 LanguageConfig {
4267 name: "Rust".into(),
4268 ..Default::default()
4269 },
4270 None,
4271 ));
4272
4273 let toml_buffer =
4274 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4275 let rust_buffer =
4276 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4277 let multibuffer = cx.new(|cx| {
4278 let mut multibuffer = MultiBuffer::new(ReadWrite);
4279 multibuffer.push_excerpts(
4280 toml_buffer.clone(),
4281 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4282 cx,
4283 );
4284 multibuffer.push_excerpts(
4285 rust_buffer.clone(),
4286 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4287 cx,
4288 );
4289 multibuffer
4290 });
4291
4292 cx.add_window(|window, cx| {
4293 let mut editor = build_editor(multibuffer, window, cx);
4294
4295 assert_eq!(
4296 editor.text(cx),
4297 indoc! {"
4298 a = 1
4299 b = 2
4300
4301 const c: usize = 3;
4302 "}
4303 );
4304
4305 select_ranges(
4306 &mut editor,
4307 indoc! {"
4308 «aˇ» = 1
4309 b = 2
4310
4311 «const c:ˇ» usize = 3;
4312 "},
4313 window,
4314 cx,
4315 );
4316
4317 editor.tab(&Tab, window, cx);
4318 assert_text_with_selections(
4319 &mut editor,
4320 indoc! {"
4321 «aˇ» = 1
4322 b = 2
4323
4324 «const c:ˇ» usize = 3;
4325 "},
4326 cx,
4327 );
4328 editor.backtab(&Backtab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339
4340 editor
4341 });
4342}
4343
4344#[gpui::test]
4345async fn test_backspace(cx: &mut TestAppContext) {
4346 init_test(cx, |_| {});
4347
4348 let mut cx = EditorTestContext::new(cx).await;
4349
4350 // Basic backspace
4351 cx.set_state(indoc! {"
4352 onˇe two three
4353 fou«rˇ» five six
4354 seven «ˇeight nine
4355 »ten
4356 "});
4357 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 oˇe two three
4360 fouˇ five six
4361 seven ˇten
4362 "});
4363
4364 // Test backspace inside and around indents
4365 cx.set_state(indoc! {"
4366 zero
4367 ˇone
4368 ˇtwo
4369 ˇ ˇ ˇ three
4370 ˇ ˇ four
4371 "});
4372 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4373 cx.assert_editor_state(indoc! {"
4374 zero
4375 ˇone
4376 ˇtwo
4377 ˇ threeˇ four
4378 "});
4379}
4380
4381#[gpui::test]
4382async fn test_delete(cx: &mut TestAppContext) {
4383 init_test(cx, |_| {});
4384
4385 let mut cx = EditorTestContext::new(cx).await;
4386 cx.set_state(indoc! {"
4387 onˇe two three
4388 fou«rˇ» five six
4389 seven «ˇeight nine
4390 »ten
4391 "});
4392 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4393 cx.assert_editor_state(indoc! {"
4394 onˇ two three
4395 fouˇ five six
4396 seven ˇten
4397 "});
4398}
4399
4400#[gpui::test]
4401fn test_delete_line(cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let editor = cx.add_window(|window, cx| {
4405 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4406 build_editor(buffer, window, cx)
4407 });
4408 _ = editor.update(cx, |editor, window, cx| {
4409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4410 s.select_display_ranges([
4411 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4412 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4413 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4414 ])
4415 });
4416 editor.delete_line(&DeleteLine, window, cx);
4417 assert_eq!(editor.display_text(cx), "ghi");
4418 assert_eq!(
4419 editor.selections.display_ranges(cx),
4420 vec![
4421 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 ]
4424 );
4425 });
4426
4427 let editor = cx.add_window(|window, cx| {
4428 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4429 build_editor(buffer, window, cx)
4430 });
4431 _ = editor.update(cx, |editor, window, cx| {
4432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4433 s.select_display_ranges([
4434 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4435 ])
4436 });
4437 editor.delete_line(&DeleteLine, window, cx);
4438 assert_eq!(editor.display_text(cx), "ghi\n");
4439 assert_eq!(
4440 editor.selections.display_ranges(cx),
4441 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4442 );
4443 });
4444
4445 let editor = cx.add_window(|window, cx| {
4446 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4447 build_editor(buffer, window, cx)
4448 });
4449 _ = editor.update(cx, |editor, window, cx| {
4450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4451 s.select_display_ranges([
4452 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4453 ])
4454 });
4455 editor.delete_line(&DeleteLine, window, cx);
4456 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4457 assert_eq!(
4458 editor.selections.display_ranges(cx),
4459 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4460 );
4461 });
4462}
4463
4464#[gpui::test]
4465fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4466 init_test(cx, |_| {});
4467
4468 cx.add_window(|window, cx| {
4469 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4470 let mut editor = build_editor(buffer.clone(), window, cx);
4471 let buffer = buffer.read(cx).as_singleton().unwrap();
4472
4473 assert_eq!(
4474 editor
4475 .selections
4476 .ranges::<Point>(&editor.display_snapshot(cx)),
4477 &[Point::new(0, 0)..Point::new(0, 0)]
4478 );
4479
4480 // When on single line, replace newline at end by space
4481 editor.join_lines(&JoinLines, window, cx);
4482 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4483 assert_eq!(
4484 editor
4485 .selections
4486 .ranges::<Point>(&editor.display_snapshot(cx)),
4487 &[Point::new(0, 3)..Point::new(0, 3)]
4488 );
4489
4490 // When multiple lines are selected, remove newlines that are spanned by the selection
4491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4492 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4493 });
4494 editor.join_lines(&JoinLines, window, cx);
4495 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4496 assert_eq!(
4497 editor
4498 .selections
4499 .ranges::<Point>(&editor.display_snapshot(cx)),
4500 &[Point::new(0, 11)..Point::new(0, 11)]
4501 );
4502
4503 // Undo should be transactional
4504 editor.undo(&Undo, window, cx);
4505 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4506 assert_eq!(
4507 editor
4508 .selections
4509 .ranges::<Point>(&editor.display_snapshot(cx)),
4510 &[Point::new(0, 5)..Point::new(2, 2)]
4511 );
4512
4513 // When joining an empty line don't insert a space
4514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4515 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4516 });
4517 editor.join_lines(&JoinLines, window, cx);
4518 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4519 assert_eq!(
4520 editor
4521 .selections
4522 .ranges::<Point>(&editor.display_snapshot(cx)),
4523 [Point::new(2, 3)..Point::new(2, 3)]
4524 );
4525
4526 // We can remove trailing newlines
4527 editor.join_lines(&JoinLines, window, cx);
4528 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 [Point::new(2, 3)..Point::new(2, 3)]
4534 );
4535
4536 // We don't blow up on the last line
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 [Point::new(2, 3)..Point::new(2, 3)]
4544 );
4545
4546 // reset to test indentation
4547 editor.buffer.update(cx, |buffer, cx| {
4548 buffer.edit(
4549 [
4550 (Point::new(1, 0)..Point::new(1, 2), " "),
4551 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4552 ],
4553 None,
4554 cx,
4555 )
4556 });
4557
4558 // We remove any leading spaces
4559 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4560 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4561 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4562 });
4563 editor.join_lines(&JoinLines, window, cx);
4564 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4565
4566 // We don't insert a space for a line containing only spaces
4567 editor.join_lines(&JoinLines, window, cx);
4568 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4569
4570 // We ignore any leading tabs
4571 editor.join_lines(&JoinLines, window, cx);
4572 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4573
4574 editor
4575 });
4576}
4577
4578#[gpui::test]
4579fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4580 init_test(cx, |_| {});
4581
4582 cx.add_window(|window, cx| {
4583 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4584 let mut editor = build_editor(buffer.clone(), window, cx);
4585 let buffer = buffer.read(cx).as_singleton().unwrap();
4586
4587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4588 s.select_ranges([
4589 Point::new(0, 2)..Point::new(1, 1),
4590 Point::new(1, 2)..Point::new(1, 2),
4591 Point::new(3, 1)..Point::new(3, 2),
4592 ])
4593 });
4594
4595 editor.join_lines(&JoinLines, window, cx);
4596 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4597
4598 assert_eq!(
4599 editor
4600 .selections
4601 .ranges::<Point>(&editor.display_snapshot(cx)),
4602 [
4603 Point::new(0, 7)..Point::new(0, 7),
4604 Point::new(1, 3)..Point::new(1, 3)
4605 ]
4606 );
4607 editor
4608 });
4609}
4610
4611#[gpui::test]
4612async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4613 init_test(cx, |_| {});
4614
4615 let mut cx = EditorTestContext::new(cx).await;
4616
4617 let diff_base = r#"
4618 Line 0
4619 Line 1
4620 Line 2
4621 Line 3
4622 "#
4623 .unindent();
4624
4625 cx.set_state(
4626 &r#"
4627 ˇLine 0
4628 Line 1
4629 Line 2
4630 Line 3
4631 "#
4632 .unindent(),
4633 );
4634
4635 cx.set_head_text(&diff_base);
4636 executor.run_until_parked();
4637
4638 // Join lines
4639 cx.update_editor(|editor, window, cx| {
4640 editor.join_lines(&JoinLines, window, cx);
4641 });
4642 executor.run_until_parked();
4643
4644 cx.assert_editor_state(
4645 &r#"
4646 Line 0ˇ Line 1
4647 Line 2
4648 Line 3
4649 "#
4650 .unindent(),
4651 );
4652 // Join again
4653 cx.update_editor(|editor, window, cx| {
4654 editor.join_lines(&JoinLines, window, cx);
4655 });
4656 executor.run_until_parked();
4657
4658 cx.assert_editor_state(
4659 &r#"
4660 Line 0 Line 1ˇ Line 2
4661 Line 3
4662 "#
4663 .unindent(),
4664 );
4665}
4666
4667#[gpui::test]
4668async fn test_custom_newlines_cause_no_false_positive_diffs(
4669 executor: BackgroundExecutor,
4670 cx: &mut TestAppContext,
4671) {
4672 init_test(cx, |_| {});
4673 let mut cx = EditorTestContext::new(cx).await;
4674 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4675 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4676 executor.run_until_parked();
4677
4678 cx.update_editor(|editor, window, cx| {
4679 let snapshot = editor.snapshot(window, cx);
4680 assert_eq!(
4681 snapshot
4682 .buffer_snapshot()
4683 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4684 .collect::<Vec<_>>(),
4685 Vec::new(),
4686 "Should not have any diffs for files with custom newlines"
4687 );
4688 });
4689}
4690
4691#[gpui::test]
4692async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4693 init_test(cx, |_| {});
4694
4695 let mut cx = EditorTestContext::new(cx).await;
4696
4697 // Test sort_lines_case_insensitive()
4698 cx.set_state(indoc! {"
4699 «z
4700 y
4701 x
4702 Z
4703 Y
4704 Xˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| {
4707 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4708 });
4709 cx.assert_editor_state(indoc! {"
4710 «x
4711 X
4712 y
4713 Y
4714 z
4715 Zˇ»
4716 "});
4717
4718 // Test sort_lines_by_length()
4719 //
4720 // Demonstrates:
4721 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4722 // - sort is stable
4723 cx.set_state(indoc! {"
4724 «123
4725 æ
4726 12
4727 ∞
4728 1
4729 æˇ»
4730 "});
4731 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4732 cx.assert_editor_state(indoc! {"
4733 «æ
4734 ∞
4735 1
4736 æ
4737 12
4738 123ˇ»
4739 "});
4740
4741 // Test reverse_lines()
4742 cx.set_state(indoc! {"
4743 «5
4744 4
4745 3
4746 2
4747 1ˇ»
4748 "});
4749 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4750 cx.assert_editor_state(indoc! {"
4751 «1
4752 2
4753 3
4754 4
4755 5ˇ»
4756 "});
4757
4758 // Skip testing shuffle_line()
4759
4760 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4761 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4762
4763 // Don't manipulate when cursor is on single line, but expand the selection
4764 cx.set_state(indoc! {"
4765 ddˇdd
4766 ccc
4767 bb
4768 a
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «ddddˇ»
4775 ccc
4776 bb
4777 a
4778 "});
4779
4780 // Basic manipulate case
4781 // Start selection moves to column 0
4782 // End of selection shrinks to fit shorter line
4783 cx.set_state(indoc! {"
4784 dd«d
4785 ccc
4786 bb
4787 aaaaaˇ»
4788 "});
4789 cx.update_editor(|e, window, cx| {
4790 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4791 });
4792 cx.assert_editor_state(indoc! {"
4793 «aaaaa
4794 bb
4795 ccc
4796 dddˇ»
4797 "});
4798
4799 // Manipulate case with newlines
4800 cx.set_state(indoc! {"
4801 dd«d
4802 ccc
4803
4804 bb
4805 aaaaa
4806
4807 ˇ»
4808 "});
4809 cx.update_editor(|e, window, cx| {
4810 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4811 });
4812 cx.assert_editor_state(indoc! {"
4813 «
4814
4815 aaaaa
4816 bb
4817 ccc
4818 dddˇ»
4819
4820 "});
4821
4822 // Adding new line
4823 cx.set_state(indoc! {"
4824 aa«a
4825 bbˇ»b
4826 "});
4827 cx.update_editor(|e, window, cx| {
4828 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4829 });
4830 cx.assert_editor_state(indoc! {"
4831 «aaa
4832 bbb
4833 added_lineˇ»
4834 "});
4835
4836 // Removing line
4837 cx.set_state(indoc! {"
4838 aa«a
4839 bbbˇ»
4840 "});
4841 cx.update_editor(|e, window, cx| {
4842 e.manipulate_immutable_lines(window, cx, |lines| {
4843 lines.pop();
4844 })
4845 });
4846 cx.assert_editor_state(indoc! {"
4847 «aaaˇ»
4848 "});
4849
4850 // Removing all lines
4851 cx.set_state(indoc! {"
4852 aa«a
4853 bbbˇ»
4854 "});
4855 cx.update_editor(|e, window, cx| {
4856 e.manipulate_immutable_lines(window, cx, |lines| {
4857 lines.drain(..);
4858 })
4859 });
4860 cx.assert_editor_state(indoc! {"
4861 ˇ
4862 "});
4863}
4864
4865#[gpui::test]
4866async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4867 init_test(cx, |_| {});
4868
4869 let mut cx = EditorTestContext::new(cx).await;
4870
4871 // Consider continuous selection as single selection
4872 cx.set_state(indoc! {"
4873 Aaa«aa
4874 cˇ»c«c
4875 bb
4876 aaaˇ»aa
4877 "});
4878 cx.update_editor(|e, window, cx| {
4879 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4880 });
4881 cx.assert_editor_state(indoc! {"
4882 «Aaaaa
4883 ccc
4884 bb
4885 aaaaaˇ»
4886 "});
4887
4888 cx.set_state(indoc! {"
4889 Aaa«aa
4890 cˇ»c«c
4891 bb
4892 aaaˇ»aa
4893 "});
4894 cx.update_editor(|e, window, cx| {
4895 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4896 });
4897 cx.assert_editor_state(indoc! {"
4898 «Aaaaa
4899 ccc
4900 bbˇ»
4901 "});
4902
4903 // Consider non continuous selection as distinct dedup operations
4904 cx.set_state(indoc! {"
4905 «aaaaa
4906 bb
4907 aaaaa
4908 aaaaaˇ»
4909
4910 aaa«aaˇ»
4911 "});
4912 cx.update_editor(|e, window, cx| {
4913 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4914 });
4915 cx.assert_editor_state(indoc! {"
4916 «aaaaa
4917 bbˇ»
4918
4919 «aaaaaˇ»
4920 "});
4921}
4922
4923#[gpui::test]
4924async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4925 init_test(cx, |_| {});
4926
4927 let mut cx = EditorTestContext::new(cx).await;
4928
4929 cx.set_state(indoc! {"
4930 «Aaa
4931 aAa
4932 Aaaˇ»
4933 "});
4934 cx.update_editor(|e, window, cx| {
4935 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4936 });
4937 cx.assert_editor_state(indoc! {"
4938 «Aaa
4939 aAaˇ»
4940 "});
4941
4942 cx.set_state(indoc! {"
4943 «Aaa
4944 aAa
4945 aaAˇ»
4946 "});
4947 cx.update_editor(|e, window, cx| {
4948 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4949 });
4950 cx.assert_editor_state(indoc! {"
4951 «Aaaˇ»
4952 "});
4953}
4954
4955#[gpui::test]
4956async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4957 init_test(cx, |_| {});
4958
4959 let mut cx = EditorTestContext::new(cx).await;
4960
4961 let js_language = Arc::new(Language::new(
4962 LanguageConfig {
4963 name: "JavaScript".into(),
4964 wrap_characters: Some(language::WrapCharactersConfig {
4965 start_prefix: "<".into(),
4966 start_suffix: ">".into(),
4967 end_prefix: "</".into(),
4968 end_suffix: ">".into(),
4969 }),
4970 ..LanguageConfig::default()
4971 },
4972 None,
4973 ));
4974
4975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4976
4977 cx.set_state(indoc! {"
4978 «testˇ»
4979 "});
4980 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4981 cx.assert_editor_state(indoc! {"
4982 <«ˇ»>test</«ˇ»>
4983 "});
4984
4985 cx.set_state(indoc! {"
4986 «test
4987 testˇ»
4988 "});
4989 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4990 cx.assert_editor_state(indoc! {"
4991 <«ˇ»>test
4992 test</«ˇ»>
4993 "});
4994
4995 cx.set_state(indoc! {"
4996 teˇst
4997 "});
4998 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4999 cx.assert_editor_state(indoc! {"
5000 te<«ˇ»></«ˇ»>st
5001 "});
5002}
5003
5004#[gpui::test]
5005async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5006 init_test(cx, |_| {});
5007
5008 let mut cx = EditorTestContext::new(cx).await;
5009
5010 let js_language = Arc::new(Language::new(
5011 LanguageConfig {
5012 name: "JavaScript".into(),
5013 wrap_characters: Some(language::WrapCharactersConfig {
5014 start_prefix: "<".into(),
5015 start_suffix: ">".into(),
5016 end_prefix: "</".into(),
5017 end_suffix: ">".into(),
5018 }),
5019 ..LanguageConfig::default()
5020 },
5021 None,
5022 ));
5023
5024 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5025
5026 cx.set_state(indoc! {"
5027 «testˇ»
5028 «testˇ» «testˇ»
5029 «testˇ»
5030 "});
5031 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5032 cx.assert_editor_state(indoc! {"
5033 <«ˇ»>test</«ˇ»>
5034 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5035 <«ˇ»>test</«ˇ»>
5036 "});
5037
5038 cx.set_state(indoc! {"
5039 «test
5040 testˇ»
5041 «test
5042 testˇ»
5043 "});
5044 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5045 cx.assert_editor_state(indoc! {"
5046 <«ˇ»>test
5047 test</«ˇ»>
5048 <«ˇ»>test
5049 test</«ˇ»>
5050 "});
5051}
5052
5053#[gpui::test]
5054async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5055 init_test(cx, |_| {});
5056
5057 let mut cx = EditorTestContext::new(cx).await;
5058
5059 let plaintext_language = Arc::new(Language::new(
5060 LanguageConfig {
5061 name: "Plain Text".into(),
5062 ..LanguageConfig::default()
5063 },
5064 None,
5065 ));
5066
5067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5068
5069 cx.set_state(indoc! {"
5070 «testˇ»
5071 "});
5072 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5073 cx.assert_editor_state(indoc! {"
5074 «testˇ»
5075 "});
5076}
5077
5078#[gpui::test]
5079async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5080 init_test(cx, |_| {});
5081
5082 let mut cx = EditorTestContext::new(cx).await;
5083
5084 // Manipulate with multiple selections on a single line
5085 cx.set_state(indoc! {"
5086 dd«dd
5087 cˇ»c«c
5088 bb
5089 aaaˇ»aa
5090 "});
5091 cx.update_editor(|e, window, cx| {
5092 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5093 });
5094 cx.assert_editor_state(indoc! {"
5095 «aaaaa
5096 bb
5097 ccc
5098 ddddˇ»
5099 "});
5100
5101 // Manipulate with multiple disjoin selections
5102 cx.set_state(indoc! {"
5103 5«
5104 4
5105 3
5106 2
5107 1ˇ»
5108
5109 dd«dd
5110 ccc
5111 bb
5112 aaaˇ»aa
5113 "});
5114 cx.update_editor(|e, window, cx| {
5115 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5116 });
5117 cx.assert_editor_state(indoc! {"
5118 «1
5119 2
5120 3
5121 4
5122 5ˇ»
5123
5124 «aaaaa
5125 bb
5126 ccc
5127 ddddˇ»
5128 "});
5129
5130 // Adding lines on each selection
5131 cx.set_state(indoc! {"
5132 2«
5133 1ˇ»
5134
5135 bb«bb
5136 aaaˇ»aa
5137 "});
5138 cx.update_editor(|e, window, cx| {
5139 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5140 });
5141 cx.assert_editor_state(indoc! {"
5142 «2
5143 1
5144 added lineˇ»
5145
5146 «bbbb
5147 aaaaa
5148 added lineˇ»
5149 "});
5150
5151 // Removing lines on each selection
5152 cx.set_state(indoc! {"
5153 2«
5154 1ˇ»
5155
5156 bb«bb
5157 aaaˇ»aa
5158 "});
5159 cx.update_editor(|e, window, cx| {
5160 e.manipulate_immutable_lines(window, cx, |lines| {
5161 lines.pop();
5162 })
5163 });
5164 cx.assert_editor_state(indoc! {"
5165 «2ˇ»
5166
5167 «bbbbˇ»
5168 "});
5169}
5170
5171#[gpui::test]
5172async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5173 init_test(cx, |settings| {
5174 settings.defaults.tab_size = NonZeroU32::new(3)
5175 });
5176
5177 let mut cx = EditorTestContext::new(cx).await;
5178
5179 // MULTI SELECTION
5180 // Ln.1 "«" tests empty lines
5181 // Ln.9 tests just leading whitespace
5182 cx.set_state(indoc! {"
5183 «
5184 abc // No indentationˇ»
5185 «\tabc // 1 tabˇ»
5186 \t\tabc « ˇ» // 2 tabs
5187 \t ab«c // Tab followed by space
5188 \tabc // Space followed by tab (3 spaces should be the result)
5189 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5190 abˇ»ˇc ˇ ˇ // Already space indented«
5191 \t
5192 \tabc\tdef // Only the leading tab is manipulatedˇ»
5193 "});
5194 cx.update_editor(|e, window, cx| {
5195 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5196 });
5197 cx.assert_editor_state(
5198 indoc! {"
5199 «
5200 abc // No indentation
5201 abc // 1 tab
5202 abc // 2 tabs
5203 abc // Tab followed by space
5204 abc // Space followed by tab (3 spaces should be the result)
5205 abc // Mixed indentation (tab conversion depends on the column)
5206 abc // Already space indented
5207 ·
5208 abc\tdef // Only the leading tab is manipulatedˇ»
5209 "}
5210 .replace("·", "")
5211 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5212 );
5213
5214 // Test on just a few lines, the others should remain unchanged
5215 // Only lines (3, 5, 10, 11) should change
5216 cx.set_state(
5217 indoc! {"
5218 ·
5219 abc // No indentation
5220 \tabcˇ // 1 tab
5221 \t\tabc // 2 tabs
5222 \t abcˇ // Tab followed by space
5223 \tabc // Space followed by tab (3 spaces should be the result)
5224 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5225 abc // Already space indented
5226 «\t
5227 \tabc\tdef // Only the leading tab is manipulatedˇ»
5228 "}
5229 .replace("·", "")
5230 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5231 );
5232 cx.update_editor(|e, window, cx| {
5233 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5234 });
5235 cx.assert_editor_state(
5236 indoc! {"
5237 ·
5238 abc // No indentation
5239 « abc // 1 tabˇ»
5240 \t\tabc // 2 tabs
5241 « abc // Tab followed by spaceˇ»
5242 \tabc // Space followed by tab (3 spaces should be the result)
5243 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5244 abc // Already space indented
5245 « ·
5246 abc\tdef // Only the leading tab is manipulatedˇ»
5247 "}
5248 .replace("·", "")
5249 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5250 );
5251
5252 // SINGLE SELECTION
5253 // Ln.1 "«" tests empty lines
5254 // Ln.9 tests just leading whitespace
5255 cx.set_state(indoc! {"
5256 «
5257 abc // No indentation
5258 \tabc // 1 tab
5259 \t\tabc // 2 tabs
5260 \t abc // Tab followed by space
5261 \tabc // Space followed by tab (3 spaces should be the result)
5262 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5263 abc // Already space indented
5264 \t
5265 \tabc\tdef // Only the leading tab is manipulatedˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| {
5268 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5269 });
5270 cx.assert_editor_state(
5271 indoc! {"
5272 «
5273 abc // No indentation
5274 abc // 1 tab
5275 abc // 2 tabs
5276 abc // Tab followed by space
5277 abc // Space followed by tab (3 spaces should be the result)
5278 abc // Mixed indentation (tab conversion depends on the column)
5279 abc // Already space indented
5280 ·
5281 abc\tdef // Only the leading tab is manipulatedˇ»
5282 "}
5283 .replace("·", "")
5284 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5285 );
5286}
5287
5288#[gpui::test]
5289async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5290 init_test(cx, |settings| {
5291 settings.defaults.tab_size = NonZeroU32::new(3)
5292 });
5293
5294 let mut cx = EditorTestContext::new(cx).await;
5295
5296 // MULTI SELECTION
5297 // Ln.1 "«" tests empty lines
5298 // Ln.11 tests just leading whitespace
5299 cx.set_state(indoc! {"
5300 «
5301 abˇ»ˇc // No indentation
5302 abc ˇ ˇ // 1 space (< 3 so dont convert)
5303 abc « // 2 spaces (< 3 so dont convert)
5304 abc // 3 spaces (convert)
5305 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5306 «\tˇ»\t«\tˇ»abc // Already tab indented
5307 «\t abc // Tab followed by space
5308 \tabc // Space followed by tab (should be consumed due to tab)
5309 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5310 \tˇ» «\t
5311 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5312 "});
5313 cx.update_editor(|e, window, cx| {
5314 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5315 });
5316 cx.assert_editor_state(indoc! {"
5317 «
5318 abc // No indentation
5319 abc // 1 space (< 3 so dont convert)
5320 abc // 2 spaces (< 3 so dont convert)
5321 \tabc // 3 spaces (convert)
5322 \t abc // 5 spaces (1 tab + 2 spaces)
5323 \t\t\tabc // Already tab indented
5324 \t abc // Tab followed by space
5325 \tabc // Space followed by tab (should be consumed due to tab)
5326 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5327 \t\t\t
5328 \tabc \t // Only the leading spaces should be convertedˇ»
5329 "});
5330
5331 // Test on just a few lines, the other should remain unchanged
5332 // Only lines (4, 8, 11, 12) should change
5333 cx.set_state(
5334 indoc! {"
5335 ·
5336 abc // No indentation
5337 abc // 1 space (< 3 so dont convert)
5338 abc // 2 spaces (< 3 so dont convert)
5339 « abc // 3 spaces (convert)ˇ»
5340 abc // 5 spaces (1 tab + 2 spaces)
5341 \t\t\tabc // Already tab indented
5342 \t abc // Tab followed by space
5343 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5344 \t\t \tabc // Mixed indentation
5345 \t \t \t \tabc // Mixed indentation
5346 \t \tˇ
5347 « abc \t // Only the leading spaces should be convertedˇ»
5348 "}
5349 .replace("·", "")
5350 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5351 );
5352 cx.update_editor(|e, window, cx| {
5353 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5354 });
5355 cx.assert_editor_state(
5356 indoc! {"
5357 ·
5358 abc // No indentation
5359 abc // 1 space (< 3 so dont convert)
5360 abc // 2 spaces (< 3 so dont convert)
5361 «\tabc // 3 spaces (convert)ˇ»
5362 abc // 5 spaces (1 tab + 2 spaces)
5363 \t\t\tabc // Already tab indented
5364 \t abc // Tab followed by space
5365 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5366 \t\t \tabc // Mixed indentation
5367 \t \t \t \tabc // Mixed indentation
5368 «\t\t\t
5369 \tabc \t // Only the leading spaces should be convertedˇ»
5370 "}
5371 .replace("·", "")
5372 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5373 );
5374
5375 // SINGLE SELECTION
5376 // Ln.1 "«" tests empty lines
5377 // Ln.11 tests just leading whitespace
5378 cx.set_state(indoc! {"
5379 «
5380 abc // No indentation
5381 abc // 1 space (< 3 so dont convert)
5382 abc // 2 spaces (< 3 so dont convert)
5383 abc // 3 spaces (convert)
5384 abc // 5 spaces (1 tab + 2 spaces)
5385 \t\t\tabc // Already tab indented
5386 \t abc // Tab followed by space
5387 \tabc // Space followed by tab (should be consumed due to tab)
5388 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5389 \t \t
5390 abc \t // Only the leading spaces should be convertedˇ»
5391 "});
5392 cx.update_editor(|e, window, cx| {
5393 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5394 });
5395 cx.assert_editor_state(indoc! {"
5396 «
5397 abc // No indentation
5398 abc // 1 space (< 3 so dont convert)
5399 abc // 2 spaces (< 3 so dont convert)
5400 \tabc // 3 spaces (convert)
5401 \t abc // 5 spaces (1 tab + 2 spaces)
5402 \t\t\tabc // Already tab indented
5403 \t abc // Tab followed by space
5404 \tabc // Space followed by tab (should be consumed due to tab)
5405 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5406 \t\t\t
5407 \tabc \t // Only the leading spaces should be convertedˇ»
5408 "});
5409}
5410
5411#[gpui::test]
5412async fn test_toggle_case(cx: &mut TestAppContext) {
5413 init_test(cx, |_| {});
5414
5415 let mut cx = EditorTestContext::new(cx).await;
5416
5417 // If all lower case -> upper case
5418 cx.set_state(indoc! {"
5419 «hello worldˇ»
5420 "});
5421 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5422 cx.assert_editor_state(indoc! {"
5423 «HELLO WORLDˇ»
5424 "});
5425
5426 // If all upper case -> lower case
5427 cx.set_state(indoc! {"
5428 «HELLO WORLDˇ»
5429 "});
5430 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5431 cx.assert_editor_state(indoc! {"
5432 «hello worldˇ»
5433 "});
5434
5435 // If any upper case characters are identified -> lower case
5436 // This matches JetBrains IDEs
5437 cx.set_state(indoc! {"
5438 «hEllo worldˇ»
5439 "});
5440 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5441 cx.assert_editor_state(indoc! {"
5442 «hello worldˇ»
5443 "});
5444}
5445
5446#[gpui::test]
5447async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5448 init_test(cx, |_| {});
5449
5450 let mut cx = EditorTestContext::new(cx).await;
5451
5452 cx.set_state(indoc! {"
5453 «implement-windows-supportˇ»
5454 "});
5455 cx.update_editor(|e, window, cx| {
5456 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5457 });
5458 cx.assert_editor_state(indoc! {"
5459 «Implement windows supportˇ»
5460 "});
5461}
5462
5463#[gpui::test]
5464async fn test_manipulate_text(cx: &mut TestAppContext) {
5465 init_test(cx, |_| {});
5466
5467 let mut cx = EditorTestContext::new(cx).await;
5468
5469 // Test convert_to_upper_case()
5470 cx.set_state(indoc! {"
5471 «hello worldˇ»
5472 "});
5473 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5474 cx.assert_editor_state(indoc! {"
5475 «HELLO WORLDˇ»
5476 "});
5477
5478 // Test convert_to_lower_case()
5479 cx.set_state(indoc! {"
5480 «HELLO WORLDˇ»
5481 "});
5482 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5483 cx.assert_editor_state(indoc! {"
5484 «hello worldˇ»
5485 "});
5486
5487 // Test multiple line, single selection case
5488 cx.set_state(indoc! {"
5489 «The quick brown
5490 fox jumps over
5491 the lazy dogˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «The Quick Brown
5496 Fox Jumps Over
5497 The Lazy Dogˇ»
5498 "});
5499
5500 // Test multiple line, single selection case
5501 cx.set_state(indoc! {"
5502 «The quick brown
5503 fox jumps over
5504 the lazy dogˇ»
5505 "});
5506 cx.update_editor(|e, window, cx| {
5507 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5508 });
5509 cx.assert_editor_state(indoc! {"
5510 «TheQuickBrown
5511 FoxJumpsOver
5512 TheLazyDogˇ»
5513 "});
5514
5515 // From here on out, test more complex cases of manipulate_text()
5516
5517 // Test no selection case - should affect words cursors are in
5518 // Cursor at beginning, middle, and end of word
5519 cx.set_state(indoc! {"
5520 ˇhello big beauˇtiful worldˇ
5521 "});
5522 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5523 cx.assert_editor_state(indoc! {"
5524 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5525 "});
5526
5527 // Test multiple selections on a single line and across multiple lines
5528 cx.set_state(indoc! {"
5529 «Theˇ» quick «brown
5530 foxˇ» jumps «overˇ»
5531 the «lazyˇ» dog
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «THEˇ» quick «BROWN
5536 FOXˇ» jumps «OVERˇ»
5537 the «LAZYˇ» dog
5538 "});
5539
5540 // Test case where text length grows
5541 cx.set_state(indoc! {"
5542 «tschüߡ»
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «TSCHÜSSˇ»
5547 "});
5548
5549 // Test to make sure we don't crash when text shrinks
5550 cx.set_state(indoc! {"
5551 aaa_bbbˇ
5552 "});
5553 cx.update_editor(|e, window, cx| {
5554 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5555 });
5556 cx.assert_editor_state(indoc! {"
5557 «aaaBbbˇ»
5558 "});
5559
5560 // Test to make sure we all aware of the fact that each word can grow and shrink
5561 // Final selections should be aware of this fact
5562 cx.set_state(indoc! {"
5563 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5564 "});
5565 cx.update_editor(|e, window, cx| {
5566 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5567 });
5568 cx.assert_editor_state(indoc! {"
5569 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5570 "});
5571
5572 cx.set_state(indoc! {"
5573 «hElLo, WoRld!ˇ»
5574 "});
5575 cx.update_editor(|e, window, cx| {
5576 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5577 });
5578 cx.assert_editor_state(indoc! {"
5579 «HeLlO, wOrLD!ˇ»
5580 "});
5581
5582 // Test selections with `line_mode() = true`.
5583 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5584 cx.set_state(indoc! {"
5585 «The quick brown
5586 fox jumps over
5587 tˇ»he lazy dog
5588 "});
5589 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 «THE QUICK BROWN
5592 FOX JUMPS OVER
5593 THE LAZY DOGˇ»
5594 "});
5595}
5596
5597#[gpui::test]
5598fn test_duplicate_line(cx: &mut TestAppContext) {
5599 init_test(cx, |_| {});
5600
5601 let editor = cx.add_window(|window, cx| {
5602 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5603 build_editor(buffer, window, cx)
5604 });
5605 _ = editor.update(cx, |editor, window, cx| {
5606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5607 s.select_display_ranges([
5608 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5609 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5610 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5611 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5612 ])
5613 });
5614 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5615 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5616 assert_eq!(
5617 editor.selections.display_ranges(cx),
5618 vec![
5619 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5620 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5621 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5622 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5623 ]
5624 );
5625 });
5626
5627 let editor = cx.add_window(|window, cx| {
5628 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5629 build_editor(buffer, window, cx)
5630 });
5631 _ = editor.update(cx, |editor, window, cx| {
5632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5633 s.select_display_ranges([
5634 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5635 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5636 ])
5637 });
5638 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5639 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5640 assert_eq!(
5641 editor.selections.display_ranges(cx),
5642 vec![
5643 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5644 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5645 ]
5646 );
5647 });
5648
5649 // With `duplicate_line_up` the selections move to the duplicated lines,
5650 // which are inserted above the original lines
5651 let editor = cx.add_window(|window, cx| {
5652 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5653 build_editor(buffer, window, cx)
5654 });
5655 _ = editor.update(cx, |editor, window, cx| {
5656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5657 s.select_display_ranges([
5658 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5659 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5660 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5661 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5662 ])
5663 });
5664 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5665 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5666 assert_eq!(
5667 editor.selections.display_ranges(cx),
5668 vec![
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5672 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5673 ]
5674 );
5675 });
5676
5677 let editor = cx.add_window(|window, cx| {
5678 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5679 build_editor(buffer, window, cx)
5680 });
5681 _ = editor.update(cx, |editor, window, cx| {
5682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5683 s.select_display_ranges([
5684 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5685 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5686 ])
5687 });
5688 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5689 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5690 assert_eq!(
5691 editor.selections.display_ranges(cx),
5692 vec![
5693 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5694 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5695 ]
5696 );
5697 });
5698
5699 let editor = cx.add_window(|window, cx| {
5700 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5701 build_editor(buffer, window, cx)
5702 });
5703 _ = editor.update(cx, |editor, window, cx| {
5704 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5705 s.select_display_ranges([
5706 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5707 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5708 ])
5709 });
5710 editor.duplicate_selection(&DuplicateSelection, window, cx);
5711 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5712 assert_eq!(
5713 editor.selections.display_ranges(cx),
5714 vec![
5715 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5716 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5717 ]
5718 );
5719 });
5720}
5721
5722#[gpui::test]
5723fn test_move_line_up_down(cx: &mut TestAppContext) {
5724 init_test(cx, |_| {});
5725
5726 let editor = cx.add_window(|window, cx| {
5727 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5728 build_editor(buffer, window, cx)
5729 });
5730 _ = editor.update(cx, |editor, window, cx| {
5731 editor.fold_creases(
5732 vec![
5733 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5734 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5735 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5736 ],
5737 true,
5738 window,
5739 cx,
5740 );
5741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5742 s.select_display_ranges([
5743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5744 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5745 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5746 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5747 ])
5748 });
5749 assert_eq!(
5750 editor.display_text(cx),
5751 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5752 );
5753
5754 editor.move_line_up(&MoveLineUp, window, cx);
5755 assert_eq!(
5756 editor.display_text(cx),
5757 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5758 );
5759 assert_eq!(
5760 editor.selections.display_ranges(cx),
5761 vec![
5762 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5763 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5764 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5765 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5766 ]
5767 );
5768 });
5769
5770 _ = editor.update(cx, |editor, window, cx| {
5771 editor.move_line_down(&MoveLineDown, window, cx);
5772 assert_eq!(
5773 editor.display_text(cx),
5774 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5775 );
5776 assert_eq!(
5777 editor.selections.display_ranges(cx),
5778 vec![
5779 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5780 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5781 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5782 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5783 ]
5784 );
5785 });
5786
5787 _ = editor.update(cx, |editor, window, cx| {
5788 editor.move_line_down(&MoveLineDown, window, cx);
5789 assert_eq!(
5790 editor.display_text(cx),
5791 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5792 );
5793 assert_eq!(
5794 editor.selections.display_ranges(cx),
5795 vec![
5796 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5797 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5798 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5799 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5800 ]
5801 );
5802 });
5803
5804 _ = editor.update(cx, |editor, window, cx| {
5805 editor.move_line_up(&MoveLineUp, window, cx);
5806 assert_eq!(
5807 editor.display_text(cx),
5808 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5809 );
5810 assert_eq!(
5811 editor.selections.display_ranges(cx),
5812 vec![
5813 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5814 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5815 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5816 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5817 ]
5818 );
5819 });
5820}
5821
5822#[gpui::test]
5823fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825 let editor = cx.add_window(|window, cx| {
5826 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5827 build_editor(buffer, window, cx)
5828 });
5829 _ = editor.update(cx, |editor, window, cx| {
5830 editor.fold_creases(
5831 vec![Crease::simple(
5832 Point::new(6, 4)..Point::new(7, 4),
5833 FoldPlaceholder::test(),
5834 )],
5835 true,
5836 window,
5837 cx,
5838 );
5839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5840 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5841 });
5842 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5843 editor.move_line_up(&MoveLineUp, window, cx);
5844 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5845 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5846 });
5847}
5848
5849#[gpui::test]
5850fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5851 init_test(cx, |_| {});
5852
5853 let editor = cx.add_window(|window, cx| {
5854 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5855 build_editor(buffer, window, cx)
5856 });
5857 _ = editor.update(cx, |editor, window, cx| {
5858 let snapshot = editor.buffer.read(cx).snapshot(cx);
5859 editor.insert_blocks(
5860 [BlockProperties {
5861 style: BlockStyle::Fixed,
5862 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5863 height: Some(1),
5864 render: Arc::new(|_| div().into_any()),
5865 priority: 0,
5866 }],
5867 Some(Autoscroll::fit()),
5868 cx,
5869 );
5870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5871 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5872 });
5873 editor.move_line_down(&MoveLineDown, window, cx);
5874 });
5875}
5876
5877#[gpui::test]
5878async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5879 init_test(cx, |_| {});
5880
5881 let mut cx = EditorTestContext::new(cx).await;
5882 cx.set_state(
5883 &"
5884 ˇzero
5885 one
5886 two
5887 three
5888 four
5889 five
5890 "
5891 .unindent(),
5892 );
5893
5894 // Create a four-line block that replaces three lines of text.
5895 cx.update_editor(|editor, window, cx| {
5896 let snapshot = editor.snapshot(window, cx);
5897 let snapshot = &snapshot.buffer_snapshot();
5898 let placement = BlockPlacement::Replace(
5899 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5900 );
5901 editor.insert_blocks(
5902 [BlockProperties {
5903 placement,
5904 height: Some(4),
5905 style: BlockStyle::Sticky,
5906 render: Arc::new(|_| gpui::div().into_any_element()),
5907 priority: 0,
5908 }],
5909 None,
5910 cx,
5911 );
5912 });
5913
5914 // Move down so that the cursor touches the block.
5915 cx.update_editor(|editor, window, cx| {
5916 editor.move_down(&Default::default(), window, cx);
5917 });
5918 cx.assert_editor_state(
5919 &"
5920 zero
5921 «one
5922 two
5923 threeˇ»
5924 four
5925 five
5926 "
5927 .unindent(),
5928 );
5929
5930 // Move down past the block.
5931 cx.update_editor(|editor, window, cx| {
5932 editor.move_down(&Default::default(), window, cx);
5933 });
5934 cx.assert_editor_state(
5935 &"
5936 zero
5937 one
5938 two
5939 three
5940 ˇfour
5941 five
5942 "
5943 .unindent(),
5944 );
5945}
5946
5947#[gpui::test]
5948fn test_transpose(cx: &mut TestAppContext) {
5949 init_test(cx, |_| {});
5950
5951 _ = cx.add_window(|window, cx| {
5952 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5953 editor.set_style(EditorStyle::default(), window, cx);
5954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5955 s.select_ranges([1..1])
5956 });
5957 editor.transpose(&Default::default(), window, cx);
5958 assert_eq!(editor.text(cx), "bac");
5959 assert_eq!(
5960 editor.selections.ranges(&editor.display_snapshot(cx)),
5961 [2..2]
5962 );
5963
5964 editor.transpose(&Default::default(), window, cx);
5965 assert_eq!(editor.text(cx), "bca");
5966 assert_eq!(
5967 editor.selections.ranges(&editor.display_snapshot(cx)),
5968 [3..3]
5969 );
5970
5971 editor.transpose(&Default::default(), window, cx);
5972 assert_eq!(editor.text(cx), "bac");
5973 assert_eq!(
5974 editor.selections.ranges(&editor.display_snapshot(cx)),
5975 [3..3]
5976 );
5977
5978 editor
5979 });
5980
5981 _ = cx.add_window(|window, cx| {
5982 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5983 editor.set_style(EditorStyle::default(), window, cx);
5984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5985 s.select_ranges([3..3])
5986 });
5987 editor.transpose(&Default::default(), window, cx);
5988 assert_eq!(editor.text(cx), "acb\nde");
5989 assert_eq!(
5990 editor.selections.ranges(&editor.display_snapshot(cx)),
5991 [3..3]
5992 );
5993
5994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5995 s.select_ranges([4..4])
5996 });
5997 editor.transpose(&Default::default(), window, cx);
5998 assert_eq!(editor.text(cx), "acbd\ne");
5999 assert_eq!(
6000 editor.selections.ranges(&editor.display_snapshot(cx)),
6001 [5..5]
6002 );
6003
6004 editor.transpose(&Default::default(), window, cx);
6005 assert_eq!(editor.text(cx), "acbde\n");
6006 assert_eq!(
6007 editor.selections.ranges(&editor.display_snapshot(cx)),
6008 [6..6]
6009 );
6010
6011 editor.transpose(&Default::default(), window, cx);
6012 assert_eq!(editor.text(cx), "acbd\ne");
6013 assert_eq!(
6014 editor.selections.ranges(&editor.display_snapshot(cx)),
6015 [6..6]
6016 );
6017
6018 editor
6019 });
6020
6021 _ = cx.add_window(|window, cx| {
6022 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6023 editor.set_style(EditorStyle::default(), window, cx);
6024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6025 s.select_ranges([1..1, 2..2, 4..4])
6026 });
6027 editor.transpose(&Default::default(), window, cx);
6028 assert_eq!(editor.text(cx), "bacd\ne");
6029 assert_eq!(
6030 editor.selections.ranges(&editor.display_snapshot(cx)),
6031 [2..2, 3..3, 5..5]
6032 );
6033
6034 editor.transpose(&Default::default(), window, cx);
6035 assert_eq!(editor.text(cx), "bcade\n");
6036 assert_eq!(
6037 editor.selections.ranges(&editor.display_snapshot(cx)),
6038 [3..3, 4..4, 6..6]
6039 );
6040
6041 editor.transpose(&Default::default(), window, cx);
6042 assert_eq!(editor.text(cx), "bcda\ne");
6043 assert_eq!(
6044 editor.selections.ranges(&editor.display_snapshot(cx)),
6045 [4..4, 6..6]
6046 );
6047
6048 editor.transpose(&Default::default(), window, cx);
6049 assert_eq!(editor.text(cx), "bcade\n");
6050 assert_eq!(
6051 editor.selections.ranges(&editor.display_snapshot(cx)),
6052 [4..4, 6..6]
6053 );
6054
6055 editor.transpose(&Default::default(), window, cx);
6056 assert_eq!(editor.text(cx), "bcaed\n");
6057 assert_eq!(
6058 editor.selections.ranges(&editor.display_snapshot(cx)),
6059 [5..5, 6..6]
6060 );
6061
6062 editor
6063 });
6064
6065 _ = cx.add_window(|window, cx| {
6066 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6067 editor.set_style(EditorStyle::default(), window, cx);
6068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6069 s.select_ranges([4..4])
6070 });
6071 editor.transpose(&Default::default(), window, cx);
6072 assert_eq!(editor.text(cx), "🏀🍐✋");
6073 assert_eq!(
6074 editor.selections.ranges(&editor.display_snapshot(cx)),
6075 [8..8]
6076 );
6077
6078 editor.transpose(&Default::default(), window, cx);
6079 assert_eq!(editor.text(cx), "🏀✋🍐");
6080 assert_eq!(
6081 editor.selections.ranges(&editor.display_snapshot(cx)),
6082 [11..11]
6083 );
6084
6085 editor.transpose(&Default::default(), window, cx);
6086 assert_eq!(editor.text(cx), "🏀🍐✋");
6087 assert_eq!(
6088 editor.selections.ranges(&editor.display_snapshot(cx)),
6089 [11..11]
6090 );
6091
6092 editor
6093 });
6094}
6095
6096#[gpui::test]
6097async fn test_rewrap(cx: &mut TestAppContext) {
6098 init_test(cx, |settings| {
6099 settings.languages.0.extend([
6100 (
6101 "Markdown".into(),
6102 LanguageSettingsContent {
6103 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6104 preferred_line_length: Some(40),
6105 ..Default::default()
6106 },
6107 ),
6108 (
6109 "Plain Text".into(),
6110 LanguageSettingsContent {
6111 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6112 preferred_line_length: Some(40),
6113 ..Default::default()
6114 },
6115 ),
6116 (
6117 "C++".into(),
6118 LanguageSettingsContent {
6119 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6120 preferred_line_length: Some(40),
6121 ..Default::default()
6122 },
6123 ),
6124 (
6125 "Python".into(),
6126 LanguageSettingsContent {
6127 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6128 preferred_line_length: Some(40),
6129 ..Default::default()
6130 },
6131 ),
6132 (
6133 "Rust".into(),
6134 LanguageSettingsContent {
6135 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6136 preferred_line_length: Some(40),
6137 ..Default::default()
6138 },
6139 ),
6140 ])
6141 });
6142
6143 let mut cx = EditorTestContext::new(cx).await;
6144
6145 let cpp_language = Arc::new(Language::new(
6146 LanguageConfig {
6147 name: "C++".into(),
6148 line_comments: vec!["// ".into()],
6149 ..LanguageConfig::default()
6150 },
6151 None,
6152 ));
6153 let python_language = Arc::new(Language::new(
6154 LanguageConfig {
6155 name: "Python".into(),
6156 line_comments: vec!["# ".into()],
6157 ..LanguageConfig::default()
6158 },
6159 None,
6160 ));
6161 let markdown_language = Arc::new(Language::new(
6162 LanguageConfig {
6163 name: "Markdown".into(),
6164 rewrap_prefixes: vec![
6165 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6166 regex::Regex::new("[-*+]\\s+").unwrap(),
6167 ],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let rust_language = Arc::new(
6173 Language::new(
6174 LanguageConfig {
6175 name: "Rust".into(),
6176 line_comments: vec!["// ".into(), "/// ".into()],
6177 ..LanguageConfig::default()
6178 },
6179 Some(tree_sitter_rust::LANGUAGE.into()),
6180 )
6181 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6182 .unwrap(),
6183 );
6184
6185 let plaintext_language = Arc::new(Language::new(
6186 LanguageConfig {
6187 name: "Plain Text".into(),
6188 ..LanguageConfig::default()
6189 },
6190 None,
6191 ));
6192
6193 // Test basic rewrapping of a long line with a cursor
6194 assert_rewrap(
6195 indoc! {"
6196 // ˇThis is a long comment that needs to be wrapped.
6197 "},
6198 indoc! {"
6199 // ˇThis is a long comment that needs to
6200 // be wrapped.
6201 "},
6202 cpp_language.clone(),
6203 &mut cx,
6204 );
6205
6206 // Test rewrapping a full selection
6207 assert_rewrap(
6208 indoc! {"
6209 «// This selected long comment needs to be wrapped.ˇ»"
6210 },
6211 indoc! {"
6212 «// This selected long comment needs to
6213 // be wrapped.ˇ»"
6214 },
6215 cpp_language.clone(),
6216 &mut cx,
6217 );
6218
6219 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6220 assert_rewrap(
6221 indoc! {"
6222 // ˇThis is the first line.
6223 // Thisˇ is the second line.
6224 // This is the thirdˇ line, all part of one paragraph.
6225 "},
6226 indoc! {"
6227 // ˇThis is the first line. Thisˇ is the
6228 // second line. This is the thirdˇ line,
6229 // all part of one paragraph.
6230 "},
6231 cpp_language.clone(),
6232 &mut cx,
6233 );
6234
6235 // Test multiple cursors in different paragraphs trigger separate rewraps
6236 assert_rewrap(
6237 indoc! {"
6238 // ˇThis is the first paragraph, first line.
6239 // ˇThis is the first paragraph, second line.
6240
6241 // ˇThis is the second paragraph, first line.
6242 // ˇThis is the second paragraph, second line.
6243 "},
6244 indoc! {"
6245 // ˇThis is the first paragraph, first
6246 // line. ˇThis is the first paragraph,
6247 // second line.
6248
6249 // ˇThis is the second paragraph, first
6250 // line. ˇThis is the second paragraph,
6251 // second line.
6252 "},
6253 cpp_language.clone(),
6254 &mut cx,
6255 );
6256
6257 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6258 assert_rewrap(
6259 indoc! {"
6260 «// A regular long long comment to be wrapped.
6261 /// A documentation long comment to be wrapped.ˇ»
6262 "},
6263 indoc! {"
6264 «// A regular long long comment to be
6265 // wrapped.
6266 /// A documentation long comment to be
6267 /// wrapped.ˇ»
6268 "},
6269 rust_language.clone(),
6270 &mut cx,
6271 );
6272
6273 // Test that change in indentation level trigger seperate rewraps
6274 assert_rewrap(
6275 indoc! {"
6276 fn foo() {
6277 «// This is a long comment at the base indent.
6278 // This is a long comment at the next indent.ˇ»
6279 }
6280 "},
6281 indoc! {"
6282 fn foo() {
6283 «// This is a long comment at the
6284 // base indent.
6285 // This is a long comment at the
6286 // next indent.ˇ»
6287 }
6288 "},
6289 rust_language.clone(),
6290 &mut cx,
6291 );
6292
6293 // Test that different comment prefix characters (e.g., '#') are handled correctly
6294 assert_rewrap(
6295 indoc! {"
6296 # ˇThis is a long comment using a pound sign.
6297 "},
6298 indoc! {"
6299 # ˇThis is a long comment using a pound
6300 # sign.
6301 "},
6302 python_language,
6303 &mut cx,
6304 );
6305
6306 // Test rewrapping only affects comments, not code even when selected
6307 assert_rewrap(
6308 indoc! {"
6309 «/// This doc comment is long and should be wrapped.
6310 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6311 "},
6312 indoc! {"
6313 «/// This doc comment is long and should
6314 /// be wrapped.
6315 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6316 "},
6317 rust_language.clone(),
6318 &mut cx,
6319 );
6320
6321 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6322 assert_rewrap(
6323 indoc! {"
6324 # Header
6325
6326 A long long long line of markdown text to wrap.ˇ
6327 "},
6328 indoc! {"
6329 # Header
6330
6331 A long long long line of markdown text
6332 to wrap.ˇ
6333 "},
6334 markdown_language.clone(),
6335 &mut cx,
6336 );
6337
6338 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6339 assert_rewrap(
6340 indoc! {"
6341 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6342 2. This is a numbered list item that is very long and needs to be wrapped properly.
6343 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6344 "},
6345 indoc! {"
6346 «1. This is a numbered list item that is
6347 very long and needs to be wrapped
6348 properly.
6349 2. This is a numbered list item that is
6350 very long and needs to be wrapped
6351 properly.
6352 - This is an unordered list item that is
6353 also very long and should not merge
6354 with the numbered item.ˇ»
6355 "},
6356 markdown_language.clone(),
6357 &mut cx,
6358 );
6359
6360 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6361 assert_rewrap(
6362 indoc! {"
6363 «1. This is a numbered list item that is
6364 very long and needs to be wrapped
6365 properly.
6366 2. This is a numbered list item that is
6367 very long and needs to be wrapped
6368 properly.
6369 - This is an unordered list item that is
6370 also very long and should not merge with
6371 the numbered item.ˇ»
6372 "},
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge
6382 with the numbered item.ˇ»
6383 "},
6384 markdown_language.clone(),
6385 &mut cx,
6386 );
6387
6388 // Test that rewrapping maintain indents even when they already exists.
6389 assert_rewrap(
6390 indoc! {"
6391 «1. This is a numbered list
6392 item that is very long and needs to be wrapped properly.
6393 2. This is a numbered list
6394 item that is very long and needs to be wrapped properly.
6395 - This is an unordered list item that is also very long and
6396 should not merge with the numbered item.ˇ»
6397 "},
6398 indoc! {"
6399 «1. This is a numbered list item that is
6400 very long and needs to be wrapped
6401 properly.
6402 2. This is a numbered list item that is
6403 very long and needs to be wrapped
6404 properly.
6405 - This is an unordered list item that is
6406 also very long and should not merge
6407 with the numbered item.ˇ»
6408 "},
6409 markdown_language,
6410 &mut cx,
6411 );
6412
6413 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6414 assert_rewrap(
6415 indoc! {"
6416 ˇThis is a very long line of plain text that will be wrapped.
6417 "},
6418 indoc! {"
6419 ˇThis is a very long line of plain text
6420 that will be wrapped.
6421 "},
6422 plaintext_language.clone(),
6423 &mut cx,
6424 );
6425
6426 // Test that non-commented code acts as a paragraph boundary within a selection
6427 assert_rewrap(
6428 indoc! {"
6429 «// This is the first long comment block to be wrapped.
6430 fn my_func(a: u32);
6431 // This is the second long comment block to be wrapped.ˇ»
6432 "},
6433 indoc! {"
6434 «// This is the first long comment block
6435 // to be wrapped.
6436 fn my_func(a: u32);
6437 // This is the second long comment block
6438 // to be wrapped.ˇ»
6439 "},
6440 rust_language,
6441 &mut cx,
6442 );
6443
6444 // Test rewrapping multiple selections, including ones with blank lines or tabs
6445 assert_rewrap(
6446 indoc! {"
6447 «ˇThis is a very long line that will be wrapped.
6448
6449 This is another paragraph in the same selection.»
6450
6451 «\tThis is a very long indented line that will be wrapped.ˇ»
6452 "},
6453 indoc! {"
6454 «ˇThis is a very long line that will be
6455 wrapped.
6456
6457 This is another paragraph in the same
6458 selection.»
6459
6460 «\tThis is a very long indented line
6461 \tthat will be wrapped.ˇ»
6462 "},
6463 plaintext_language,
6464 &mut cx,
6465 );
6466
6467 // Test that an empty comment line acts as a paragraph boundary
6468 assert_rewrap(
6469 indoc! {"
6470 // ˇThis is a long comment that will be wrapped.
6471 //
6472 // And this is another long comment that will also be wrapped.ˇ
6473 "},
6474 indoc! {"
6475 // ˇThis is a long comment that will be
6476 // wrapped.
6477 //
6478 // And this is another long comment that
6479 // will also be wrapped.ˇ
6480 "},
6481 cpp_language,
6482 &mut cx,
6483 );
6484
6485 #[track_caller]
6486 fn assert_rewrap(
6487 unwrapped_text: &str,
6488 wrapped_text: &str,
6489 language: Arc<Language>,
6490 cx: &mut EditorTestContext,
6491 ) {
6492 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6493 cx.set_state(unwrapped_text);
6494 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6495 cx.assert_editor_state(wrapped_text);
6496 }
6497}
6498
6499#[gpui::test]
6500async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6501 init_test(cx, |settings| {
6502 settings.languages.0.extend([(
6503 "Rust".into(),
6504 LanguageSettingsContent {
6505 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6506 preferred_line_length: Some(40),
6507 ..Default::default()
6508 },
6509 )])
6510 });
6511
6512 let mut cx = EditorTestContext::new(cx).await;
6513
6514 let rust_lang = Arc::new(
6515 Language::new(
6516 LanguageConfig {
6517 name: "Rust".into(),
6518 line_comments: vec!["// ".into()],
6519 block_comment: Some(BlockCommentConfig {
6520 start: "/*".into(),
6521 end: "*/".into(),
6522 prefix: "* ".into(),
6523 tab_size: 1,
6524 }),
6525 documentation_comment: Some(BlockCommentConfig {
6526 start: "/**".into(),
6527 end: "*/".into(),
6528 prefix: "* ".into(),
6529 tab_size: 1,
6530 }),
6531
6532 ..LanguageConfig::default()
6533 },
6534 Some(tree_sitter_rust::LANGUAGE.into()),
6535 )
6536 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6537 .unwrap(),
6538 );
6539
6540 // regular block comment
6541 assert_rewrap(
6542 indoc! {"
6543 /*
6544 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6545 */
6546 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6547 "},
6548 indoc! {"
6549 /*
6550 *ˇ Lorem ipsum dolor sit amet,
6551 * consectetur adipiscing elit.
6552 */
6553 /*
6554 *ˇ Lorem ipsum dolor sit amet,
6555 * consectetur adipiscing elit.
6556 */
6557 "},
6558 rust_lang.clone(),
6559 &mut cx,
6560 );
6561
6562 // indent is respected
6563 assert_rewrap(
6564 indoc! {"
6565 {}
6566 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6567 "},
6568 indoc! {"
6569 {}
6570 /*
6571 *ˇ Lorem ipsum dolor sit amet,
6572 * consectetur adipiscing elit.
6573 */
6574 "},
6575 rust_lang.clone(),
6576 &mut cx,
6577 );
6578
6579 // short block comments with inline delimiters
6580 assert_rewrap(
6581 indoc! {"
6582 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6583 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6584 */
6585 /*
6586 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6587 "},
6588 indoc! {"
6589 /*
6590 *ˇ Lorem ipsum dolor sit amet,
6591 * consectetur adipiscing elit.
6592 */
6593 /*
6594 *ˇ Lorem ipsum dolor sit amet,
6595 * consectetur adipiscing elit.
6596 */
6597 /*
6598 *ˇ Lorem ipsum dolor sit amet,
6599 * consectetur adipiscing elit.
6600 */
6601 "},
6602 rust_lang.clone(),
6603 &mut cx,
6604 );
6605
6606 // multiline block comment with inline start/end delimiters
6607 assert_rewrap(
6608 indoc! {"
6609 /*ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit. */
6611 "},
6612 indoc! {"
6613 /*
6614 *ˇ Lorem ipsum dolor sit amet,
6615 * consectetur adipiscing elit.
6616 */
6617 "},
6618 rust_lang.clone(),
6619 &mut cx,
6620 );
6621
6622 // block comment rewrap still respects paragraph bounds
6623 assert_rewrap(
6624 indoc! {"
6625 /*
6626 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6627 *
6628 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6629 */
6630 "},
6631 indoc! {"
6632 /*
6633 *ˇ Lorem ipsum dolor sit amet,
6634 * consectetur adipiscing elit.
6635 *
6636 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6637 */
6638 "},
6639 rust_lang.clone(),
6640 &mut cx,
6641 );
6642
6643 // documentation comments
6644 assert_rewrap(
6645 indoc! {"
6646 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6647 /**
6648 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6649 */
6650 "},
6651 indoc! {"
6652 /**
6653 *ˇ Lorem ipsum dolor sit amet,
6654 * consectetur adipiscing elit.
6655 */
6656 /**
6657 *ˇ Lorem ipsum dolor sit amet,
6658 * consectetur adipiscing elit.
6659 */
6660 "},
6661 rust_lang.clone(),
6662 &mut cx,
6663 );
6664
6665 // different, adjacent comments
6666 assert_rewrap(
6667 indoc! {"
6668 /**
6669 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6670 */
6671 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6672 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6673 "},
6674 indoc! {"
6675 /**
6676 *ˇ Lorem ipsum dolor sit amet,
6677 * consectetur adipiscing elit.
6678 */
6679 /*
6680 *ˇ Lorem ipsum dolor sit amet,
6681 * consectetur adipiscing elit.
6682 */
6683 //ˇ Lorem ipsum dolor sit amet,
6684 // consectetur adipiscing elit.
6685 "},
6686 rust_lang.clone(),
6687 &mut cx,
6688 );
6689
6690 // selection w/ single short block comment
6691 assert_rewrap(
6692 indoc! {"
6693 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6694 "},
6695 indoc! {"
6696 «/*
6697 * Lorem ipsum dolor sit amet,
6698 * consectetur adipiscing elit.
6699 */ˇ»
6700 "},
6701 rust_lang.clone(),
6702 &mut cx,
6703 );
6704
6705 // rewrapping a single comment w/ abutting comments
6706 assert_rewrap(
6707 indoc! {"
6708 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6709 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6710 "},
6711 indoc! {"
6712 /*
6713 * ˇLorem ipsum dolor sit amet,
6714 * consectetur adipiscing elit.
6715 */
6716 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6717 "},
6718 rust_lang.clone(),
6719 &mut cx,
6720 );
6721
6722 // selection w/ non-abutting short block comments
6723 assert_rewrap(
6724 indoc! {"
6725 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6726
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6728 "},
6729 indoc! {"
6730 «/*
6731 * Lorem ipsum dolor sit amet,
6732 * consectetur adipiscing elit.
6733 */
6734
6735 /*
6736 * Lorem ipsum dolor sit amet,
6737 * consectetur adipiscing elit.
6738 */ˇ»
6739 "},
6740 rust_lang.clone(),
6741 &mut cx,
6742 );
6743
6744 // selection of multiline block comments
6745 assert_rewrap(
6746 indoc! {"
6747 «/* Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit. */ˇ»
6749 "},
6750 indoc! {"
6751 «/*
6752 * Lorem ipsum dolor sit amet,
6753 * consectetur adipiscing elit.
6754 */ˇ»
6755 "},
6756 rust_lang.clone(),
6757 &mut cx,
6758 );
6759
6760 // partial selection of multiline block comments
6761 assert_rewrap(
6762 indoc! {"
6763 «/* Lorem ipsum dolor sit amet,ˇ»
6764 * consectetur adipiscing elit. */
6765 /* Lorem ipsum dolor sit amet,
6766 «* consectetur adipiscing elit. */ˇ»
6767 "},
6768 indoc! {"
6769 «/*
6770 * Lorem ipsum dolor sit amet,ˇ»
6771 * consectetur adipiscing elit. */
6772 /* Lorem ipsum dolor sit amet,
6773 «* consectetur adipiscing elit.
6774 */ˇ»
6775 "},
6776 rust_lang.clone(),
6777 &mut cx,
6778 );
6779
6780 // selection w/ abutting short block comments
6781 // TODO: should not be combined; should rewrap as 2 comments
6782 assert_rewrap(
6783 indoc! {"
6784 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6785 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6786 "},
6787 // desired behavior:
6788 // indoc! {"
6789 // «/*
6790 // * Lorem ipsum dolor sit amet,
6791 // * consectetur adipiscing elit.
6792 // */
6793 // /*
6794 // * Lorem ipsum dolor sit amet,
6795 // * consectetur adipiscing elit.
6796 // */ˇ»
6797 // "},
6798 // actual behaviour:
6799 indoc! {"
6800 «/*
6801 * Lorem ipsum dolor sit amet,
6802 * consectetur adipiscing elit. Lorem
6803 * ipsum dolor sit amet, consectetur
6804 * adipiscing elit.
6805 */ˇ»
6806 "},
6807 rust_lang.clone(),
6808 &mut cx,
6809 );
6810
6811 // TODO: same as above, but with delimiters on separate line
6812 // assert_rewrap(
6813 // indoc! {"
6814 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6815 // */
6816 // /*
6817 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6818 // "},
6819 // // desired:
6820 // // indoc! {"
6821 // // «/*
6822 // // * Lorem ipsum dolor sit amet,
6823 // // * consectetur adipiscing elit.
6824 // // */
6825 // // /*
6826 // // * Lorem ipsum dolor sit amet,
6827 // // * consectetur adipiscing elit.
6828 // // */ˇ»
6829 // // "},
6830 // // actual: (but with trailing w/s on the empty lines)
6831 // indoc! {"
6832 // «/*
6833 // * Lorem ipsum dolor sit amet,
6834 // * consectetur adipiscing elit.
6835 // *
6836 // */
6837 // /*
6838 // *
6839 // * Lorem ipsum dolor sit amet,
6840 // * consectetur adipiscing elit.
6841 // */ˇ»
6842 // "},
6843 // rust_lang.clone(),
6844 // &mut cx,
6845 // );
6846
6847 // TODO these are unhandled edge cases; not correct, just documenting known issues
6848 assert_rewrap(
6849 indoc! {"
6850 /*
6851 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6852 */
6853 /*
6854 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6855 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6856 "},
6857 // desired:
6858 // indoc! {"
6859 // /*
6860 // *ˇ Lorem ipsum dolor sit amet,
6861 // * consectetur adipiscing elit.
6862 // */
6863 // /*
6864 // *ˇ Lorem ipsum dolor sit amet,
6865 // * consectetur adipiscing elit.
6866 // */
6867 // /*
6868 // *ˇ Lorem ipsum dolor sit amet
6869 // */ /* consectetur adipiscing elit. */
6870 // "},
6871 // actual:
6872 indoc! {"
6873 /*
6874 //ˇ Lorem ipsum dolor sit amet,
6875 // consectetur adipiscing elit.
6876 */
6877 /*
6878 * //ˇ Lorem ipsum dolor sit amet,
6879 * consectetur adipiscing elit.
6880 */
6881 /*
6882 *ˇ Lorem ipsum dolor sit amet */ /*
6883 * consectetur adipiscing elit.
6884 */
6885 "},
6886 rust_lang,
6887 &mut cx,
6888 );
6889
6890 #[track_caller]
6891 fn assert_rewrap(
6892 unwrapped_text: &str,
6893 wrapped_text: &str,
6894 language: Arc<Language>,
6895 cx: &mut EditorTestContext,
6896 ) {
6897 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6898 cx.set_state(unwrapped_text);
6899 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6900 cx.assert_editor_state(wrapped_text);
6901 }
6902}
6903
6904#[gpui::test]
6905async fn test_hard_wrap(cx: &mut TestAppContext) {
6906 init_test(cx, |_| {});
6907 let mut cx = EditorTestContext::new(cx).await;
6908
6909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6910 cx.update_editor(|editor, _, cx| {
6911 editor.set_hard_wrap(Some(14), cx);
6912 });
6913
6914 cx.set_state(indoc!(
6915 "
6916 one two three ˇ
6917 "
6918 ));
6919 cx.simulate_input("four");
6920 cx.run_until_parked();
6921
6922 cx.assert_editor_state(indoc!(
6923 "
6924 one two three
6925 fourˇ
6926 "
6927 ));
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.newline(&Default::default(), window, cx);
6931 });
6932 cx.run_until_parked();
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 four
6937 ˇ
6938 "
6939 ));
6940
6941 cx.simulate_input("five");
6942 cx.run_until_parked();
6943 cx.assert_editor_state(indoc!(
6944 "
6945 one two three
6946 four
6947 fiveˇ
6948 "
6949 ));
6950
6951 cx.update_editor(|editor, window, cx| {
6952 editor.newline(&Default::default(), window, cx);
6953 });
6954 cx.run_until_parked();
6955 cx.simulate_input("# ");
6956 cx.run_until_parked();
6957 cx.assert_editor_state(indoc!(
6958 "
6959 one two three
6960 four
6961 five
6962 # ˇ
6963 "
6964 ));
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.newline(&Default::default(), window, cx);
6968 });
6969 cx.run_until_parked();
6970 cx.assert_editor_state(indoc!(
6971 "
6972 one two three
6973 four
6974 five
6975 #\x20
6976 #ˇ
6977 "
6978 ));
6979
6980 cx.simulate_input(" 6");
6981 cx.run_until_parked();
6982 cx.assert_editor_state(indoc!(
6983 "
6984 one two three
6985 four
6986 five
6987 #
6988 # 6ˇ
6989 "
6990 ));
6991}
6992
6993#[gpui::test]
6994async fn test_cut_line_ends(cx: &mut TestAppContext) {
6995 init_test(cx, |_| {});
6996
6997 let mut cx = EditorTestContext::new(cx).await;
6998
6999 cx.set_state(indoc! {"The quick brownˇ"});
7000 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7001 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7002
7003 cx.set_state(indoc! {"The emacs foxˇ"});
7004 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7005 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7006
7007 cx.set_state(indoc! {"
7008 The quick« brownˇ»
7009 fox jumps overˇ
7010 the lazy dog"});
7011 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7012 cx.assert_editor_state(indoc! {"
7013 The quickˇ
7014 ˇthe lazy dog"});
7015
7016 cx.set_state(indoc! {"
7017 The quick« brownˇ»
7018 fox jumps overˇ
7019 the lazy dog"});
7020 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7021 cx.assert_editor_state(indoc! {"
7022 The quickˇ
7023 fox jumps overˇthe lazy dog"});
7024
7025 cx.set_state(indoc! {"
7026 The quick« brownˇ»
7027 fox jumps overˇ
7028 the lazy dog"});
7029 cx.update_editor(|e, window, cx| {
7030 e.cut_to_end_of_line(
7031 &CutToEndOfLine {
7032 stop_at_newlines: true,
7033 },
7034 window,
7035 cx,
7036 )
7037 });
7038 cx.assert_editor_state(indoc! {"
7039 The quickˇ
7040 fox jumps overˇ
7041 the lazy dog"});
7042
7043 cx.set_state(indoc! {"
7044 The quick« brownˇ»
7045 fox jumps overˇ
7046 the lazy dog"});
7047 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7048 cx.assert_editor_state(indoc! {"
7049 The quickˇ
7050 fox jumps overˇthe lazy dog"});
7051}
7052
7053#[gpui::test]
7054async fn test_clipboard(cx: &mut TestAppContext) {
7055 init_test(cx, |_| {});
7056
7057 let mut cx = EditorTestContext::new(cx).await;
7058
7059 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7060 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7061 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7062
7063 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7064 cx.set_state("two ˇfour ˇsix ˇ");
7065 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7066 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7067
7068 // Paste again but with only two cursors. Since the number of cursors doesn't
7069 // match the number of slices in the clipboard, the entire clipboard text
7070 // is pasted at each cursor.
7071 cx.set_state("ˇtwo one✅ four three six five ˇ");
7072 cx.update_editor(|e, window, cx| {
7073 e.handle_input("( ", window, cx);
7074 e.paste(&Paste, window, cx);
7075 e.handle_input(") ", window, cx);
7076 });
7077 cx.assert_editor_state(
7078 &([
7079 "( one✅ ",
7080 "three ",
7081 "five ) ˇtwo one✅ four three six five ( one✅ ",
7082 "three ",
7083 "five ) ˇ",
7084 ]
7085 .join("\n")),
7086 );
7087
7088 // Cut with three selections, one of which is full-line.
7089 cx.set_state(indoc! {"
7090 1«2ˇ»3
7091 4ˇ567
7092 «8ˇ»9"});
7093 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7094 cx.assert_editor_state(indoc! {"
7095 1ˇ3
7096 ˇ9"});
7097
7098 // Paste with three selections, noticing how the copied selection that was full-line
7099 // gets inserted before the second cursor.
7100 cx.set_state(indoc! {"
7101 1ˇ3
7102 9ˇ
7103 «oˇ»ne"});
7104 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 12ˇ3
7107 4567
7108 9ˇ
7109 8ˇne"});
7110
7111 // Copy with a single cursor only, which writes the whole line into the clipboard.
7112 cx.set_state(indoc! {"
7113 The quick brown
7114 fox juˇmps over
7115 the lazy dog"});
7116 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7117 assert_eq!(
7118 cx.read_from_clipboard()
7119 .and_then(|item| item.text().as_deref().map(str::to_string)),
7120 Some("fox jumps over\n".to_string())
7121 );
7122
7123 // Paste with three selections, noticing how the copied full-line selection is inserted
7124 // before the empty selections but replaces the selection that is non-empty.
7125 cx.set_state(indoc! {"
7126 Tˇhe quick brown
7127 «foˇ»x jumps over
7128 tˇhe lazy dog"});
7129 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7130 cx.assert_editor_state(indoc! {"
7131 fox jumps over
7132 Tˇhe quick brown
7133 fox jumps over
7134 ˇx jumps over
7135 fox jumps over
7136 tˇhe lazy dog"});
7137}
7138
7139#[gpui::test]
7140async fn test_copy_trim(cx: &mut TestAppContext) {
7141 init_test(cx, |_| {});
7142
7143 let mut cx = EditorTestContext::new(cx).await;
7144 cx.set_state(
7145 r#" «for selection in selections.iter() {
7146 let mut start = selection.start;
7147 let mut end = selection.end;
7148 let is_entire_line = selection.is_empty();
7149 if is_entire_line {
7150 start = Point::new(start.row, 0);ˇ»
7151 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7152 }
7153 "#,
7154 );
7155 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7156 assert_eq!(
7157 cx.read_from_clipboard()
7158 .and_then(|item| item.text().as_deref().map(str::to_string)),
7159 Some(
7160 "for selection in selections.iter() {
7161 let mut start = selection.start;
7162 let mut end = selection.end;
7163 let is_entire_line = selection.is_empty();
7164 if is_entire_line {
7165 start = Point::new(start.row, 0);"
7166 .to_string()
7167 ),
7168 "Regular copying preserves all indentation selected",
7169 );
7170 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7171 assert_eq!(
7172 cx.read_from_clipboard()
7173 .and_then(|item| item.text().as_deref().map(str::to_string)),
7174 Some(
7175 "for selection in selections.iter() {
7176let mut start = selection.start;
7177let mut end = selection.end;
7178let is_entire_line = selection.is_empty();
7179if is_entire_line {
7180 start = Point::new(start.row, 0);"
7181 .to_string()
7182 ),
7183 "Copying with stripping should strip all leading whitespaces"
7184 );
7185
7186 cx.set_state(
7187 r#" « for selection in selections.iter() {
7188 let mut start = selection.start;
7189 let mut end = selection.end;
7190 let is_entire_line = selection.is_empty();
7191 if is_entire_line {
7192 start = Point::new(start.row, 0);ˇ»
7193 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7194 }
7195 "#,
7196 );
7197 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7198 assert_eq!(
7199 cx.read_from_clipboard()
7200 .and_then(|item| item.text().as_deref().map(str::to_string)),
7201 Some(
7202 " for selection in selections.iter() {
7203 let mut start = selection.start;
7204 let mut end = selection.end;
7205 let is_entire_line = selection.is_empty();
7206 if is_entire_line {
7207 start = Point::new(start.row, 0);"
7208 .to_string()
7209 ),
7210 "Regular copying preserves all indentation selected",
7211 );
7212 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7213 assert_eq!(
7214 cx.read_from_clipboard()
7215 .and_then(|item| item.text().as_deref().map(str::to_string)),
7216 Some(
7217 "for selection in selections.iter() {
7218let mut start = selection.start;
7219let mut end = selection.end;
7220let is_entire_line = selection.is_empty();
7221if is_entire_line {
7222 start = Point::new(start.row, 0);"
7223 .to_string()
7224 ),
7225 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7226 );
7227
7228 cx.set_state(
7229 r#" «ˇ for selection in selections.iter() {
7230 let mut start = selection.start;
7231 let mut end = selection.end;
7232 let is_entire_line = selection.is_empty();
7233 if is_entire_line {
7234 start = Point::new(start.row, 0);»
7235 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7236 }
7237 "#,
7238 );
7239 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7240 assert_eq!(
7241 cx.read_from_clipboard()
7242 .and_then(|item| item.text().as_deref().map(str::to_string)),
7243 Some(
7244 " for selection in selections.iter() {
7245 let mut start = selection.start;
7246 let mut end = selection.end;
7247 let is_entire_line = selection.is_empty();
7248 if is_entire_line {
7249 start = Point::new(start.row, 0);"
7250 .to_string()
7251 ),
7252 "Regular copying for reverse selection works the same",
7253 );
7254 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7255 assert_eq!(
7256 cx.read_from_clipboard()
7257 .and_then(|item| item.text().as_deref().map(str::to_string)),
7258 Some(
7259 "for selection in selections.iter() {
7260let mut start = selection.start;
7261let mut end = selection.end;
7262let is_entire_line = selection.is_empty();
7263if is_entire_line {
7264 start = Point::new(start.row, 0);"
7265 .to_string()
7266 ),
7267 "Copying with stripping for reverse selection works the same"
7268 );
7269
7270 cx.set_state(
7271 r#" for selection «in selections.iter() {
7272 let mut start = selection.start;
7273 let mut end = selection.end;
7274 let is_entire_line = selection.is_empty();
7275 if is_entire_line {
7276 start = Point::new(start.row, 0);ˇ»
7277 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7278 }
7279 "#,
7280 );
7281 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7282 assert_eq!(
7283 cx.read_from_clipboard()
7284 .and_then(|item| item.text().as_deref().map(str::to_string)),
7285 Some(
7286 "in selections.iter() {
7287 let mut start = selection.start;
7288 let mut end = selection.end;
7289 let is_entire_line = selection.is_empty();
7290 if is_entire_line {
7291 start = Point::new(start.row, 0);"
7292 .to_string()
7293 ),
7294 "When selecting past the indent, the copying works as usual",
7295 );
7296 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7297 assert_eq!(
7298 cx.read_from_clipboard()
7299 .and_then(|item| item.text().as_deref().map(str::to_string)),
7300 Some(
7301 "in selections.iter() {
7302 let mut start = selection.start;
7303 let mut end = selection.end;
7304 let is_entire_line = selection.is_empty();
7305 if is_entire_line {
7306 start = Point::new(start.row, 0);"
7307 .to_string()
7308 ),
7309 "When selecting past the indent, nothing is trimmed"
7310 );
7311
7312 cx.set_state(
7313 r#" «for selection in selections.iter() {
7314 let mut start = selection.start;
7315
7316 let mut end = selection.end;
7317 let is_entire_line = selection.is_empty();
7318 if is_entire_line {
7319 start = Point::new(start.row, 0);
7320ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7321 }
7322 "#,
7323 );
7324 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7325 assert_eq!(
7326 cx.read_from_clipboard()
7327 .and_then(|item| item.text().as_deref().map(str::to_string)),
7328 Some(
7329 "for selection in selections.iter() {
7330let mut start = selection.start;
7331
7332let mut end = selection.end;
7333let is_entire_line = selection.is_empty();
7334if is_entire_line {
7335 start = Point::new(start.row, 0);
7336"
7337 .to_string()
7338 ),
7339 "Copying with stripping should ignore empty lines"
7340 );
7341}
7342
7343#[gpui::test]
7344async fn test_paste_multiline(cx: &mut TestAppContext) {
7345 init_test(cx, |_| {});
7346
7347 let mut cx = EditorTestContext::new(cx).await;
7348 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7349
7350 // Cut an indented block, without the leading whitespace.
7351 cx.set_state(indoc! {"
7352 const a: B = (
7353 c(),
7354 «d(
7355 e,
7356 f
7357 )ˇ»
7358 );
7359 "});
7360 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7361 cx.assert_editor_state(indoc! {"
7362 const a: B = (
7363 c(),
7364 ˇ
7365 );
7366 "});
7367
7368 // Paste it at the same position.
7369 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7370 cx.assert_editor_state(indoc! {"
7371 const a: B = (
7372 c(),
7373 d(
7374 e,
7375 f
7376 )ˇ
7377 );
7378 "});
7379
7380 // Paste it at a line with a lower indent level.
7381 cx.set_state(indoc! {"
7382 ˇ
7383 const a: B = (
7384 c(),
7385 );
7386 "});
7387 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7388 cx.assert_editor_state(indoc! {"
7389 d(
7390 e,
7391 f
7392 )ˇ
7393 const a: B = (
7394 c(),
7395 );
7396 "});
7397
7398 // Cut an indented block, with the leading whitespace.
7399 cx.set_state(indoc! {"
7400 const a: B = (
7401 c(),
7402 « d(
7403 e,
7404 f
7405 )
7406 ˇ»);
7407 "});
7408 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7409 cx.assert_editor_state(indoc! {"
7410 const a: B = (
7411 c(),
7412 ˇ);
7413 "});
7414
7415 // Paste it at the same position.
7416 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7417 cx.assert_editor_state(indoc! {"
7418 const a: B = (
7419 c(),
7420 d(
7421 e,
7422 f
7423 )
7424 ˇ);
7425 "});
7426
7427 // Paste it at a line with a higher indent level.
7428 cx.set_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 fˇ
7434 )
7435 );
7436 "});
7437 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7438 cx.assert_editor_state(indoc! {"
7439 const a: B = (
7440 c(),
7441 d(
7442 e,
7443 f d(
7444 e,
7445 f
7446 )
7447 ˇ
7448 )
7449 );
7450 "});
7451
7452 // Copy an indented block, starting mid-line
7453 cx.set_state(indoc! {"
7454 const a: B = (
7455 c(),
7456 somethin«g(
7457 e,
7458 f
7459 )ˇ»
7460 );
7461 "});
7462 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7463
7464 // Paste it on a line with a lower indent level
7465 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7466 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7467 cx.assert_editor_state(indoc! {"
7468 const a: B = (
7469 c(),
7470 something(
7471 e,
7472 f
7473 )
7474 );
7475 g(
7476 e,
7477 f
7478 )ˇ"});
7479}
7480
7481#[gpui::test]
7482async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7483 init_test(cx, |_| {});
7484
7485 cx.write_to_clipboard(ClipboardItem::new_string(
7486 " d(\n e\n );\n".into(),
7487 ));
7488
7489 let mut cx = EditorTestContext::new(cx).await;
7490 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7491
7492 cx.set_state(indoc! {"
7493 fn a() {
7494 b();
7495 if c() {
7496 ˇ
7497 }
7498 }
7499 "});
7500
7501 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7502 cx.assert_editor_state(indoc! {"
7503 fn a() {
7504 b();
7505 if c() {
7506 d(
7507 e
7508 );
7509 ˇ
7510 }
7511 }
7512 "});
7513
7514 cx.set_state(indoc! {"
7515 fn a() {
7516 b();
7517 ˇ
7518 }
7519 "});
7520
7521 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7522 cx.assert_editor_state(indoc! {"
7523 fn a() {
7524 b();
7525 d(
7526 e
7527 );
7528 ˇ
7529 }
7530 "});
7531}
7532
7533#[gpui::test]
7534fn test_select_all(cx: &mut TestAppContext) {
7535 init_test(cx, |_| {});
7536
7537 let editor = cx.add_window(|window, cx| {
7538 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7539 build_editor(buffer, window, cx)
7540 });
7541 _ = editor.update(cx, |editor, window, cx| {
7542 editor.select_all(&SelectAll, window, cx);
7543 assert_eq!(
7544 editor.selections.display_ranges(cx),
7545 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7546 );
7547 });
7548}
7549
7550#[gpui::test]
7551fn test_select_line(cx: &mut TestAppContext) {
7552 init_test(cx, |_| {});
7553
7554 let editor = cx.add_window(|window, cx| {
7555 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7556 build_editor(buffer, window, cx)
7557 });
7558 _ = editor.update(cx, |editor, window, cx| {
7559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7560 s.select_display_ranges([
7561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7564 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7565 ])
7566 });
7567 editor.select_line(&SelectLine, window, cx);
7568 assert_eq!(
7569 editor.selections.display_ranges(cx),
7570 vec![
7571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7572 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7573 ]
7574 );
7575 });
7576
7577 _ = editor.update(cx, |editor, window, cx| {
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 editor.selections.display_ranges(cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 editor.selections.display_ranges(cx),
7592 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7593 );
7594 });
7595}
7596
7597#[gpui::test]
7598async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7599 init_test(cx, |_| {});
7600 let mut cx = EditorTestContext::new(cx).await;
7601
7602 #[track_caller]
7603 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7604 cx.set_state(initial_state);
7605 cx.update_editor(|e, window, cx| {
7606 e.split_selection_into_lines(&Default::default(), window, cx)
7607 });
7608 cx.assert_editor_state(expected_state);
7609 }
7610
7611 // Selection starts and ends at the middle of lines, left-to-right
7612 test(
7613 &mut cx,
7614 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7615 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7616 );
7617 // Same thing, right-to-left
7618 test(
7619 &mut cx,
7620 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7621 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7622 );
7623
7624 // Whole buffer, left-to-right, last line *doesn't* end with newline
7625 test(
7626 &mut cx,
7627 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7628 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7629 );
7630 // Same thing, right-to-left
7631 test(
7632 &mut cx,
7633 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7634 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7635 );
7636
7637 // Whole buffer, left-to-right, last line ends with newline
7638 test(
7639 &mut cx,
7640 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7641 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7642 );
7643 // Same thing, right-to-left
7644 test(
7645 &mut cx,
7646 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7647 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7648 );
7649
7650 // Starts at the end of a line, ends at the start of another
7651 test(
7652 &mut cx,
7653 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7654 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7655 );
7656}
7657
7658#[gpui::test]
7659async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7660 init_test(cx, |_| {});
7661
7662 let editor = cx.add_window(|window, cx| {
7663 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7664 build_editor(buffer, window, cx)
7665 });
7666
7667 // setup
7668 _ = editor.update(cx, |editor, window, cx| {
7669 editor.fold_creases(
7670 vec![
7671 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7672 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7673 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7674 ],
7675 true,
7676 window,
7677 cx,
7678 );
7679 assert_eq!(
7680 editor.display_text(cx),
7681 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7682 );
7683 });
7684
7685 _ = editor.update(cx, |editor, window, cx| {
7686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7687 s.select_display_ranges([
7688 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7689 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7691 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7692 ])
7693 });
7694 editor.split_selection_into_lines(&Default::default(), window, cx);
7695 assert_eq!(
7696 editor.display_text(cx),
7697 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7698 );
7699 });
7700 EditorTestContext::for_editor(editor, cx)
7701 .await
7702 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7703
7704 _ = editor.update(cx, |editor, window, cx| {
7705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7706 s.select_display_ranges([
7707 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7708 ])
7709 });
7710 editor.split_selection_into_lines(&Default::default(), window, cx);
7711 assert_eq!(
7712 editor.display_text(cx),
7713 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7714 );
7715 assert_eq!(
7716 editor.selections.display_ranges(cx),
7717 [
7718 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7719 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7720 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7721 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7722 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7723 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7724 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7725 ]
7726 );
7727 });
7728 EditorTestContext::for_editor(editor, cx)
7729 .await
7730 .assert_editor_state(
7731 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7732 );
7733}
7734
7735#[gpui::test]
7736async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7737 init_test(cx, |_| {});
7738
7739 let mut cx = EditorTestContext::new(cx).await;
7740
7741 cx.set_state(indoc!(
7742 r#"abc
7743 defˇghi
7744
7745 jk
7746 nlmo
7747 "#
7748 ));
7749
7750 cx.update_editor(|editor, window, cx| {
7751 editor.add_selection_above(&Default::default(), window, cx);
7752 });
7753
7754 cx.assert_editor_state(indoc!(
7755 r#"abcˇ
7756 defˇghi
7757
7758 jk
7759 nlmo
7760 "#
7761 ));
7762
7763 cx.update_editor(|editor, window, cx| {
7764 editor.add_selection_above(&Default::default(), window, cx);
7765 });
7766
7767 cx.assert_editor_state(indoc!(
7768 r#"abcˇ
7769 defˇghi
7770
7771 jk
7772 nlmo
7773 "#
7774 ));
7775
7776 cx.update_editor(|editor, window, cx| {
7777 editor.add_selection_below(&Default::default(), window, cx);
7778 });
7779
7780 cx.assert_editor_state(indoc!(
7781 r#"abc
7782 defˇghi
7783
7784 jk
7785 nlmo
7786 "#
7787 ));
7788
7789 cx.update_editor(|editor, window, cx| {
7790 editor.undo_selection(&Default::default(), window, cx);
7791 });
7792
7793 cx.assert_editor_state(indoc!(
7794 r#"abcˇ
7795 defˇghi
7796
7797 jk
7798 nlmo
7799 "#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.redo_selection(&Default::default(), window, cx);
7804 });
7805
7806 cx.assert_editor_state(indoc!(
7807 r#"abc
7808 defˇghi
7809
7810 jk
7811 nlmo
7812 "#
7813 ));
7814
7815 cx.update_editor(|editor, window, cx| {
7816 editor.add_selection_below(&Default::default(), window, cx);
7817 });
7818
7819 cx.assert_editor_state(indoc!(
7820 r#"abc
7821 defˇghi
7822 ˇ
7823 jk
7824 nlmo
7825 "#
7826 ));
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.add_selection_below(&Default::default(), window, cx);
7830 });
7831
7832 cx.assert_editor_state(indoc!(
7833 r#"abc
7834 defˇghi
7835 ˇ
7836 jkˇ
7837 nlmo
7838 "#
7839 ));
7840
7841 cx.update_editor(|editor, window, cx| {
7842 editor.add_selection_below(&Default::default(), window, cx);
7843 });
7844
7845 cx.assert_editor_state(indoc!(
7846 r#"abc
7847 defˇghi
7848 ˇ
7849 jkˇ
7850 nlmˇo
7851 "#
7852 ));
7853
7854 cx.update_editor(|editor, window, cx| {
7855 editor.add_selection_below(&Default::default(), window, cx);
7856 });
7857
7858 cx.assert_editor_state(indoc!(
7859 r#"abc
7860 defˇghi
7861 ˇ
7862 jkˇ
7863 nlmˇo
7864 ˇ"#
7865 ));
7866
7867 // change selections
7868 cx.set_state(indoc!(
7869 r#"abc
7870 def«ˇg»hi
7871
7872 jk
7873 nlmo
7874 "#
7875 ));
7876
7877 cx.update_editor(|editor, window, cx| {
7878 editor.add_selection_below(&Default::default(), window, cx);
7879 });
7880
7881 cx.assert_editor_state(indoc!(
7882 r#"abc
7883 def«ˇg»hi
7884
7885 jk
7886 nlm«ˇo»
7887 "#
7888 ));
7889
7890 cx.update_editor(|editor, window, cx| {
7891 editor.add_selection_below(&Default::default(), window, cx);
7892 });
7893
7894 cx.assert_editor_state(indoc!(
7895 r#"abc
7896 def«ˇg»hi
7897
7898 jk
7899 nlm«ˇo»
7900 "#
7901 ));
7902
7903 cx.update_editor(|editor, window, cx| {
7904 editor.add_selection_above(&Default::default(), window, cx);
7905 });
7906
7907 cx.assert_editor_state(indoc!(
7908 r#"abc
7909 def«ˇg»hi
7910
7911 jk
7912 nlmo
7913 "#
7914 ));
7915
7916 cx.update_editor(|editor, window, cx| {
7917 editor.add_selection_above(&Default::default(), window, cx);
7918 });
7919
7920 cx.assert_editor_state(indoc!(
7921 r#"abc
7922 def«ˇg»hi
7923
7924 jk
7925 nlmo
7926 "#
7927 ));
7928
7929 // Change selections again
7930 cx.set_state(indoc!(
7931 r#"a«bc
7932 defgˇ»hi
7933
7934 jk
7935 nlmo
7936 "#
7937 ));
7938
7939 cx.update_editor(|editor, window, cx| {
7940 editor.add_selection_below(&Default::default(), window, cx);
7941 });
7942
7943 cx.assert_editor_state(indoc!(
7944 r#"a«bcˇ»
7945 d«efgˇ»hi
7946
7947 j«kˇ»
7948 nlmo
7949 "#
7950 ));
7951
7952 cx.update_editor(|editor, window, cx| {
7953 editor.add_selection_below(&Default::default(), window, cx);
7954 });
7955 cx.assert_editor_state(indoc!(
7956 r#"a«bcˇ»
7957 d«efgˇ»hi
7958
7959 j«kˇ»
7960 n«lmoˇ»
7961 "#
7962 ));
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_above(&Default::default(), window, cx);
7965 });
7966
7967 cx.assert_editor_state(indoc!(
7968 r#"a«bcˇ»
7969 d«efgˇ»hi
7970
7971 j«kˇ»
7972 nlmo
7973 "#
7974 ));
7975
7976 // Change selections again
7977 cx.set_state(indoc!(
7978 r#"abc
7979 d«ˇefghi
7980
7981 jk
7982 nlm»o
7983 "#
7984 ));
7985
7986 cx.update_editor(|editor, window, cx| {
7987 editor.add_selection_above(&Default::default(), window, cx);
7988 });
7989
7990 cx.assert_editor_state(indoc!(
7991 r#"a«ˇbc»
7992 d«ˇef»ghi
7993
7994 j«ˇk»
7995 n«ˇlm»o
7996 "#
7997 ));
7998
7999 cx.update_editor(|editor, window, cx| {
8000 editor.add_selection_below(&Default::default(), window, cx);
8001 });
8002
8003 cx.assert_editor_state(indoc!(
8004 r#"abc
8005 d«ˇef»ghi
8006
8007 j«ˇk»
8008 n«ˇlm»o
8009 "#
8010 ));
8011}
8012
8013#[gpui::test]
8014async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8015 init_test(cx, |_| {});
8016 let mut cx = EditorTestContext::new(cx).await;
8017
8018 cx.set_state(indoc!(
8019 r#"line onˇe
8020 liˇne two
8021 line three
8022 line four"#
8023 ));
8024
8025 cx.update_editor(|editor, window, cx| {
8026 editor.add_selection_below(&Default::default(), window, cx);
8027 });
8028
8029 // test multiple cursors expand in the same direction
8030 cx.assert_editor_state(indoc!(
8031 r#"line onˇe
8032 liˇne twˇo
8033 liˇne three
8034 line four"#
8035 ));
8036
8037 cx.update_editor(|editor, window, cx| {
8038 editor.add_selection_below(&Default::default(), window, cx);
8039 });
8040
8041 cx.update_editor(|editor, window, cx| {
8042 editor.add_selection_below(&Default::default(), window, cx);
8043 });
8044
8045 // test multiple cursors expand below overflow
8046 cx.assert_editor_state(indoc!(
8047 r#"line onˇe
8048 liˇne twˇo
8049 liˇne thˇree
8050 liˇne foˇur"#
8051 ));
8052
8053 cx.update_editor(|editor, window, cx| {
8054 editor.add_selection_above(&Default::default(), window, cx);
8055 });
8056
8057 // test multiple cursors retrieves back correctly
8058 cx.assert_editor_state(indoc!(
8059 r#"line onˇe
8060 liˇne twˇo
8061 liˇne thˇree
8062 line four"#
8063 ));
8064
8065 cx.update_editor(|editor, window, cx| {
8066 editor.add_selection_above(&Default::default(), window, cx);
8067 });
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.add_selection_above(&Default::default(), window, cx);
8071 });
8072
8073 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8074 cx.assert_editor_state(indoc!(
8075 r#"liˇne onˇe
8076 liˇne two
8077 line three
8078 line four"#
8079 ));
8080
8081 cx.update_editor(|editor, window, cx| {
8082 editor.undo_selection(&Default::default(), window, cx);
8083 });
8084
8085 // test undo
8086 cx.assert_editor_state(indoc!(
8087 r#"line onˇe
8088 liˇne twˇo
8089 line three
8090 line four"#
8091 ));
8092
8093 cx.update_editor(|editor, window, cx| {
8094 editor.redo_selection(&Default::default(), window, cx);
8095 });
8096
8097 // test redo
8098 cx.assert_editor_state(indoc!(
8099 r#"liˇne onˇe
8100 liˇne two
8101 line three
8102 line four"#
8103 ));
8104
8105 cx.set_state(indoc!(
8106 r#"abcd
8107 ef«ghˇ»
8108 ijkl
8109 «mˇ»nop"#
8110 ));
8111
8112 cx.update_editor(|editor, window, cx| {
8113 editor.add_selection_above(&Default::default(), window, cx);
8114 });
8115
8116 // test multiple selections expand in the same direction
8117 cx.assert_editor_state(indoc!(
8118 r#"ab«cdˇ»
8119 ef«ghˇ»
8120 «iˇ»jkl
8121 «mˇ»nop"#
8122 ));
8123
8124 cx.update_editor(|editor, window, cx| {
8125 editor.add_selection_above(&Default::default(), window, cx);
8126 });
8127
8128 // test multiple selection upward overflow
8129 cx.assert_editor_state(indoc!(
8130 r#"ab«cdˇ»
8131 «eˇ»f«ghˇ»
8132 «iˇ»jkl
8133 «mˇ»nop"#
8134 ));
8135
8136 cx.update_editor(|editor, window, cx| {
8137 editor.add_selection_below(&Default::default(), window, cx);
8138 });
8139
8140 // test multiple selection retrieves back correctly
8141 cx.assert_editor_state(indoc!(
8142 r#"abcd
8143 ef«ghˇ»
8144 «iˇ»jkl
8145 «mˇ»nop"#
8146 ));
8147
8148 cx.update_editor(|editor, window, cx| {
8149 editor.add_selection_below(&Default::default(), window, cx);
8150 });
8151
8152 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8153 cx.assert_editor_state(indoc!(
8154 r#"abcd
8155 ef«ghˇ»
8156 ij«klˇ»
8157 «mˇ»nop"#
8158 ));
8159
8160 cx.update_editor(|editor, window, cx| {
8161 editor.undo_selection(&Default::default(), window, cx);
8162 });
8163
8164 // test undo
8165 cx.assert_editor_state(indoc!(
8166 r#"abcd
8167 ef«ghˇ»
8168 «iˇ»jkl
8169 «mˇ»nop"#
8170 ));
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.redo_selection(&Default::default(), window, cx);
8174 });
8175
8176 // test redo
8177 cx.assert_editor_state(indoc!(
8178 r#"abcd
8179 ef«ghˇ»
8180 ij«klˇ»
8181 «mˇ»nop"#
8182 ));
8183}
8184
8185#[gpui::test]
8186async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8187 init_test(cx, |_| {});
8188 let mut cx = EditorTestContext::new(cx).await;
8189
8190 cx.set_state(indoc!(
8191 r#"line onˇe
8192 liˇne two
8193 line three
8194 line four"#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 editor.add_selection_below(&Default::default(), window, cx);
8200 editor.add_selection_below(&Default::default(), window, cx);
8201 });
8202
8203 // initial state with two multi cursor groups
8204 cx.assert_editor_state(indoc!(
8205 r#"line onˇe
8206 liˇne twˇo
8207 liˇne thˇree
8208 liˇne foˇur"#
8209 ));
8210
8211 // add single cursor in middle - simulate opt click
8212 cx.update_editor(|editor, window, cx| {
8213 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8214 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8215 editor.end_selection(window, cx);
8216 });
8217
8218 cx.assert_editor_state(indoc!(
8219 r#"line onˇe
8220 liˇne twˇo
8221 liˇneˇ thˇree
8222 liˇne foˇur"#
8223 ));
8224
8225 cx.update_editor(|editor, window, cx| {
8226 editor.add_selection_above(&Default::default(), window, cx);
8227 });
8228
8229 // test new added selection expands above and existing selection shrinks
8230 cx.assert_editor_state(indoc!(
8231 r#"line onˇe
8232 liˇneˇ twˇo
8233 liˇneˇ thˇree
8234 line four"#
8235 ));
8236
8237 cx.update_editor(|editor, window, cx| {
8238 editor.add_selection_above(&Default::default(), window, cx);
8239 });
8240
8241 // test new added selection expands above and existing selection shrinks
8242 cx.assert_editor_state(indoc!(
8243 r#"lineˇ onˇe
8244 liˇneˇ twˇo
8245 lineˇ three
8246 line four"#
8247 ));
8248
8249 // intial state with two selection groups
8250 cx.set_state(indoc!(
8251 r#"abcd
8252 ef«ghˇ»
8253 ijkl
8254 «mˇ»nop"#
8255 ));
8256
8257 cx.update_editor(|editor, window, cx| {
8258 editor.add_selection_above(&Default::default(), window, cx);
8259 editor.add_selection_above(&Default::default(), window, cx);
8260 });
8261
8262 cx.assert_editor_state(indoc!(
8263 r#"ab«cdˇ»
8264 «eˇ»f«ghˇ»
8265 «iˇ»jkl
8266 «mˇ»nop"#
8267 ));
8268
8269 // add single selection in middle - simulate opt drag
8270 cx.update_editor(|editor, window, cx| {
8271 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8272 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8273 editor.update_selection(
8274 DisplayPoint::new(DisplayRow(2), 4),
8275 0,
8276 gpui::Point::<f32>::default(),
8277 window,
8278 cx,
8279 );
8280 editor.end_selection(window, cx);
8281 });
8282
8283 cx.assert_editor_state(indoc!(
8284 r#"ab«cdˇ»
8285 «eˇ»f«ghˇ»
8286 «iˇ»jk«lˇ»
8287 «mˇ»nop"#
8288 ));
8289
8290 cx.update_editor(|editor, window, cx| {
8291 editor.add_selection_below(&Default::default(), window, cx);
8292 });
8293
8294 // test new added selection expands below, others shrinks from above
8295 cx.assert_editor_state(indoc!(
8296 r#"abcd
8297 ef«ghˇ»
8298 «iˇ»jk«lˇ»
8299 «mˇ»no«pˇ»"#
8300 ));
8301}
8302
8303#[gpui::test]
8304async fn test_select_next(cx: &mut TestAppContext) {
8305 init_test(cx, |_| {});
8306
8307 let mut cx = EditorTestContext::new(cx).await;
8308 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8309
8310 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8311 .unwrap();
8312 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8313
8314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8315 .unwrap();
8316 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8317
8318 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8319 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8320
8321 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8322 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8323
8324 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8325 .unwrap();
8326 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8327
8328 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8329 .unwrap();
8330 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8331
8332 // Test selection direction should be preserved
8333 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8334
8335 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8336 .unwrap();
8337 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8338}
8339
8340#[gpui::test]
8341async fn test_select_all_matches(cx: &mut TestAppContext) {
8342 init_test(cx, |_| {});
8343
8344 let mut cx = EditorTestContext::new(cx).await;
8345
8346 // Test caret-only selections
8347 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8348 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8349 .unwrap();
8350 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8351
8352 // Test left-to-right selections
8353 cx.set_state("abc\n«abcˇ»\nabc");
8354 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8355 .unwrap();
8356 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8357
8358 // Test right-to-left selections
8359 cx.set_state("abc\n«ˇabc»\nabc");
8360 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8361 .unwrap();
8362 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8363
8364 // Test selecting whitespace with caret selection
8365 cx.set_state("abc\nˇ abc\nabc");
8366 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8367 .unwrap();
8368 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8369
8370 // Test selecting whitespace with left-to-right selection
8371 cx.set_state("abc\n«ˇ »abc\nabc");
8372 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8373 .unwrap();
8374 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8375
8376 // Test no matches with right-to-left selection
8377 cx.set_state("abc\n« ˇ»abc\nabc");
8378 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8379 .unwrap();
8380 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8381
8382 // Test with a single word and clip_at_line_ends=true (#29823)
8383 cx.set_state("aˇbc");
8384 cx.update_editor(|e, window, cx| {
8385 e.set_clip_at_line_ends(true, cx);
8386 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8387 e.set_clip_at_line_ends(false, cx);
8388 });
8389 cx.assert_editor_state("«abcˇ»");
8390}
8391
8392#[gpui::test]
8393async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let mut cx = EditorTestContext::new(cx).await;
8397
8398 let large_body_1 = "\nd".repeat(200);
8399 let large_body_2 = "\ne".repeat(200);
8400
8401 cx.set_state(&format!(
8402 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8403 ));
8404 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8405 let scroll_position = editor.scroll_position(cx);
8406 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8407 scroll_position
8408 });
8409
8410 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8411 .unwrap();
8412 cx.assert_editor_state(&format!(
8413 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8414 ));
8415 let scroll_position_after_selection =
8416 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8417 assert_eq!(
8418 initial_scroll_position, scroll_position_after_selection,
8419 "Scroll position should not change after selecting all matches"
8420 );
8421}
8422
8423#[gpui::test]
8424async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8425 init_test(cx, |_| {});
8426
8427 let mut cx = EditorLspTestContext::new_rust(
8428 lsp::ServerCapabilities {
8429 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8430 ..Default::default()
8431 },
8432 cx,
8433 )
8434 .await;
8435
8436 cx.set_state(indoc! {"
8437 line 1
8438 line 2
8439 linˇe 3
8440 line 4
8441 line 5
8442 "});
8443
8444 // Make an edit
8445 cx.update_editor(|editor, window, cx| {
8446 editor.handle_input("X", window, cx);
8447 });
8448
8449 // Move cursor to a different position
8450 cx.update_editor(|editor, window, cx| {
8451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8452 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8453 });
8454 });
8455
8456 cx.assert_editor_state(indoc! {"
8457 line 1
8458 line 2
8459 linXe 3
8460 line 4
8461 liˇne 5
8462 "});
8463
8464 cx.lsp
8465 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8466 Ok(Some(vec![lsp::TextEdit::new(
8467 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8468 "PREFIX ".to_string(),
8469 )]))
8470 });
8471
8472 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8473 .unwrap()
8474 .await
8475 .unwrap();
8476
8477 cx.assert_editor_state(indoc! {"
8478 PREFIX line 1
8479 line 2
8480 linXe 3
8481 line 4
8482 liˇne 5
8483 "});
8484
8485 // Undo formatting
8486 cx.update_editor(|editor, window, cx| {
8487 editor.undo(&Default::default(), window, cx);
8488 });
8489
8490 // Verify cursor moved back to position after edit
8491 cx.assert_editor_state(indoc! {"
8492 line 1
8493 line 2
8494 linXˇe 3
8495 line 4
8496 line 5
8497 "});
8498}
8499
8500#[gpui::test]
8501async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8502 init_test(cx, |_| {});
8503
8504 let mut cx = EditorTestContext::new(cx).await;
8505
8506 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8507 cx.update_editor(|editor, window, cx| {
8508 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8509 });
8510
8511 cx.set_state(indoc! {"
8512 line 1
8513 line 2
8514 linˇe 3
8515 line 4
8516 line 5
8517 line 6
8518 line 7
8519 line 8
8520 line 9
8521 line 10
8522 "});
8523
8524 let snapshot = cx.buffer_snapshot();
8525 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8526
8527 cx.update(|_, cx| {
8528 provider.update(cx, |provider, _| {
8529 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8530 id: None,
8531 edits: vec![(edit_position..edit_position, "X".into())],
8532 edit_preview: None,
8533 }))
8534 })
8535 });
8536
8537 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8538 cx.update_editor(|editor, window, cx| {
8539 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8540 });
8541
8542 cx.assert_editor_state(indoc! {"
8543 line 1
8544 line 2
8545 lineXˇ 3
8546 line 4
8547 line 5
8548 line 6
8549 line 7
8550 line 8
8551 line 9
8552 line 10
8553 "});
8554
8555 cx.update_editor(|editor, window, cx| {
8556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8557 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8558 });
8559 });
8560
8561 cx.assert_editor_state(indoc! {"
8562 line 1
8563 line 2
8564 lineX 3
8565 line 4
8566 line 5
8567 line 6
8568 line 7
8569 line 8
8570 line 9
8571 liˇne 10
8572 "});
8573
8574 cx.update_editor(|editor, window, cx| {
8575 editor.undo(&Default::default(), window, cx);
8576 });
8577
8578 cx.assert_editor_state(indoc! {"
8579 line 1
8580 line 2
8581 lineˇ 3
8582 line 4
8583 line 5
8584 line 6
8585 line 7
8586 line 8
8587 line 9
8588 line 10
8589 "});
8590}
8591
8592#[gpui::test]
8593async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8594 init_test(cx, |_| {});
8595
8596 let mut cx = EditorTestContext::new(cx).await;
8597 cx.set_state(
8598 r#"let foo = 2;
8599lˇet foo = 2;
8600let fooˇ = 2;
8601let foo = 2;
8602let foo = ˇ2;"#,
8603 );
8604
8605 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8606 .unwrap();
8607 cx.assert_editor_state(
8608 r#"let foo = 2;
8609«letˇ» foo = 2;
8610let «fooˇ» = 2;
8611let foo = 2;
8612let foo = «2ˇ»;"#,
8613 );
8614
8615 // noop for multiple selections with different contents
8616 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8617 .unwrap();
8618 cx.assert_editor_state(
8619 r#"let foo = 2;
8620«letˇ» foo = 2;
8621let «fooˇ» = 2;
8622let foo = 2;
8623let foo = «2ˇ»;"#,
8624 );
8625
8626 // Test last selection direction should be preserved
8627 cx.set_state(
8628 r#"let foo = 2;
8629let foo = 2;
8630let «fooˇ» = 2;
8631let «ˇfoo» = 2;
8632let foo = 2;"#,
8633 );
8634
8635 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8636 .unwrap();
8637 cx.assert_editor_state(
8638 r#"let foo = 2;
8639let foo = 2;
8640let «fooˇ» = 2;
8641let «ˇfoo» = 2;
8642let «ˇfoo» = 2;"#,
8643 );
8644}
8645
8646#[gpui::test]
8647async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8648 init_test(cx, |_| {});
8649
8650 let mut cx =
8651 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8652
8653 cx.assert_editor_state(indoc! {"
8654 ˇbbb
8655 ccc
8656
8657 bbb
8658 ccc
8659 "});
8660 cx.dispatch_action(SelectPrevious::default());
8661 cx.assert_editor_state(indoc! {"
8662 «bbbˇ»
8663 ccc
8664
8665 bbb
8666 ccc
8667 "});
8668 cx.dispatch_action(SelectPrevious::default());
8669 cx.assert_editor_state(indoc! {"
8670 «bbbˇ»
8671 ccc
8672
8673 «bbbˇ»
8674 ccc
8675 "});
8676}
8677
8678#[gpui::test]
8679async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8680 init_test(cx, |_| {});
8681
8682 let mut cx = EditorTestContext::new(cx).await;
8683 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8684
8685 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8686 .unwrap();
8687 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8688
8689 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8690 .unwrap();
8691 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8692
8693 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8694 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8695
8696 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8697 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8698
8699 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8700 .unwrap();
8701 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8702
8703 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8704 .unwrap();
8705 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8706}
8707
8708#[gpui::test]
8709async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8710 init_test(cx, |_| {});
8711
8712 let mut cx = EditorTestContext::new(cx).await;
8713 cx.set_state("aˇ");
8714
8715 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8716 .unwrap();
8717 cx.assert_editor_state("«aˇ»");
8718 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8719 .unwrap();
8720 cx.assert_editor_state("«aˇ»");
8721}
8722
8723#[gpui::test]
8724async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728 cx.set_state(
8729 r#"let foo = 2;
8730lˇet foo = 2;
8731let fooˇ = 2;
8732let foo = 2;
8733let foo = ˇ2;"#,
8734 );
8735
8736 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8737 .unwrap();
8738 cx.assert_editor_state(
8739 r#"let foo = 2;
8740«letˇ» foo = 2;
8741let «fooˇ» = 2;
8742let foo = 2;
8743let foo = «2ˇ»;"#,
8744 );
8745
8746 // noop for multiple selections with different contents
8747 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756}
8757
8758#[gpui::test]
8759async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8760 init_test(cx, |_| {});
8761
8762 let mut cx = EditorTestContext::new(cx).await;
8763 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8764
8765 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8766 .unwrap();
8767 // selection direction is preserved
8768 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8769
8770 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8771 .unwrap();
8772 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8773
8774 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8775 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8776
8777 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8778 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8779
8780 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8783
8784 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8785 .unwrap();
8786 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8787}
8788
8789#[gpui::test]
8790async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8791 init_test(cx, |_| {});
8792
8793 let language = Arc::new(Language::new(
8794 LanguageConfig::default(),
8795 Some(tree_sitter_rust::LANGUAGE.into()),
8796 ));
8797
8798 let text = r#"
8799 use mod1::mod2::{mod3, mod4};
8800
8801 fn fn_1(param1: bool, param2: &str) {
8802 let var1 = "text";
8803 }
8804 "#
8805 .unindent();
8806
8807 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8809 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8810
8811 editor
8812 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8813 .await;
8814
8815 editor.update_in(cx, |editor, window, cx| {
8816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8817 s.select_display_ranges([
8818 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8819 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8820 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8821 ]);
8822 });
8823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8824 });
8825 editor.update(cx, |editor, cx| {
8826 assert_text_with_selections(
8827 editor,
8828 indoc! {r#"
8829 use mod1::mod2::{mod3, «mod4ˇ»};
8830
8831 fn fn_1«ˇ(param1: bool, param2: &str)» {
8832 let var1 = "«ˇtext»";
8833 }
8834 "#},
8835 cx,
8836 );
8837 });
8838
8839 editor.update_in(cx, |editor, window, cx| {
8840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8841 });
8842 editor.update(cx, |editor, cx| {
8843 assert_text_with_selections(
8844 editor,
8845 indoc! {r#"
8846 use mod1::mod2::«{mod3, mod4}ˇ»;
8847
8848 «ˇfn fn_1(param1: bool, param2: &str) {
8849 let var1 = "text";
8850 }»
8851 "#},
8852 cx,
8853 );
8854 });
8855
8856 editor.update_in(cx, |editor, window, cx| {
8857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8858 });
8859 assert_eq!(
8860 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8861 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8862 );
8863
8864 // Trying to expand the selected syntax node one more time has no effect.
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 assert_eq!(
8869 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8870 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8871 );
8872
8873 editor.update_in(cx, |editor, window, cx| {
8874 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8875 });
8876 editor.update(cx, |editor, cx| {
8877 assert_text_with_selections(
8878 editor,
8879 indoc! {r#"
8880 use mod1::mod2::«{mod3, mod4}ˇ»;
8881
8882 «ˇfn fn_1(param1: bool, param2: &str) {
8883 let var1 = "text";
8884 }»
8885 "#},
8886 cx,
8887 );
8888 });
8889
8890 editor.update_in(cx, |editor, window, cx| {
8891 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8892 });
8893 editor.update(cx, |editor, cx| {
8894 assert_text_with_selections(
8895 editor,
8896 indoc! {r#"
8897 use mod1::mod2::{mod3, «mod4ˇ»};
8898
8899 fn fn_1«ˇ(param1: bool, param2: &str)» {
8900 let var1 = "«ˇtext»";
8901 }
8902 "#},
8903 cx,
8904 );
8905 });
8906
8907 editor.update_in(cx, |editor, window, cx| {
8908 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8909 });
8910 editor.update(cx, |editor, cx| {
8911 assert_text_with_selections(
8912 editor,
8913 indoc! {r#"
8914 use mod1::mod2::{mod3, moˇd4};
8915
8916 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8917 let var1 = "teˇxt";
8918 }
8919 "#},
8920 cx,
8921 );
8922 });
8923
8924 // Trying to shrink the selected syntax node one more time has no effect.
8925 editor.update_in(cx, |editor, window, cx| {
8926 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8927 });
8928 editor.update_in(cx, |editor, _, cx| {
8929 assert_text_with_selections(
8930 editor,
8931 indoc! {r#"
8932 use mod1::mod2::{mod3, moˇd4};
8933
8934 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8935 let var1 = "teˇxt";
8936 }
8937 "#},
8938 cx,
8939 );
8940 });
8941
8942 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8943 // a fold.
8944 editor.update_in(cx, |editor, window, cx| {
8945 editor.fold_creases(
8946 vec![
8947 Crease::simple(
8948 Point::new(0, 21)..Point::new(0, 24),
8949 FoldPlaceholder::test(),
8950 ),
8951 Crease::simple(
8952 Point::new(3, 20)..Point::new(3, 22),
8953 FoldPlaceholder::test(),
8954 ),
8955 ],
8956 true,
8957 window,
8958 cx,
8959 );
8960 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8961 });
8962 editor.update(cx, |editor, cx| {
8963 assert_text_with_selections(
8964 editor,
8965 indoc! {r#"
8966 use mod1::mod2::«{mod3, mod4}ˇ»;
8967
8968 fn fn_1«ˇ(param1: bool, param2: &str)» {
8969 let var1 = "«ˇtext»";
8970 }
8971 "#},
8972 cx,
8973 );
8974 });
8975}
8976
8977#[gpui::test]
8978async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8979 init_test(cx, |_| {});
8980
8981 let language = Arc::new(Language::new(
8982 LanguageConfig::default(),
8983 Some(tree_sitter_rust::LANGUAGE.into()),
8984 ));
8985
8986 let text = "let a = 2;";
8987
8988 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8989 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8990 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8991
8992 editor
8993 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8994 .await;
8995
8996 // Test case 1: Cursor at end of word
8997 editor.update_in(cx, |editor, window, cx| {
8998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8999 s.select_display_ranges([
9000 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9001 ]);
9002 });
9003 });
9004 editor.update(cx, |editor, cx| {
9005 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9006 });
9007 editor.update_in(cx, |editor, window, cx| {
9008 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9009 });
9010 editor.update(cx, |editor, cx| {
9011 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9012 });
9013 editor.update_in(cx, |editor, window, cx| {
9014 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9015 });
9016 editor.update(cx, |editor, cx| {
9017 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9018 });
9019
9020 // Test case 2: Cursor at end of statement
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9023 s.select_display_ranges([
9024 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9025 ]);
9026 });
9027 });
9028 editor.update(cx, |editor, cx| {
9029 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9030 });
9031 editor.update_in(cx, |editor, window, cx| {
9032 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9033 });
9034 editor.update(cx, |editor, cx| {
9035 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9036 });
9037}
9038
9039#[gpui::test]
9040async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9041 init_test(cx, |_| {});
9042
9043 let language = Arc::new(Language::new(
9044 LanguageConfig {
9045 name: "JavaScript".into(),
9046 ..Default::default()
9047 },
9048 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9049 ));
9050
9051 let text = r#"
9052 let a = {
9053 key: "value",
9054 };
9055 "#
9056 .unindent();
9057
9058 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9059 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9060 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9061
9062 editor
9063 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9064 .await;
9065
9066 // Test case 1: Cursor after '{'
9067 editor.update_in(cx, |editor, window, cx| {
9068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9069 s.select_display_ranges([
9070 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9071 ]);
9072 });
9073 });
9074 editor.update(cx, |editor, cx| {
9075 assert_text_with_selections(
9076 editor,
9077 indoc! {r#"
9078 let a = {ˇ
9079 key: "value",
9080 };
9081 "#},
9082 cx,
9083 );
9084 });
9085 editor.update_in(cx, |editor, window, cx| {
9086 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9087 });
9088 editor.update(cx, |editor, cx| {
9089 assert_text_with_selections(
9090 editor,
9091 indoc! {r#"
9092 let a = «ˇ{
9093 key: "value",
9094 }»;
9095 "#},
9096 cx,
9097 );
9098 });
9099
9100 // Test case 2: Cursor after ':'
9101 editor.update_in(cx, |editor, window, cx| {
9102 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9103 s.select_display_ranges([
9104 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9105 ]);
9106 });
9107 });
9108 editor.update(cx, |editor, cx| {
9109 assert_text_with_selections(
9110 editor,
9111 indoc! {r#"
9112 let a = {
9113 key:ˇ "value",
9114 };
9115 "#},
9116 cx,
9117 );
9118 });
9119 editor.update_in(cx, |editor, window, cx| {
9120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9121 });
9122 editor.update(cx, |editor, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 let a = {
9127 «ˇkey: "value"»,
9128 };
9129 "#},
9130 cx,
9131 );
9132 });
9133 editor.update_in(cx, |editor, window, cx| {
9134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9135 });
9136 editor.update(cx, |editor, cx| {
9137 assert_text_with_selections(
9138 editor,
9139 indoc! {r#"
9140 let a = «ˇ{
9141 key: "value",
9142 }»;
9143 "#},
9144 cx,
9145 );
9146 });
9147
9148 // Test case 3: Cursor after ','
9149 editor.update_in(cx, |editor, window, cx| {
9150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9151 s.select_display_ranges([
9152 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9153 ]);
9154 });
9155 });
9156 editor.update(cx, |editor, cx| {
9157 assert_text_with_selections(
9158 editor,
9159 indoc! {r#"
9160 let a = {
9161 key: "value",ˇ
9162 };
9163 "#},
9164 cx,
9165 );
9166 });
9167 editor.update_in(cx, |editor, window, cx| {
9168 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9169 });
9170 editor.update(cx, |editor, cx| {
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 let a = «ˇ{
9175 key: "value",
9176 }»;
9177 "#},
9178 cx,
9179 );
9180 });
9181
9182 // Test case 4: Cursor after ';'
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9185 s.select_display_ranges([
9186 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9187 ]);
9188 });
9189 });
9190 editor.update(cx, |editor, cx| {
9191 assert_text_with_selections(
9192 editor,
9193 indoc! {r#"
9194 let a = {
9195 key: "value",
9196 };ˇ
9197 "#},
9198 cx,
9199 );
9200 });
9201 editor.update_in(cx, |editor, window, cx| {
9202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9203 });
9204 editor.update(cx, |editor, cx| {
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 «ˇlet a = {
9209 key: "value",
9210 };
9211 »"#},
9212 cx,
9213 );
9214 });
9215}
9216
9217#[gpui::test]
9218async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9219 init_test(cx, |_| {});
9220
9221 let language = Arc::new(Language::new(
9222 LanguageConfig::default(),
9223 Some(tree_sitter_rust::LANGUAGE.into()),
9224 ));
9225
9226 let text = r#"
9227 use mod1::mod2::{mod3, mod4};
9228
9229 fn fn_1(param1: bool, param2: &str) {
9230 let var1 = "hello world";
9231 }
9232 "#
9233 .unindent();
9234
9235 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9236 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9237 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9238
9239 editor
9240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9241 .await;
9242
9243 // Test 1: Cursor on a letter of a string word
9244 editor.update_in(cx, |editor, window, cx| {
9245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9246 s.select_display_ranges([
9247 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9248 ]);
9249 });
9250 });
9251 editor.update_in(cx, |editor, window, cx| {
9252 assert_text_with_selections(
9253 editor,
9254 indoc! {r#"
9255 use mod1::mod2::{mod3, mod4};
9256
9257 fn fn_1(param1: bool, param2: &str) {
9258 let var1 = "hˇello world";
9259 }
9260 "#},
9261 cx,
9262 );
9263 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9264 assert_text_with_selections(
9265 editor,
9266 indoc! {r#"
9267 use mod1::mod2::{mod3, mod4};
9268
9269 fn fn_1(param1: bool, param2: &str) {
9270 let var1 = "«ˇhello» world";
9271 }
9272 "#},
9273 cx,
9274 );
9275 });
9276
9277 // Test 2: Partial selection within a word
9278 editor.update_in(cx, |editor, window, cx| {
9279 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9280 s.select_display_ranges([
9281 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9282 ]);
9283 });
9284 });
9285 editor.update_in(cx, |editor, window, cx| {
9286 assert_text_with_selections(
9287 editor,
9288 indoc! {r#"
9289 use mod1::mod2::{mod3, mod4};
9290
9291 fn fn_1(param1: bool, param2: &str) {
9292 let var1 = "h«elˇ»lo world";
9293 }
9294 "#},
9295 cx,
9296 );
9297 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9298 assert_text_with_selections(
9299 editor,
9300 indoc! {r#"
9301 use mod1::mod2::{mod3, mod4};
9302
9303 fn fn_1(param1: bool, param2: &str) {
9304 let var1 = "«ˇhello» world";
9305 }
9306 "#},
9307 cx,
9308 );
9309 });
9310
9311 // Test 3: Complete word already selected
9312 editor.update_in(cx, |editor, window, cx| {
9313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9314 s.select_display_ranges([
9315 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9316 ]);
9317 });
9318 });
9319 editor.update_in(cx, |editor, window, cx| {
9320 assert_text_with_selections(
9321 editor,
9322 indoc! {r#"
9323 use mod1::mod2::{mod3, mod4};
9324
9325 fn fn_1(param1: bool, param2: &str) {
9326 let var1 = "«helloˇ» world";
9327 }
9328 "#},
9329 cx,
9330 );
9331 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9332 assert_text_with_selections(
9333 editor,
9334 indoc! {r#"
9335 use mod1::mod2::{mod3, mod4};
9336
9337 fn fn_1(param1: bool, param2: &str) {
9338 let var1 = "«hello worldˇ»";
9339 }
9340 "#},
9341 cx,
9342 );
9343 });
9344
9345 // Test 4: Selection spanning across words
9346 editor.update_in(cx, |editor, window, cx| {
9347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9348 s.select_display_ranges([
9349 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9350 ]);
9351 });
9352 });
9353 editor.update_in(cx, |editor, window, cx| {
9354 assert_text_with_selections(
9355 editor,
9356 indoc! {r#"
9357 use mod1::mod2::{mod3, mod4};
9358
9359 fn fn_1(param1: bool, param2: &str) {
9360 let var1 = "hel«lo woˇ»rld";
9361 }
9362 "#},
9363 cx,
9364 );
9365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9366 assert_text_with_selections(
9367 editor,
9368 indoc! {r#"
9369 use mod1::mod2::{mod3, mod4};
9370
9371 fn fn_1(param1: bool, param2: &str) {
9372 let var1 = "«ˇhello world»";
9373 }
9374 "#},
9375 cx,
9376 );
9377 });
9378
9379 // Test 5: Expansion beyond string
9380 editor.update_in(cx, |editor, window, cx| {
9381 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9382 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9383 assert_text_with_selections(
9384 editor,
9385 indoc! {r#"
9386 use mod1::mod2::{mod3, mod4};
9387
9388 fn fn_1(param1: bool, param2: &str) {
9389 «ˇlet var1 = "hello world";»
9390 }
9391 "#},
9392 cx,
9393 );
9394 });
9395}
9396
9397#[gpui::test]
9398async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9399 init_test(cx, |_| {});
9400
9401 let mut cx = EditorTestContext::new(cx).await;
9402
9403 let language = Arc::new(Language::new(
9404 LanguageConfig::default(),
9405 Some(tree_sitter_rust::LANGUAGE.into()),
9406 ));
9407
9408 cx.update_buffer(|buffer, cx| {
9409 buffer.set_language(Some(language), cx);
9410 });
9411
9412 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9413 cx.update_editor(|editor, window, cx| {
9414 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9415 });
9416
9417 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9418
9419 cx.set_state(indoc! { r#"fn a() {
9420 // what
9421 // a
9422 // ˇlong
9423 // method
9424 // I
9425 // sure
9426 // hope
9427 // it
9428 // works
9429 }"# });
9430
9431 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9432 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9433 cx.update(|_, cx| {
9434 multi_buffer.update(cx, |multi_buffer, cx| {
9435 multi_buffer.set_excerpts_for_path(
9436 PathKey::for_buffer(&buffer, cx),
9437 buffer,
9438 [Point::new(1, 0)..Point::new(1, 0)],
9439 3,
9440 cx,
9441 );
9442 });
9443 });
9444
9445 let editor2 = cx.new_window_entity(|window, cx| {
9446 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9447 });
9448
9449 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9450 cx.update_editor(|editor, window, cx| {
9451 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9452 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9453 })
9454 });
9455
9456 cx.assert_editor_state(indoc! { "
9457 fn a() {
9458 // what
9459 // a
9460 ˇ // long
9461 // method"});
9462
9463 cx.update_editor(|editor, window, cx| {
9464 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9465 });
9466
9467 // Although we could potentially make the action work when the syntax node
9468 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9469 // did. Maybe we could also expand the excerpt to contain the range?
9470 cx.assert_editor_state(indoc! { "
9471 fn a() {
9472 // what
9473 // a
9474 ˇ // long
9475 // method"});
9476}
9477
9478#[gpui::test]
9479async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9480 init_test(cx, |_| {});
9481
9482 let base_text = r#"
9483 impl A {
9484 // this is an uncommitted comment
9485
9486 fn b() {
9487 c();
9488 }
9489
9490 // this is another uncommitted comment
9491
9492 fn d() {
9493 // e
9494 // f
9495 }
9496 }
9497
9498 fn g() {
9499 // h
9500 }
9501 "#
9502 .unindent();
9503
9504 let text = r#"
9505 ˇimpl A {
9506
9507 fn b() {
9508 c();
9509 }
9510
9511 fn d() {
9512 // e
9513 // f
9514 }
9515 }
9516
9517 fn g() {
9518 // h
9519 }
9520 "#
9521 .unindent();
9522
9523 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9524 cx.set_state(&text);
9525 cx.set_head_text(&base_text);
9526 cx.update_editor(|editor, window, cx| {
9527 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9528 });
9529
9530 cx.assert_state_with_diff(
9531 "
9532 ˇimpl A {
9533 - // this is an uncommitted comment
9534
9535 fn b() {
9536 c();
9537 }
9538
9539 - // this is another uncommitted comment
9540 -
9541 fn d() {
9542 // e
9543 // f
9544 }
9545 }
9546
9547 fn g() {
9548 // h
9549 }
9550 "
9551 .unindent(),
9552 );
9553
9554 let expected_display_text = "
9555 impl A {
9556 // this is an uncommitted comment
9557
9558 fn b() {
9559 ⋯
9560 }
9561
9562 // this is another uncommitted comment
9563
9564 fn d() {
9565 ⋯
9566 }
9567 }
9568
9569 fn g() {
9570 ⋯
9571 }
9572 "
9573 .unindent();
9574
9575 cx.update_editor(|editor, window, cx| {
9576 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9577 assert_eq!(editor.display_text(cx), expected_display_text);
9578 });
9579}
9580
9581#[gpui::test]
9582async fn test_autoindent(cx: &mut TestAppContext) {
9583 init_test(cx, |_| {});
9584
9585 let language = Arc::new(
9586 Language::new(
9587 LanguageConfig {
9588 brackets: BracketPairConfig {
9589 pairs: vec![
9590 BracketPair {
9591 start: "{".to_string(),
9592 end: "}".to_string(),
9593 close: false,
9594 surround: false,
9595 newline: true,
9596 },
9597 BracketPair {
9598 start: "(".to_string(),
9599 end: ")".to_string(),
9600 close: false,
9601 surround: false,
9602 newline: true,
9603 },
9604 ],
9605 ..Default::default()
9606 },
9607 ..Default::default()
9608 },
9609 Some(tree_sitter_rust::LANGUAGE.into()),
9610 )
9611 .with_indents_query(
9612 r#"
9613 (_ "(" ")" @end) @indent
9614 (_ "{" "}" @end) @indent
9615 "#,
9616 )
9617 .unwrap(),
9618 );
9619
9620 let text = "fn a() {}";
9621
9622 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9623 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9625 editor
9626 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9627 .await;
9628
9629 editor.update_in(cx, |editor, window, cx| {
9630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9631 s.select_ranges([5..5, 8..8, 9..9])
9632 });
9633 editor.newline(&Newline, window, cx);
9634 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9635 assert_eq!(
9636 editor.selections.ranges(&editor.display_snapshot(cx)),
9637 &[
9638 Point::new(1, 4)..Point::new(1, 4),
9639 Point::new(3, 4)..Point::new(3, 4),
9640 Point::new(5, 0)..Point::new(5, 0)
9641 ]
9642 );
9643 });
9644}
9645
9646#[gpui::test]
9647async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9648 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9649
9650 let language = Arc::new(
9651 Language::new(
9652 LanguageConfig {
9653 brackets: BracketPairConfig {
9654 pairs: vec![
9655 BracketPair {
9656 start: "{".to_string(),
9657 end: "}".to_string(),
9658 close: false,
9659 surround: false,
9660 newline: true,
9661 },
9662 BracketPair {
9663 start: "(".to_string(),
9664 end: ")".to_string(),
9665 close: false,
9666 surround: false,
9667 newline: true,
9668 },
9669 ],
9670 ..Default::default()
9671 },
9672 ..Default::default()
9673 },
9674 Some(tree_sitter_rust::LANGUAGE.into()),
9675 )
9676 .with_indents_query(
9677 r#"
9678 (_ "(" ")" @end) @indent
9679 (_ "{" "}" @end) @indent
9680 "#,
9681 )
9682 .unwrap(),
9683 );
9684
9685 let text = "fn a() {}";
9686
9687 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9688 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9689 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9690 editor
9691 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9692 .await;
9693
9694 editor.update_in(cx, |editor, window, cx| {
9695 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9696 s.select_ranges([5..5, 8..8, 9..9])
9697 });
9698 editor.newline(&Newline, window, cx);
9699 assert_eq!(
9700 editor.text(cx),
9701 indoc!(
9702 "
9703 fn a(
9704
9705 ) {
9706
9707 }
9708 "
9709 )
9710 );
9711 assert_eq!(
9712 editor.selections.ranges(&editor.display_snapshot(cx)),
9713 &[
9714 Point::new(1, 0)..Point::new(1, 0),
9715 Point::new(3, 0)..Point::new(3, 0),
9716 Point::new(5, 0)..Point::new(5, 0)
9717 ]
9718 );
9719 });
9720}
9721
9722#[gpui::test]
9723async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9724 init_test(cx, |settings| {
9725 settings.defaults.auto_indent = Some(true);
9726 settings.languages.0.insert(
9727 "python".into(),
9728 LanguageSettingsContent {
9729 auto_indent: Some(false),
9730 ..Default::default()
9731 },
9732 );
9733 });
9734
9735 let mut cx = EditorTestContext::new(cx).await;
9736
9737 let injected_language = Arc::new(
9738 Language::new(
9739 LanguageConfig {
9740 brackets: BracketPairConfig {
9741 pairs: vec![
9742 BracketPair {
9743 start: "{".to_string(),
9744 end: "}".to_string(),
9745 close: false,
9746 surround: false,
9747 newline: true,
9748 },
9749 BracketPair {
9750 start: "(".to_string(),
9751 end: ")".to_string(),
9752 close: true,
9753 surround: false,
9754 newline: true,
9755 },
9756 ],
9757 ..Default::default()
9758 },
9759 name: "python".into(),
9760 ..Default::default()
9761 },
9762 Some(tree_sitter_python::LANGUAGE.into()),
9763 )
9764 .with_indents_query(
9765 r#"
9766 (_ "(" ")" @end) @indent
9767 (_ "{" "}" @end) @indent
9768 "#,
9769 )
9770 .unwrap(),
9771 );
9772
9773 let language = Arc::new(
9774 Language::new(
9775 LanguageConfig {
9776 brackets: BracketPairConfig {
9777 pairs: vec![
9778 BracketPair {
9779 start: "{".to_string(),
9780 end: "}".to_string(),
9781 close: false,
9782 surround: false,
9783 newline: true,
9784 },
9785 BracketPair {
9786 start: "(".to_string(),
9787 end: ")".to_string(),
9788 close: true,
9789 surround: false,
9790 newline: true,
9791 },
9792 ],
9793 ..Default::default()
9794 },
9795 name: LanguageName::new("rust"),
9796 ..Default::default()
9797 },
9798 Some(tree_sitter_rust::LANGUAGE.into()),
9799 )
9800 .with_indents_query(
9801 r#"
9802 (_ "(" ")" @end) @indent
9803 (_ "{" "}" @end) @indent
9804 "#,
9805 )
9806 .unwrap()
9807 .with_injection_query(
9808 r#"
9809 (macro_invocation
9810 macro: (identifier) @_macro_name
9811 (token_tree) @injection.content
9812 (#set! injection.language "python"))
9813 "#,
9814 )
9815 .unwrap(),
9816 );
9817
9818 cx.language_registry().add(injected_language);
9819 cx.language_registry().add(language.clone());
9820
9821 cx.update_buffer(|buffer, cx| {
9822 buffer.set_language(Some(language), cx);
9823 });
9824
9825 cx.set_state(r#"struct A {ˇ}"#);
9826
9827 cx.update_editor(|editor, window, cx| {
9828 editor.newline(&Default::default(), window, cx);
9829 });
9830
9831 cx.assert_editor_state(indoc!(
9832 "struct A {
9833 ˇ
9834 }"
9835 ));
9836
9837 cx.set_state(r#"select_biased!(ˇ)"#);
9838
9839 cx.update_editor(|editor, window, cx| {
9840 editor.newline(&Default::default(), window, cx);
9841 editor.handle_input("def ", window, cx);
9842 editor.handle_input("(", window, cx);
9843 editor.newline(&Default::default(), window, cx);
9844 editor.handle_input("a", window, cx);
9845 });
9846
9847 cx.assert_editor_state(indoc!(
9848 "select_biased!(
9849 def (
9850 aˇ
9851 )
9852 )"
9853 ));
9854}
9855
9856#[gpui::test]
9857async fn test_autoindent_selections(cx: &mut TestAppContext) {
9858 init_test(cx, |_| {});
9859
9860 {
9861 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9862 cx.set_state(indoc! {"
9863 impl A {
9864
9865 fn b() {}
9866
9867 «fn c() {
9868
9869 }ˇ»
9870 }
9871 "});
9872
9873 cx.update_editor(|editor, window, cx| {
9874 editor.autoindent(&Default::default(), window, cx);
9875 });
9876
9877 cx.assert_editor_state(indoc! {"
9878 impl A {
9879
9880 fn b() {}
9881
9882 «fn c() {
9883
9884 }ˇ»
9885 }
9886 "});
9887 }
9888
9889 {
9890 let mut cx = EditorTestContext::new_multibuffer(
9891 cx,
9892 [indoc! { "
9893 impl A {
9894 «
9895 // a
9896 fn b(){}
9897 »
9898 «
9899 }
9900 fn c(){}
9901 »
9902 "}],
9903 );
9904
9905 let buffer = cx.update_editor(|editor, _, cx| {
9906 let buffer = editor.buffer().update(cx, |buffer, _| {
9907 buffer.all_buffers().iter().next().unwrap().clone()
9908 });
9909 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9910 buffer
9911 });
9912
9913 cx.run_until_parked();
9914 cx.update_editor(|editor, window, cx| {
9915 editor.select_all(&Default::default(), window, cx);
9916 editor.autoindent(&Default::default(), window, cx)
9917 });
9918 cx.run_until_parked();
9919
9920 cx.update(|_, cx| {
9921 assert_eq!(
9922 buffer.read(cx).text(),
9923 indoc! { "
9924 impl A {
9925
9926 // a
9927 fn b(){}
9928
9929
9930 }
9931 fn c(){}
9932
9933 " }
9934 )
9935 });
9936 }
9937}
9938
9939#[gpui::test]
9940async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9941 init_test(cx, |_| {});
9942
9943 let mut cx = EditorTestContext::new(cx).await;
9944
9945 let language = Arc::new(Language::new(
9946 LanguageConfig {
9947 brackets: BracketPairConfig {
9948 pairs: vec![
9949 BracketPair {
9950 start: "{".to_string(),
9951 end: "}".to_string(),
9952 close: true,
9953 surround: true,
9954 newline: true,
9955 },
9956 BracketPair {
9957 start: "(".to_string(),
9958 end: ")".to_string(),
9959 close: true,
9960 surround: true,
9961 newline: true,
9962 },
9963 BracketPair {
9964 start: "/*".to_string(),
9965 end: " */".to_string(),
9966 close: true,
9967 surround: true,
9968 newline: true,
9969 },
9970 BracketPair {
9971 start: "[".to_string(),
9972 end: "]".to_string(),
9973 close: false,
9974 surround: false,
9975 newline: true,
9976 },
9977 BracketPair {
9978 start: "\"".to_string(),
9979 end: "\"".to_string(),
9980 close: true,
9981 surround: true,
9982 newline: false,
9983 },
9984 BracketPair {
9985 start: "<".to_string(),
9986 end: ">".to_string(),
9987 close: false,
9988 surround: true,
9989 newline: true,
9990 },
9991 ],
9992 ..Default::default()
9993 },
9994 autoclose_before: "})]".to_string(),
9995 ..Default::default()
9996 },
9997 Some(tree_sitter_rust::LANGUAGE.into()),
9998 ));
9999
10000 cx.language_registry().add(language.clone());
10001 cx.update_buffer(|buffer, cx| {
10002 buffer.set_language(Some(language), cx);
10003 });
10004
10005 cx.set_state(
10006 &r#"
10007 🏀ˇ
10008 εˇ
10009 ❤️ˇ
10010 "#
10011 .unindent(),
10012 );
10013
10014 // autoclose multiple nested brackets at multiple cursors
10015 cx.update_editor(|editor, window, cx| {
10016 editor.handle_input("{", window, cx);
10017 editor.handle_input("{", window, cx);
10018 editor.handle_input("{", window, cx);
10019 });
10020 cx.assert_editor_state(
10021 &"
10022 🏀{{{ˇ}}}
10023 ε{{{ˇ}}}
10024 ❤️{{{ˇ}}}
10025 "
10026 .unindent(),
10027 );
10028
10029 // insert a different closing bracket
10030 cx.update_editor(|editor, window, cx| {
10031 editor.handle_input(")", window, cx);
10032 });
10033 cx.assert_editor_state(
10034 &"
10035 🏀{{{)ˇ}}}
10036 ε{{{)ˇ}}}
10037 ❤️{{{)ˇ}}}
10038 "
10039 .unindent(),
10040 );
10041
10042 // skip over the auto-closed brackets when typing a closing bracket
10043 cx.update_editor(|editor, window, cx| {
10044 editor.move_right(&MoveRight, window, cx);
10045 editor.handle_input("}", window, cx);
10046 editor.handle_input("}", window, cx);
10047 editor.handle_input("}", window, cx);
10048 });
10049 cx.assert_editor_state(
10050 &"
10051 🏀{{{)}}}}ˇ
10052 ε{{{)}}}}ˇ
10053 ❤️{{{)}}}}ˇ
10054 "
10055 .unindent(),
10056 );
10057
10058 // autoclose multi-character pairs
10059 cx.set_state(
10060 &"
10061 ˇ
10062 ˇ
10063 "
10064 .unindent(),
10065 );
10066 cx.update_editor(|editor, window, cx| {
10067 editor.handle_input("/", window, cx);
10068 editor.handle_input("*", window, cx);
10069 });
10070 cx.assert_editor_state(
10071 &"
10072 /*ˇ */
10073 /*ˇ */
10074 "
10075 .unindent(),
10076 );
10077
10078 // one cursor autocloses a multi-character pair, one cursor
10079 // does not autoclose.
10080 cx.set_state(
10081 &"
10082 /ˇ
10083 ˇ
10084 "
10085 .unindent(),
10086 );
10087 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10088 cx.assert_editor_state(
10089 &"
10090 /*ˇ */
10091 *ˇ
10092 "
10093 .unindent(),
10094 );
10095
10096 // Don't autoclose if the next character isn't whitespace and isn't
10097 // listed in the language's "autoclose_before" section.
10098 cx.set_state("ˇa b");
10099 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10100 cx.assert_editor_state("{ˇa b");
10101
10102 // Don't autoclose if `close` is false for the bracket pair
10103 cx.set_state("ˇ");
10104 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10105 cx.assert_editor_state("[ˇ");
10106
10107 // Surround with brackets if text is selected
10108 cx.set_state("«aˇ» b");
10109 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10110 cx.assert_editor_state("{«aˇ»} b");
10111
10112 // Autoclose when not immediately after a word character
10113 cx.set_state("a ˇ");
10114 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10115 cx.assert_editor_state("a \"ˇ\"");
10116
10117 // Autoclose pair where the start and end characters are the same
10118 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10119 cx.assert_editor_state("a \"\"ˇ");
10120
10121 // Don't autoclose when immediately after a word character
10122 cx.set_state("aˇ");
10123 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10124 cx.assert_editor_state("a\"ˇ");
10125
10126 // Do autoclose when after a non-word character
10127 cx.set_state("{ˇ");
10128 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10129 cx.assert_editor_state("{\"ˇ\"");
10130
10131 // Non identical pairs autoclose regardless of preceding character
10132 cx.set_state("aˇ");
10133 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10134 cx.assert_editor_state("a{ˇ}");
10135
10136 // Don't autoclose pair if autoclose is disabled
10137 cx.set_state("ˇ");
10138 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10139 cx.assert_editor_state("<ˇ");
10140
10141 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10142 cx.set_state("«aˇ» b");
10143 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10144 cx.assert_editor_state("<«aˇ»> b");
10145}
10146
10147#[gpui::test]
10148async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10149 init_test(cx, |settings| {
10150 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10151 });
10152
10153 let mut cx = EditorTestContext::new(cx).await;
10154
10155 let language = Arc::new(Language::new(
10156 LanguageConfig {
10157 brackets: BracketPairConfig {
10158 pairs: vec![
10159 BracketPair {
10160 start: "{".to_string(),
10161 end: "}".to_string(),
10162 close: true,
10163 surround: true,
10164 newline: true,
10165 },
10166 BracketPair {
10167 start: "(".to_string(),
10168 end: ")".to_string(),
10169 close: true,
10170 surround: true,
10171 newline: true,
10172 },
10173 BracketPair {
10174 start: "[".to_string(),
10175 end: "]".to_string(),
10176 close: false,
10177 surround: false,
10178 newline: true,
10179 },
10180 ],
10181 ..Default::default()
10182 },
10183 autoclose_before: "})]".to_string(),
10184 ..Default::default()
10185 },
10186 Some(tree_sitter_rust::LANGUAGE.into()),
10187 ));
10188
10189 cx.language_registry().add(language.clone());
10190 cx.update_buffer(|buffer, cx| {
10191 buffer.set_language(Some(language), cx);
10192 });
10193
10194 cx.set_state(
10195 &"
10196 ˇ
10197 ˇ
10198 ˇ
10199 "
10200 .unindent(),
10201 );
10202
10203 // ensure only matching closing brackets are skipped over
10204 cx.update_editor(|editor, window, cx| {
10205 editor.handle_input("}", window, cx);
10206 editor.move_left(&MoveLeft, window, cx);
10207 editor.handle_input(")", window, cx);
10208 editor.move_left(&MoveLeft, window, cx);
10209 });
10210 cx.assert_editor_state(
10211 &"
10212 ˇ)}
10213 ˇ)}
10214 ˇ)}
10215 "
10216 .unindent(),
10217 );
10218
10219 // skip-over closing brackets at multiple cursors
10220 cx.update_editor(|editor, window, cx| {
10221 editor.handle_input(")", window, cx);
10222 editor.handle_input("}", window, cx);
10223 });
10224 cx.assert_editor_state(
10225 &"
10226 )}ˇ
10227 )}ˇ
10228 )}ˇ
10229 "
10230 .unindent(),
10231 );
10232
10233 // ignore non-close brackets
10234 cx.update_editor(|editor, window, cx| {
10235 editor.handle_input("]", window, cx);
10236 editor.move_left(&MoveLeft, window, cx);
10237 editor.handle_input("]", window, cx);
10238 });
10239 cx.assert_editor_state(
10240 &"
10241 )}]ˇ]
10242 )}]ˇ]
10243 )}]ˇ]
10244 "
10245 .unindent(),
10246 );
10247}
10248
10249#[gpui::test]
10250async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10251 init_test(cx, |_| {});
10252
10253 let mut cx = EditorTestContext::new(cx).await;
10254
10255 let html_language = Arc::new(
10256 Language::new(
10257 LanguageConfig {
10258 name: "HTML".into(),
10259 brackets: BracketPairConfig {
10260 pairs: vec![
10261 BracketPair {
10262 start: "<".into(),
10263 end: ">".into(),
10264 close: true,
10265 ..Default::default()
10266 },
10267 BracketPair {
10268 start: "{".into(),
10269 end: "}".into(),
10270 close: true,
10271 ..Default::default()
10272 },
10273 BracketPair {
10274 start: "(".into(),
10275 end: ")".into(),
10276 close: true,
10277 ..Default::default()
10278 },
10279 ],
10280 ..Default::default()
10281 },
10282 autoclose_before: "})]>".into(),
10283 ..Default::default()
10284 },
10285 Some(tree_sitter_html::LANGUAGE.into()),
10286 )
10287 .with_injection_query(
10288 r#"
10289 (script_element
10290 (raw_text) @injection.content
10291 (#set! injection.language "javascript"))
10292 "#,
10293 )
10294 .unwrap(),
10295 );
10296
10297 let javascript_language = Arc::new(Language::new(
10298 LanguageConfig {
10299 name: "JavaScript".into(),
10300 brackets: BracketPairConfig {
10301 pairs: vec![
10302 BracketPair {
10303 start: "/*".into(),
10304 end: " */".into(),
10305 close: true,
10306 ..Default::default()
10307 },
10308 BracketPair {
10309 start: "{".into(),
10310 end: "}".into(),
10311 close: true,
10312 ..Default::default()
10313 },
10314 BracketPair {
10315 start: "(".into(),
10316 end: ")".into(),
10317 close: true,
10318 ..Default::default()
10319 },
10320 ],
10321 ..Default::default()
10322 },
10323 autoclose_before: "})]>".into(),
10324 ..Default::default()
10325 },
10326 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10327 ));
10328
10329 cx.language_registry().add(html_language.clone());
10330 cx.language_registry().add(javascript_language);
10331 cx.executor().run_until_parked();
10332
10333 cx.update_buffer(|buffer, cx| {
10334 buffer.set_language(Some(html_language), cx);
10335 });
10336
10337 cx.set_state(
10338 &r#"
10339 <body>ˇ
10340 <script>
10341 var x = 1;ˇ
10342 </script>
10343 </body>ˇ
10344 "#
10345 .unindent(),
10346 );
10347
10348 // Precondition: different languages are active at different locations.
10349 cx.update_editor(|editor, window, cx| {
10350 let snapshot = editor.snapshot(window, cx);
10351 let cursors = editor
10352 .selections
10353 .ranges::<usize>(&editor.display_snapshot(cx));
10354 let languages = cursors
10355 .iter()
10356 .map(|c| snapshot.language_at(c.start).unwrap().name())
10357 .collect::<Vec<_>>();
10358 assert_eq!(
10359 languages,
10360 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10361 );
10362 });
10363
10364 // Angle brackets autoclose in HTML, but not JavaScript.
10365 cx.update_editor(|editor, window, cx| {
10366 editor.handle_input("<", window, cx);
10367 editor.handle_input("a", window, cx);
10368 });
10369 cx.assert_editor_state(
10370 &r#"
10371 <body><aˇ>
10372 <script>
10373 var x = 1;<aˇ
10374 </script>
10375 </body><aˇ>
10376 "#
10377 .unindent(),
10378 );
10379
10380 // Curly braces and parens autoclose in both HTML and JavaScript.
10381 cx.update_editor(|editor, window, cx| {
10382 editor.handle_input(" b=", window, cx);
10383 editor.handle_input("{", window, cx);
10384 editor.handle_input("c", window, cx);
10385 editor.handle_input("(", window, cx);
10386 });
10387 cx.assert_editor_state(
10388 &r#"
10389 <body><a b={c(ˇ)}>
10390 <script>
10391 var x = 1;<a b={c(ˇ)}
10392 </script>
10393 </body><a b={c(ˇ)}>
10394 "#
10395 .unindent(),
10396 );
10397
10398 // Brackets that were already autoclosed are skipped.
10399 cx.update_editor(|editor, window, cx| {
10400 editor.handle_input(")", window, cx);
10401 editor.handle_input("d", window, cx);
10402 editor.handle_input("}", window, cx);
10403 });
10404 cx.assert_editor_state(
10405 &r#"
10406 <body><a b={c()d}ˇ>
10407 <script>
10408 var x = 1;<a b={c()d}ˇ
10409 </script>
10410 </body><a b={c()d}ˇ>
10411 "#
10412 .unindent(),
10413 );
10414 cx.update_editor(|editor, window, cx| {
10415 editor.handle_input(">", window, cx);
10416 });
10417 cx.assert_editor_state(
10418 &r#"
10419 <body><a b={c()d}>ˇ
10420 <script>
10421 var x = 1;<a b={c()d}>ˇ
10422 </script>
10423 </body><a b={c()d}>ˇ
10424 "#
10425 .unindent(),
10426 );
10427
10428 // Reset
10429 cx.set_state(
10430 &r#"
10431 <body>ˇ
10432 <script>
10433 var x = 1;ˇ
10434 </script>
10435 </body>ˇ
10436 "#
10437 .unindent(),
10438 );
10439
10440 cx.update_editor(|editor, window, cx| {
10441 editor.handle_input("<", window, cx);
10442 });
10443 cx.assert_editor_state(
10444 &r#"
10445 <body><ˇ>
10446 <script>
10447 var x = 1;<ˇ
10448 </script>
10449 </body><ˇ>
10450 "#
10451 .unindent(),
10452 );
10453
10454 // When backspacing, the closing angle brackets are removed.
10455 cx.update_editor(|editor, window, cx| {
10456 editor.backspace(&Backspace, window, cx);
10457 });
10458 cx.assert_editor_state(
10459 &r#"
10460 <body>ˇ
10461 <script>
10462 var x = 1;ˇ
10463 </script>
10464 </body>ˇ
10465 "#
10466 .unindent(),
10467 );
10468
10469 // Block comments autoclose in JavaScript, but not HTML.
10470 cx.update_editor(|editor, window, cx| {
10471 editor.handle_input("/", window, cx);
10472 editor.handle_input("*", window, cx);
10473 });
10474 cx.assert_editor_state(
10475 &r#"
10476 <body>/*ˇ
10477 <script>
10478 var x = 1;/*ˇ */
10479 </script>
10480 </body>/*ˇ
10481 "#
10482 .unindent(),
10483 );
10484}
10485
10486#[gpui::test]
10487async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10488 init_test(cx, |_| {});
10489
10490 let mut cx = EditorTestContext::new(cx).await;
10491
10492 let rust_language = Arc::new(
10493 Language::new(
10494 LanguageConfig {
10495 name: "Rust".into(),
10496 brackets: serde_json::from_value(json!([
10497 { "start": "{", "end": "}", "close": true, "newline": true },
10498 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10499 ]))
10500 .unwrap(),
10501 autoclose_before: "})]>".into(),
10502 ..Default::default()
10503 },
10504 Some(tree_sitter_rust::LANGUAGE.into()),
10505 )
10506 .with_override_query("(string_literal) @string")
10507 .unwrap(),
10508 );
10509
10510 cx.language_registry().add(rust_language.clone());
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(rust_language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 let x = ˇ
10518 "#
10519 .unindent(),
10520 );
10521
10522 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10523 cx.update_editor(|editor, window, cx| {
10524 editor.handle_input("\"", window, cx);
10525 });
10526 cx.assert_editor_state(
10527 &r#"
10528 let x = "ˇ"
10529 "#
10530 .unindent(),
10531 );
10532
10533 // Inserting another quotation mark. The cursor moves across the existing
10534 // automatically-inserted quotation mark.
10535 cx.update_editor(|editor, window, cx| {
10536 editor.handle_input("\"", window, cx);
10537 });
10538 cx.assert_editor_state(
10539 &r#"
10540 let x = ""ˇ
10541 "#
10542 .unindent(),
10543 );
10544
10545 // Reset
10546 cx.set_state(
10547 &r#"
10548 let x = ˇ
10549 "#
10550 .unindent(),
10551 );
10552
10553 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10554 cx.update_editor(|editor, window, cx| {
10555 editor.handle_input("\"", window, cx);
10556 editor.handle_input(" ", window, cx);
10557 editor.move_left(&Default::default(), window, cx);
10558 editor.handle_input("\\", window, cx);
10559 editor.handle_input("\"", window, cx);
10560 });
10561 cx.assert_editor_state(
10562 &r#"
10563 let x = "\"ˇ "
10564 "#
10565 .unindent(),
10566 );
10567
10568 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10569 // mark. Nothing is inserted.
10570 cx.update_editor(|editor, window, cx| {
10571 editor.move_right(&Default::default(), window, cx);
10572 editor.handle_input("\"", window, cx);
10573 });
10574 cx.assert_editor_state(
10575 &r#"
10576 let x = "\" "ˇ
10577 "#
10578 .unindent(),
10579 );
10580}
10581
10582#[gpui::test]
10583async fn test_surround_with_pair(cx: &mut TestAppContext) {
10584 init_test(cx, |_| {});
10585
10586 let language = Arc::new(Language::new(
10587 LanguageConfig {
10588 brackets: BracketPairConfig {
10589 pairs: vec![
10590 BracketPair {
10591 start: "{".to_string(),
10592 end: "}".to_string(),
10593 close: true,
10594 surround: true,
10595 newline: true,
10596 },
10597 BracketPair {
10598 start: "/* ".to_string(),
10599 end: "*/".to_string(),
10600 close: true,
10601 surround: true,
10602 ..Default::default()
10603 },
10604 ],
10605 ..Default::default()
10606 },
10607 ..Default::default()
10608 },
10609 Some(tree_sitter_rust::LANGUAGE.into()),
10610 ));
10611
10612 let text = r#"
10613 a
10614 b
10615 c
10616 "#
10617 .unindent();
10618
10619 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10620 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10621 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10622 editor
10623 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10624 .await;
10625
10626 editor.update_in(cx, |editor, window, cx| {
10627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10628 s.select_display_ranges([
10629 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10631 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10632 ])
10633 });
10634
10635 editor.handle_input("{", window, cx);
10636 editor.handle_input("{", window, cx);
10637 editor.handle_input("{", window, cx);
10638 assert_eq!(
10639 editor.text(cx),
10640 "
10641 {{{a}}}
10642 {{{b}}}
10643 {{{c}}}
10644 "
10645 .unindent()
10646 );
10647 assert_eq!(
10648 editor.selections.display_ranges(cx),
10649 [
10650 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10651 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10652 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10653 ]
10654 );
10655
10656 editor.undo(&Undo, window, cx);
10657 editor.undo(&Undo, window, cx);
10658 editor.undo(&Undo, window, cx);
10659 assert_eq!(
10660 editor.text(cx),
10661 "
10662 a
10663 b
10664 c
10665 "
10666 .unindent()
10667 );
10668 assert_eq!(
10669 editor.selections.display_ranges(cx),
10670 [
10671 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10672 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10673 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10674 ]
10675 );
10676
10677 // Ensure inserting the first character of a multi-byte bracket pair
10678 // doesn't surround the selections with the bracket.
10679 editor.handle_input("/", window, cx);
10680 assert_eq!(
10681 editor.text(cx),
10682 "
10683 /
10684 /
10685 /
10686 "
10687 .unindent()
10688 );
10689 assert_eq!(
10690 editor.selections.display_ranges(cx),
10691 [
10692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10693 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10694 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10695 ]
10696 );
10697
10698 editor.undo(&Undo, window, cx);
10699 assert_eq!(
10700 editor.text(cx),
10701 "
10702 a
10703 b
10704 c
10705 "
10706 .unindent()
10707 );
10708 assert_eq!(
10709 editor.selections.display_ranges(cx),
10710 [
10711 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10713 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10714 ]
10715 );
10716
10717 // Ensure inserting the last character of a multi-byte bracket pair
10718 // doesn't surround the selections with the bracket.
10719 editor.handle_input("*", window, cx);
10720 assert_eq!(
10721 editor.text(cx),
10722 "
10723 *
10724 *
10725 *
10726 "
10727 .unindent()
10728 );
10729 assert_eq!(
10730 editor.selections.display_ranges(cx),
10731 [
10732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10733 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10734 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10735 ]
10736 );
10737 });
10738}
10739
10740#[gpui::test]
10741async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10742 init_test(cx, |_| {});
10743
10744 let language = Arc::new(Language::new(
10745 LanguageConfig {
10746 brackets: BracketPairConfig {
10747 pairs: vec![BracketPair {
10748 start: "{".to_string(),
10749 end: "}".to_string(),
10750 close: true,
10751 surround: true,
10752 newline: true,
10753 }],
10754 ..Default::default()
10755 },
10756 autoclose_before: "}".to_string(),
10757 ..Default::default()
10758 },
10759 Some(tree_sitter_rust::LANGUAGE.into()),
10760 ));
10761
10762 let text = r#"
10763 a
10764 b
10765 c
10766 "#
10767 .unindent();
10768
10769 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10770 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10771 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10772 editor
10773 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10774 .await;
10775
10776 editor.update_in(cx, |editor, window, cx| {
10777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10778 s.select_ranges([
10779 Point::new(0, 1)..Point::new(0, 1),
10780 Point::new(1, 1)..Point::new(1, 1),
10781 Point::new(2, 1)..Point::new(2, 1),
10782 ])
10783 });
10784
10785 editor.handle_input("{", window, cx);
10786 editor.handle_input("{", window, cx);
10787 editor.handle_input("_", window, cx);
10788 assert_eq!(
10789 editor.text(cx),
10790 "
10791 a{{_}}
10792 b{{_}}
10793 c{{_}}
10794 "
10795 .unindent()
10796 );
10797 assert_eq!(
10798 editor
10799 .selections
10800 .ranges::<Point>(&editor.display_snapshot(cx)),
10801 [
10802 Point::new(0, 4)..Point::new(0, 4),
10803 Point::new(1, 4)..Point::new(1, 4),
10804 Point::new(2, 4)..Point::new(2, 4)
10805 ]
10806 );
10807
10808 editor.backspace(&Default::default(), window, cx);
10809 editor.backspace(&Default::default(), window, cx);
10810 assert_eq!(
10811 editor.text(cx),
10812 "
10813 a{}
10814 b{}
10815 c{}
10816 "
10817 .unindent()
10818 );
10819 assert_eq!(
10820 editor
10821 .selections
10822 .ranges::<Point>(&editor.display_snapshot(cx)),
10823 [
10824 Point::new(0, 2)..Point::new(0, 2),
10825 Point::new(1, 2)..Point::new(1, 2),
10826 Point::new(2, 2)..Point::new(2, 2)
10827 ]
10828 );
10829
10830 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10831 assert_eq!(
10832 editor.text(cx),
10833 "
10834 a
10835 b
10836 c
10837 "
10838 .unindent()
10839 );
10840 assert_eq!(
10841 editor
10842 .selections
10843 .ranges::<Point>(&editor.display_snapshot(cx)),
10844 [
10845 Point::new(0, 1)..Point::new(0, 1),
10846 Point::new(1, 1)..Point::new(1, 1),
10847 Point::new(2, 1)..Point::new(2, 1)
10848 ]
10849 );
10850 });
10851}
10852
10853#[gpui::test]
10854async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10855 init_test(cx, |settings| {
10856 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10857 });
10858
10859 let mut cx = EditorTestContext::new(cx).await;
10860
10861 let language = Arc::new(Language::new(
10862 LanguageConfig {
10863 brackets: BracketPairConfig {
10864 pairs: vec![
10865 BracketPair {
10866 start: "{".to_string(),
10867 end: "}".to_string(),
10868 close: true,
10869 surround: true,
10870 newline: true,
10871 },
10872 BracketPair {
10873 start: "(".to_string(),
10874 end: ")".to_string(),
10875 close: true,
10876 surround: true,
10877 newline: true,
10878 },
10879 BracketPair {
10880 start: "[".to_string(),
10881 end: "]".to_string(),
10882 close: false,
10883 surround: true,
10884 newline: true,
10885 },
10886 ],
10887 ..Default::default()
10888 },
10889 autoclose_before: "})]".to_string(),
10890 ..Default::default()
10891 },
10892 Some(tree_sitter_rust::LANGUAGE.into()),
10893 ));
10894
10895 cx.language_registry().add(language.clone());
10896 cx.update_buffer(|buffer, cx| {
10897 buffer.set_language(Some(language), cx);
10898 });
10899
10900 cx.set_state(
10901 &"
10902 {(ˇ)}
10903 [[ˇ]]
10904 {(ˇ)}
10905 "
10906 .unindent(),
10907 );
10908
10909 cx.update_editor(|editor, window, cx| {
10910 editor.backspace(&Default::default(), window, cx);
10911 editor.backspace(&Default::default(), window, cx);
10912 });
10913
10914 cx.assert_editor_state(
10915 &"
10916 ˇ
10917 ˇ]]
10918 ˇ
10919 "
10920 .unindent(),
10921 );
10922
10923 cx.update_editor(|editor, window, cx| {
10924 editor.handle_input("{", window, cx);
10925 editor.handle_input("{", window, cx);
10926 editor.move_right(&MoveRight, window, cx);
10927 editor.move_right(&MoveRight, window, cx);
10928 editor.move_left(&MoveLeft, window, cx);
10929 editor.move_left(&MoveLeft, window, cx);
10930 editor.backspace(&Default::default(), window, cx);
10931 });
10932
10933 cx.assert_editor_state(
10934 &"
10935 {ˇ}
10936 {ˇ}]]
10937 {ˇ}
10938 "
10939 .unindent(),
10940 );
10941
10942 cx.update_editor(|editor, window, cx| {
10943 editor.backspace(&Default::default(), window, cx);
10944 });
10945
10946 cx.assert_editor_state(
10947 &"
10948 ˇ
10949 ˇ]]
10950 ˇ
10951 "
10952 .unindent(),
10953 );
10954}
10955
10956#[gpui::test]
10957async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10958 init_test(cx, |_| {});
10959
10960 let language = Arc::new(Language::new(
10961 LanguageConfig::default(),
10962 Some(tree_sitter_rust::LANGUAGE.into()),
10963 ));
10964
10965 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10968 editor
10969 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10970 .await;
10971
10972 editor.update_in(cx, |editor, window, cx| {
10973 editor.set_auto_replace_emoji_shortcode(true);
10974
10975 editor.handle_input("Hello ", window, cx);
10976 editor.handle_input(":wave", window, cx);
10977 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10978
10979 editor.handle_input(":", window, cx);
10980 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10981
10982 editor.handle_input(" :smile", window, cx);
10983 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10984
10985 editor.handle_input(":", window, cx);
10986 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10987
10988 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10989 editor.handle_input(":wave", window, cx);
10990 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10991
10992 editor.handle_input(":", window, cx);
10993 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10994
10995 editor.handle_input(":1", window, cx);
10996 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10997
10998 editor.handle_input(":", window, cx);
10999 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11000
11001 // Ensure shortcode does not get replaced when it is part of a word
11002 editor.handle_input(" Test:wave", window, cx);
11003 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11004
11005 editor.handle_input(":", window, cx);
11006 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11007
11008 editor.set_auto_replace_emoji_shortcode(false);
11009
11010 // Ensure shortcode does not get replaced when auto replace is off
11011 editor.handle_input(" :wave", window, cx);
11012 assert_eq!(
11013 editor.text(cx),
11014 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11015 );
11016
11017 editor.handle_input(":", window, cx);
11018 assert_eq!(
11019 editor.text(cx),
11020 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11021 );
11022 });
11023}
11024
11025#[gpui::test]
11026async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11027 init_test(cx, |_| {});
11028
11029 let (text, insertion_ranges) = marked_text_ranges(
11030 indoc! {"
11031 ˇ
11032 "},
11033 false,
11034 );
11035
11036 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11037 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11038
11039 _ = editor.update_in(cx, |editor, window, cx| {
11040 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11041
11042 editor
11043 .insert_snippet(&insertion_ranges, snippet, window, cx)
11044 .unwrap();
11045
11046 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11047 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11048 assert_eq!(editor.text(cx), expected_text);
11049 assert_eq!(
11050 editor
11051 .selections
11052 .ranges::<usize>(&editor.display_snapshot(cx)),
11053 selection_ranges
11054 );
11055 }
11056
11057 assert(
11058 editor,
11059 cx,
11060 indoc! {"
11061 type «» =•
11062 "},
11063 );
11064
11065 assert!(editor.context_menu_visible(), "There should be a matches");
11066 });
11067}
11068
11069#[gpui::test]
11070async fn test_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_document_format_during_save(cx: &mut TestAppContext) {
11194 init_test(cx, |_| {});
11195
11196 let fs = FakeFs::new(cx.executor());
11197 fs.insert_file(path!("/file.rs"), Default::default()).await;
11198
11199 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11200
11201 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11202 language_registry.add(rust_lang());
11203 let mut fake_servers = language_registry.register_fake_lsp(
11204 "Rust",
11205 FakeLspAdapter {
11206 capabilities: lsp::ServerCapabilities {
11207 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11208 ..Default::default()
11209 },
11210 ..Default::default()
11211 },
11212 );
11213
11214 let buffer = project
11215 .update(cx, |project, cx| {
11216 project.open_local_buffer(path!("/file.rs"), cx)
11217 })
11218 .await
11219 .unwrap();
11220
11221 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11222 let (editor, cx) = cx.add_window_view(|window, cx| {
11223 build_editor_with_project(project.clone(), buffer, window, cx)
11224 });
11225 editor.update_in(cx, |editor, window, cx| {
11226 editor.set_text("one\ntwo\nthree\n", window, cx)
11227 });
11228 assert!(cx.read(|cx| editor.is_dirty(cx)));
11229
11230 cx.executor().start_waiting();
11231 let fake_server = fake_servers.next().await.unwrap();
11232
11233 {
11234 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11235 move |params, _| async move {
11236 assert_eq!(
11237 params.text_document.uri,
11238 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11239 );
11240 assert_eq!(params.options.tab_size, 4);
11241 Ok(Some(vec![lsp::TextEdit::new(
11242 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11243 ", ".to_string(),
11244 )]))
11245 },
11246 );
11247 let save = editor
11248 .update_in(cx, |editor, window, cx| {
11249 editor.save(
11250 SaveOptions {
11251 format: true,
11252 autosave: false,
11253 },
11254 project.clone(),
11255 window,
11256 cx,
11257 )
11258 })
11259 .unwrap();
11260 cx.executor().start_waiting();
11261 save.await;
11262
11263 assert_eq!(
11264 editor.update(cx, |editor, cx| editor.text(cx)),
11265 "one, two\nthree\n"
11266 );
11267 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11268 }
11269
11270 {
11271 editor.update_in(cx, |editor, window, cx| {
11272 editor.set_text("one\ntwo\nthree\n", window, cx)
11273 });
11274 assert!(cx.read(|cx| editor.is_dirty(cx)));
11275
11276 // Ensure we can still save even if formatting hangs.
11277 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11278 move |params, _| async move {
11279 assert_eq!(
11280 params.text_document.uri,
11281 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11282 );
11283 futures::future::pending::<()>().await;
11284 unreachable!()
11285 },
11286 );
11287 let save = editor
11288 .update_in(cx, |editor, window, cx| {
11289 editor.save(
11290 SaveOptions {
11291 format: true,
11292 autosave: false,
11293 },
11294 project.clone(),
11295 window,
11296 cx,
11297 )
11298 })
11299 .unwrap();
11300 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11301 cx.executor().start_waiting();
11302 save.await;
11303 assert_eq!(
11304 editor.update(cx, |editor, cx| editor.text(cx)),
11305 "one\ntwo\nthree\n"
11306 );
11307 }
11308
11309 // Set rust language override and assert overridden tabsize is sent to language server
11310 update_test_language_settings(cx, |settings| {
11311 settings.languages.0.insert(
11312 "Rust".into(),
11313 LanguageSettingsContent {
11314 tab_size: NonZeroU32::new(8),
11315 ..Default::default()
11316 },
11317 );
11318 });
11319
11320 {
11321 editor.update_in(cx, |editor, window, cx| {
11322 editor.set_text("somehting_new\n", window, cx)
11323 });
11324 assert!(cx.read(|cx| editor.is_dirty(cx)));
11325 let _formatting_request_signal = fake_server
11326 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11327 assert_eq!(
11328 params.text_document.uri,
11329 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11330 );
11331 assert_eq!(params.options.tab_size, 8);
11332 Ok(Some(vec![]))
11333 });
11334 let save = editor
11335 .update_in(cx, |editor, window, cx| {
11336 editor.save(
11337 SaveOptions {
11338 format: true,
11339 autosave: false,
11340 },
11341 project.clone(),
11342 window,
11343 cx,
11344 )
11345 })
11346 .unwrap();
11347 cx.executor().start_waiting();
11348 save.await;
11349 }
11350}
11351
11352#[gpui::test]
11353async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11354 init_test(cx, |settings| {
11355 settings.defaults.ensure_final_newline_on_save = Some(false);
11356 });
11357
11358 let fs = FakeFs::new(cx.executor());
11359 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11360
11361 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11362
11363 let buffer = project
11364 .update(cx, |project, cx| {
11365 project.open_local_buffer(path!("/file.txt"), cx)
11366 })
11367 .await
11368 .unwrap();
11369
11370 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11371 let (editor, cx) = cx.add_window_view(|window, cx| {
11372 build_editor_with_project(project.clone(), buffer, window, cx)
11373 });
11374 editor.update_in(cx, |editor, window, cx| {
11375 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11376 s.select_ranges([0..0])
11377 });
11378 });
11379 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11380
11381 editor.update_in(cx, |editor, window, cx| {
11382 editor.handle_input("\n", window, cx)
11383 });
11384 cx.run_until_parked();
11385 save(&editor, &project, cx).await;
11386 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11387
11388 editor.update_in(cx, |editor, window, cx| {
11389 editor.undo(&Default::default(), window, cx);
11390 });
11391 save(&editor, &project, cx).await;
11392 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11393
11394 editor.update_in(cx, |editor, window, cx| {
11395 editor.redo(&Default::default(), window, cx);
11396 });
11397 cx.run_until_parked();
11398 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11399
11400 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11401 let save = editor
11402 .update_in(cx, |editor, window, cx| {
11403 editor.save(
11404 SaveOptions {
11405 format: true,
11406 autosave: false,
11407 },
11408 project.clone(),
11409 window,
11410 cx,
11411 )
11412 })
11413 .unwrap();
11414 cx.executor().start_waiting();
11415 save.await;
11416 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11417 }
11418}
11419
11420#[gpui::test]
11421async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11422 init_test(cx, |_| {});
11423
11424 let cols = 4;
11425 let rows = 10;
11426 let sample_text_1 = sample_text(rows, cols, 'a');
11427 assert_eq!(
11428 sample_text_1,
11429 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11430 );
11431 let sample_text_2 = sample_text(rows, cols, 'l');
11432 assert_eq!(
11433 sample_text_2,
11434 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11435 );
11436 let sample_text_3 = sample_text(rows, cols, 'v');
11437 assert_eq!(
11438 sample_text_3,
11439 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11440 );
11441
11442 let fs = FakeFs::new(cx.executor());
11443 fs.insert_tree(
11444 path!("/a"),
11445 json!({
11446 "main.rs": sample_text_1,
11447 "other.rs": sample_text_2,
11448 "lib.rs": sample_text_3,
11449 }),
11450 )
11451 .await;
11452
11453 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11454 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11455 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11456
11457 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11458 language_registry.add(rust_lang());
11459 let mut fake_servers = language_registry.register_fake_lsp(
11460 "Rust",
11461 FakeLspAdapter {
11462 capabilities: lsp::ServerCapabilities {
11463 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11464 ..Default::default()
11465 },
11466 ..Default::default()
11467 },
11468 );
11469
11470 let worktree = project.update(cx, |project, cx| {
11471 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11472 assert_eq!(worktrees.len(), 1);
11473 worktrees.pop().unwrap()
11474 });
11475 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11476
11477 let buffer_1 = project
11478 .update(cx, |project, cx| {
11479 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11480 })
11481 .await
11482 .unwrap();
11483 let buffer_2 = project
11484 .update(cx, |project, cx| {
11485 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11486 })
11487 .await
11488 .unwrap();
11489 let buffer_3 = project
11490 .update(cx, |project, cx| {
11491 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11492 })
11493 .await
11494 .unwrap();
11495
11496 let multi_buffer = cx.new(|cx| {
11497 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11498 multi_buffer.push_excerpts(
11499 buffer_1.clone(),
11500 [
11501 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11502 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11503 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11504 ],
11505 cx,
11506 );
11507 multi_buffer.push_excerpts(
11508 buffer_2.clone(),
11509 [
11510 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11511 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11512 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11513 ],
11514 cx,
11515 );
11516 multi_buffer.push_excerpts(
11517 buffer_3.clone(),
11518 [
11519 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11520 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11521 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11522 ],
11523 cx,
11524 );
11525 multi_buffer
11526 });
11527 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11528 Editor::new(
11529 EditorMode::full(),
11530 multi_buffer,
11531 Some(project.clone()),
11532 window,
11533 cx,
11534 )
11535 });
11536
11537 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11538 editor.change_selections(
11539 SelectionEffects::scroll(Autoscroll::Next),
11540 window,
11541 cx,
11542 |s| s.select_ranges(Some(1..2)),
11543 );
11544 editor.insert("|one|two|three|", window, cx);
11545 });
11546 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11547 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11548 editor.change_selections(
11549 SelectionEffects::scroll(Autoscroll::Next),
11550 window,
11551 cx,
11552 |s| s.select_ranges(Some(60..70)),
11553 );
11554 editor.insert("|four|five|six|", window, cx);
11555 });
11556 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11557
11558 // First two buffers should be edited, but not the third one.
11559 assert_eq!(
11560 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11561 "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}",
11562 );
11563 buffer_1.update(cx, |buffer, _| {
11564 assert!(buffer.is_dirty());
11565 assert_eq!(
11566 buffer.text(),
11567 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11568 )
11569 });
11570 buffer_2.update(cx, |buffer, _| {
11571 assert!(buffer.is_dirty());
11572 assert_eq!(
11573 buffer.text(),
11574 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11575 )
11576 });
11577 buffer_3.update(cx, |buffer, _| {
11578 assert!(!buffer.is_dirty());
11579 assert_eq!(buffer.text(), sample_text_3,)
11580 });
11581 cx.executor().run_until_parked();
11582
11583 cx.executor().start_waiting();
11584 let save = multi_buffer_editor
11585 .update_in(cx, |editor, window, cx| {
11586 editor.save(
11587 SaveOptions {
11588 format: true,
11589 autosave: false,
11590 },
11591 project.clone(),
11592 window,
11593 cx,
11594 )
11595 })
11596 .unwrap();
11597
11598 let fake_server = fake_servers.next().await.unwrap();
11599 fake_server
11600 .server
11601 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11602 Ok(Some(vec![lsp::TextEdit::new(
11603 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11604 format!("[{} formatted]", params.text_document.uri),
11605 )]))
11606 })
11607 .detach();
11608 save.await;
11609
11610 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11611 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11612 assert_eq!(
11613 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11614 uri!(
11615 "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}"
11616 ),
11617 );
11618 buffer_1.update(cx, |buffer, _| {
11619 assert!(!buffer.is_dirty());
11620 assert_eq!(
11621 buffer.text(),
11622 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11623 )
11624 });
11625 buffer_2.update(cx, |buffer, _| {
11626 assert!(!buffer.is_dirty());
11627 assert_eq!(
11628 buffer.text(),
11629 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11630 )
11631 });
11632 buffer_3.update(cx, |buffer, _| {
11633 assert!(!buffer.is_dirty());
11634 assert_eq!(buffer.text(), sample_text_3,)
11635 });
11636}
11637
11638#[gpui::test]
11639async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11640 init_test(cx, |_| {});
11641
11642 let fs = FakeFs::new(cx.executor());
11643 fs.insert_tree(
11644 path!("/dir"),
11645 json!({
11646 "file1.rs": "fn main() { println!(\"hello\"); }",
11647 "file2.rs": "fn test() { println!(\"test\"); }",
11648 "file3.rs": "fn other() { println!(\"other\"); }\n",
11649 }),
11650 )
11651 .await;
11652
11653 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11654 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11655 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11656
11657 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11658 language_registry.add(rust_lang());
11659
11660 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11661 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11662
11663 // Open three buffers
11664 let buffer_1 = project
11665 .update(cx, |project, cx| {
11666 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11667 })
11668 .await
11669 .unwrap();
11670 let buffer_2 = project
11671 .update(cx, |project, cx| {
11672 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11673 })
11674 .await
11675 .unwrap();
11676 let buffer_3 = project
11677 .update(cx, |project, cx| {
11678 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11679 })
11680 .await
11681 .unwrap();
11682
11683 // Create a multi-buffer with all three buffers
11684 let multi_buffer = cx.new(|cx| {
11685 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11686 multi_buffer.push_excerpts(
11687 buffer_1.clone(),
11688 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11689 cx,
11690 );
11691 multi_buffer.push_excerpts(
11692 buffer_2.clone(),
11693 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11694 cx,
11695 );
11696 multi_buffer.push_excerpts(
11697 buffer_3.clone(),
11698 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11699 cx,
11700 );
11701 multi_buffer
11702 });
11703
11704 let editor = cx.new_window_entity(|window, cx| {
11705 Editor::new(
11706 EditorMode::full(),
11707 multi_buffer,
11708 Some(project.clone()),
11709 window,
11710 cx,
11711 )
11712 });
11713
11714 // Edit only the first buffer
11715 editor.update_in(cx, |editor, window, cx| {
11716 editor.change_selections(
11717 SelectionEffects::scroll(Autoscroll::Next),
11718 window,
11719 cx,
11720 |s| s.select_ranges(Some(10..10)),
11721 );
11722 editor.insert("// edited", window, cx);
11723 });
11724
11725 // Verify that only buffer 1 is dirty
11726 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11727 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11728 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11729
11730 // Get write counts after file creation (files were created with initial content)
11731 // We expect each file to have been written once during creation
11732 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11733 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11734 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11735
11736 // Perform autosave
11737 let save_task = editor.update_in(cx, |editor, window, cx| {
11738 editor.save(
11739 SaveOptions {
11740 format: true,
11741 autosave: true,
11742 },
11743 project.clone(),
11744 window,
11745 cx,
11746 )
11747 });
11748 save_task.await.unwrap();
11749
11750 // Only the dirty buffer should have been saved
11751 assert_eq!(
11752 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11753 1,
11754 "Buffer 1 was dirty, so it should have been written once during autosave"
11755 );
11756 assert_eq!(
11757 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11758 0,
11759 "Buffer 2 was clean, so it should not have been written during autosave"
11760 );
11761 assert_eq!(
11762 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11763 0,
11764 "Buffer 3 was clean, so it should not have been written during autosave"
11765 );
11766
11767 // Verify buffer states after autosave
11768 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11769 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11770 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11771
11772 // Now perform a manual save (format = true)
11773 let save_task = editor.update_in(cx, |editor, window, cx| {
11774 editor.save(
11775 SaveOptions {
11776 format: true,
11777 autosave: false,
11778 },
11779 project.clone(),
11780 window,
11781 cx,
11782 )
11783 });
11784 save_task.await.unwrap();
11785
11786 // During manual save, clean buffers don't get written to disk
11787 // They just get did_save called for language server notifications
11788 assert_eq!(
11789 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11790 1,
11791 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11792 );
11793 assert_eq!(
11794 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11795 0,
11796 "Buffer 2 should not have been written at all"
11797 );
11798 assert_eq!(
11799 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11800 0,
11801 "Buffer 3 should not have been written at all"
11802 );
11803}
11804
11805async fn setup_range_format_test(
11806 cx: &mut TestAppContext,
11807) -> (
11808 Entity<Project>,
11809 Entity<Editor>,
11810 &mut gpui::VisualTestContext,
11811 lsp::FakeLanguageServer,
11812) {
11813 init_test(cx, |_| {});
11814
11815 let fs = FakeFs::new(cx.executor());
11816 fs.insert_file(path!("/file.rs"), Default::default()).await;
11817
11818 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11819
11820 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11821 language_registry.add(rust_lang());
11822 let mut fake_servers = language_registry.register_fake_lsp(
11823 "Rust",
11824 FakeLspAdapter {
11825 capabilities: lsp::ServerCapabilities {
11826 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11827 ..lsp::ServerCapabilities::default()
11828 },
11829 ..FakeLspAdapter::default()
11830 },
11831 );
11832
11833 let buffer = project
11834 .update(cx, |project, cx| {
11835 project.open_local_buffer(path!("/file.rs"), cx)
11836 })
11837 .await
11838 .unwrap();
11839
11840 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11841 let (editor, cx) = cx.add_window_view(|window, cx| {
11842 build_editor_with_project(project.clone(), buffer, window, cx)
11843 });
11844
11845 cx.executor().start_waiting();
11846 let fake_server = fake_servers.next().await.unwrap();
11847
11848 (project, editor, cx, fake_server)
11849}
11850
11851#[gpui::test]
11852async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11853 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11854
11855 editor.update_in(cx, |editor, window, cx| {
11856 editor.set_text("one\ntwo\nthree\n", window, cx)
11857 });
11858 assert!(cx.read(|cx| editor.is_dirty(cx)));
11859
11860 let save = editor
11861 .update_in(cx, |editor, window, cx| {
11862 editor.save(
11863 SaveOptions {
11864 format: true,
11865 autosave: false,
11866 },
11867 project.clone(),
11868 window,
11869 cx,
11870 )
11871 })
11872 .unwrap();
11873 fake_server
11874 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11875 assert_eq!(
11876 params.text_document.uri,
11877 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11878 );
11879 assert_eq!(params.options.tab_size, 4);
11880 Ok(Some(vec![lsp::TextEdit::new(
11881 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11882 ", ".to_string(),
11883 )]))
11884 })
11885 .next()
11886 .await;
11887 cx.executor().start_waiting();
11888 save.await;
11889 assert_eq!(
11890 editor.update(cx, |editor, cx| editor.text(cx)),
11891 "one, two\nthree\n"
11892 );
11893 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11894}
11895
11896#[gpui::test]
11897async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11898 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11899
11900 editor.update_in(cx, |editor, window, cx| {
11901 editor.set_text("one\ntwo\nthree\n", window, cx)
11902 });
11903 assert!(cx.read(|cx| editor.is_dirty(cx)));
11904
11905 // Test that save still works when formatting hangs
11906 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11907 move |params, _| async move {
11908 assert_eq!(
11909 params.text_document.uri,
11910 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11911 );
11912 futures::future::pending::<()>().await;
11913 unreachable!()
11914 },
11915 );
11916 let save = editor
11917 .update_in(cx, |editor, window, cx| {
11918 editor.save(
11919 SaveOptions {
11920 format: true,
11921 autosave: false,
11922 },
11923 project.clone(),
11924 window,
11925 cx,
11926 )
11927 })
11928 .unwrap();
11929 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11930 cx.executor().start_waiting();
11931 save.await;
11932 assert_eq!(
11933 editor.update(cx, |editor, cx| editor.text(cx)),
11934 "one\ntwo\nthree\n"
11935 );
11936 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11937}
11938
11939#[gpui::test]
11940async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11941 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11942
11943 // Buffer starts clean, no formatting should be requested
11944 let save = editor
11945 .update_in(cx, |editor, window, cx| {
11946 editor.save(
11947 SaveOptions {
11948 format: false,
11949 autosave: false,
11950 },
11951 project.clone(),
11952 window,
11953 cx,
11954 )
11955 })
11956 .unwrap();
11957 let _pending_format_request = fake_server
11958 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11959 panic!("Should not be invoked");
11960 })
11961 .next();
11962 cx.executor().start_waiting();
11963 save.await;
11964 cx.run_until_parked();
11965}
11966
11967#[gpui::test]
11968async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11969 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11970
11971 // Set Rust language override and assert overridden tabsize is sent to language server
11972 update_test_language_settings(cx, |settings| {
11973 settings.languages.0.insert(
11974 "Rust".into(),
11975 LanguageSettingsContent {
11976 tab_size: NonZeroU32::new(8),
11977 ..Default::default()
11978 },
11979 );
11980 });
11981
11982 editor.update_in(cx, |editor, window, cx| {
11983 editor.set_text("something_new\n", window, cx)
11984 });
11985 assert!(cx.read(|cx| editor.is_dirty(cx)));
11986 let save = editor
11987 .update_in(cx, |editor, window, cx| {
11988 editor.save(
11989 SaveOptions {
11990 format: true,
11991 autosave: false,
11992 },
11993 project.clone(),
11994 window,
11995 cx,
11996 )
11997 })
11998 .unwrap();
11999 fake_server
12000 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12001 assert_eq!(
12002 params.text_document.uri,
12003 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12004 );
12005 assert_eq!(params.options.tab_size, 8);
12006 Ok(Some(Vec::new()))
12007 })
12008 .next()
12009 .await;
12010 save.await;
12011}
12012
12013#[gpui::test]
12014async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12015 init_test(cx, |settings| {
12016 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12017 settings::LanguageServerFormatterSpecifier::Current,
12018 )))
12019 });
12020
12021 let fs = FakeFs::new(cx.executor());
12022 fs.insert_file(path!("/file.rs"), Default::default()).await;
12023
12024 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12025
12026 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12027 language_registry.add(Arc::new(Language::new(
12028 LanguageConfig {
12029 name: "Rust".into(),
12030 matcher: LanguageMatcher {
12031 path_suffixes: vec!["rs".to_string()],
12032 ..Default::default()
12033 },
12034 ..LanguageConfig::default()
12035 },
12036 Some(tree_sitter_rust::LANGUAGE.into()),
12037 )));
12038 update_test_language_settings(cx, |settings| {
12039 // Enable Prettier formatting for the same buffer, and ensure
12040 // LSP is called instead of Prettier.
12041 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12042 });
12043 let mut fake_servers = language_registry.register_fake_lsp(
12044 "Rust",
12045 FakeLspAdapter {
12046 capabilities: lsp::ServerCapabilities {
12047 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12048 ..Default::default()
12049 },
12050 ..Default::default()
12051 },
12052 );
12053
12054 let buffer = project
12055 .update(cx, |project, cx| {
12056 project.open_local_buffer(path!("/file.rs"), cx)
12057 })
12058 .await
12059 .unwrap();
12060
12061 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12062 let (editor, cx) = cx.add_window_view(|window, cx| {
12063 build_editor_with_project(project.clone(), buffer, window, cx)
12064 });
12065 editor.update_in(cx, |editor, window, cx| {
12066 editor.set_text("one\ntwo\nthree\n", window, cx)
12067 });
12068
12069 cx.executor().start_waiting();
12070 let fake_server = fake_servers.next().await.unwrap();
12071
12072 let format = editor
12073 .update_in(cx, |editor, window, cx| {
12074 editor.perform_format(
12075 project.clone(),
12076 FormatTrigger::Manual,
12077 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12078 window,
12079 cx,
12080 )
12081 })
12082 .unwrap();
12083 fake_server
12084 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12085 assert_eq!(
12086 params.text_document.uri,
12087 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12088 );
12089 assert_eq!(params.options.tab_size, 4);
12090 Ok(Some(vec![lsp::TextEdit::new(
12091 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12092 ", ".to_string(),
12093 )]))
12094 })
12095 .next()
12096 .await;
12097 cx.executor().start_waiting();
12098 format.await;
12099 assert_eq!(
12100 editor.update(cx, |editor, cx| editor.text(cx)),
12101 "one, two\nthree\n"
12102 );
12103
12104 editor.update_in(cx, |editor, window, cx| {
12105 editor.set_text("one\ntwo\nthree\n", window, cx)
12106 });
12107 // Ensure we don't lock if formatting hangs.
12108 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12109 move |params, _| async move {
12110 assert_eq!(
12111 params.text_document.uri,
12112 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12113 );
12114 futures::future::pending::<()>().await;
12115 unreachable!()
12116 },
12117 );
12118 let format = editor
12119 .update_in(cx, |editor, window, cx| {
12120 editor.perform_format(
12121 project,
12122 FormatTrigger::Manual,
12123 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12124 window,
12125 cx,
12126 )
12127 })
12128 .unwrap();
12129 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12130 cx.executor().start_waiting();
12131 format.await;
12132 assert_eq!(
12133 editor.update(cx, |editor, cx| editor.text(cx)),
12134 "one\ntwo\nthree\n"
12135 );
12136}
12137
12138#[gpui::test]
12139async fn test_multiple_formatters(cx: &mut TestAppContext) {
12140 init_test(cx, |settings| {
12141 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12142 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12143 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12144 Formatter::CodeAction("code-action-1".into()),
12145 Formatter::CodeAction("code-action-2".into()),
12146 ]))
12147 });
12148
12149 let fs = FakeFs::new(cx.executor());
12150 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12151 .await;
12152
12153 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12154 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12155 language_registry.add(rust_lang());
12156
12157 let mut fake_servers = language_registry.register_fake_lsp(
12158 "Rust",
12159 FakeLspAdapter {
12160 capabilities: lsp::ServerCapabilities {
12161 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12162 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12163 commands: vec!["the-command-for-code-action-1".into()],
12164 ..Default::default()
12165 }),
12166 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12167 ..Default::default()
12168 },
12169 ..Default::default()
12170 },
12171 );
12172
12173 let buffer = project
12174 .update(cx, |project, cx| {
12175 project.open_local_buffer(path!("/file.rs"), cx)
12176 })
12177 .await
12178 .unwrap();
12179
12180 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12181 let (editor, cx) = cx.add_window_view(|window, cx| {
12182 build_editor_with_project(project.clone(), buffer, window, cx)
12183 });
12184
12185 cx.executor().start_waiting();
12186
12187 let fake_server = fake_servers.next().await.unwrap();
12188 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12189 move |_params, _| async move {
12190 Ok(Some(vec![lsp::TextEdit::new(
12191 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12192 "applied-formatting\n".to_string(),
12193 )]))
12194 },
12195 );
12196 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12197 move |params, _| async move {
12198 let requested_code_actions = params.context.only.expect("Expected code action request");
12199 assert_eq!(requested_code_actions.len(), 1);
12200
12201 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12202 let code_action = match requested_code_actions[0].as_str() {
12203 "code-action-1" => lsp::CodeAction {
12204 kind: Some("code-action-1".into()),
12205 edit: Some(lsp::WorkspaceEdit::new(
12206 [(
12207 uri,
12208 vec![lsp::TextEdit::new(
12209 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12210 "applied-code-action-1-edit\n".to_string(),
12211 )],
12212 )]
12213 .into_iter()
12214 .collect(),
12215 )),
12216 command: Some(lsp::Command {
12217 command: "the-command-for-code-action-1".into(),
12218 ..Default::default()
12219 }),
12220 ..Default::default()
12221 },
12222 "code-action-2" => lsp::CodeAction {
12223 kind: Some("code-action-2".into()),
12224 edit: Some(lsp::WorkspaceEdit::new(
12225 [(
12226 uri,
12227 vec![lsp::TextEdit::new(
12228 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12229 "applied-code-action-2-edit\n".to_string(),
12230 )],
12231 )]
12232 .into_iter()
12233 .collect(),
12234 )),
12235 ..Default::default()
12236 },
12237 req => panic!("Unexpected code action request: {:?}", req),
12238 };
12239 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12240 code_action,
12241 )]))
12242 },
12243 );
12244
12245 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12246 move |params, _| async move { Ok(params) }
12247 });
12248
12249 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12250 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12251 let fake = fake_server.clone();
12252 let lock = command_lock.clone();
12253 move |params, _| {
12254 assert_eq!(params.command, "the-command-for-code-action-1");
12255 let fake = fake.clone();
12256 let lock = lock.clone();
12257 async move {
12258 lock.lock().await;
12259 fake.server
12260 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12261 label: None,
12262 edit: lsp::WorkspaceEdit {
12263 changes: Some(
12264 [(
12265 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12266 vec![lsp::TextEdit {
12267 range: lsp::Range::new(
12268 lsp::Position::new(0, 0),
12269 lsp::Position::new(0, 0),
12270 ),
12271 new_text: "applied-code-action-1-command\n".into(),
12272 }],
12273 )]
12274 .into_iter()
12275 .collect(),
12276 ),
12277 ..Default::default()
12278 },
12279 })
12280 .await
12281 .into_response()
12282 .unwrap();
12283 Ok(Some(json!(null)))
12284 }
12285 }
12286 });
12287
12288 cx.executor().start_waiting();
12289 editor
12290 .update_in(cx, |editor, window, cx| {
12291 editor.perform_format(
12292 project.clone(),
12293 FormatTrigger::Manual,
12294 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12295 window,
12296 cx,
12297 )
12298 })
12299 .unwrap()
12300 .await;
12301 editor.update(cx, |editor, cx| {
12302 assert_eq!(
12303 editor.text(cx),
12304 r#"
12305 applied-code-action-2-edit
12306 applied-code-action-1-command
12307 applied-code-action-1-edit
12308 applied-formatting
12309 one
12310 two
12311 three
12312 "#
12313 .unindent()
12314 );
12315 });
12316
12317 editor.update_in(cx, |editor, window, cx| {
12318 editor.undo(&Default::default(), window, cx);
12319 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12320 });
12321
12322 // Perform a manual edit while waiting for an LSP command
12323 // that's being run as part of a formatting code action.
12324 let lock_guard = command_lock.lock().await;
12325 let format = editor
12326 .update_in(cx, |editor, window, cx| {
12327 editor.perform_format(
12328 project.clone(),
12329 FormatTrigger::Manual,
12330 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12331 window,
12332 cx,
12333 )
12334 })
12335 .unwrap();
12336 cx.run_until_parked();
12337 editor.update(cx, |editor, cx| {
12338 assert_eq!(
12339 editor.text(cx),
12340 r#"
12341 applied-code-action-1-edit
12342 applied-formatting
12343 one
12344 two
12345 three
12346 "#
12347 .unindent()
12348 );
12349
12350 editor.buffer.update(cx, |buffer, cx| {
12351 let ix = buffer.len(cx);
12352 buffer.edit([(ix..ix, "edited\n")], None, cx);
12353 });
12354 });
12355
12356 // Allow the LSP command to proceed. Because the buffer was edited,
12357 // the second code action will not be run.
12358 drop(lock_guard);
12359 format.await;
12360 editor.update_in(cx, |editor, window, cx| {
12361 assert_eq!(
12362 editor.text(cx),
12363 r#"
12364 applied-code-action-1-command
12365 applied-code-action-1-edit
12366 applied-formatting
12367 one
12368 two
12369 three
12370 edited
12371 "#
12372 .unindent()
12373 );
12374
12375 // The manual edit is undone first, because it is the last thing the user did
12376 // (even though the command completed afterwards).
12377 editor.undo(&Default::default(), window, cx);
12378 assert_eq!(
12379 editor.text(cx),
12380 r#"
12381 applied-code-action-1-command
12382 applied-code-action-1-edit
12383 applied-formatting
12384 one
12385 two
12386 three
12387 "#
12388 .unindent()
12389 );
12390
12391 // All the formatting (including the command, which completed after the manual edit)
12392 // is undone together.
12393 editor.undo(&Default::default(), window, cx);
12394 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12395 });
12396}
12397
12398#[gpui::test]
12399async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12400 init_test(cx, |settings| {
12401 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12402 settings::LanguageServerFormatterSpecifier::Current,
12403 )]))
12404 });
12405
12406 let fs = FakeFs::new(cx.executor());
12407 fs.insert_file(path!("/file.ts"), Default::default()).await;
12408
12409 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12410
12411 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12412 language_registry.add(Arc::new(Language::new(
12413 LanguageConfig {
12414 name: "TypeScript".into(),
12415 matcher: LanguageMatcher {
12416 path_suffixes: vec!["ts".to_string()],
12417 ..Default::default()
12418 },
12419 ..LanguageConfig::default()
12420 },
12421 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12422 )));
12423 update_test_language_settings(cx, |settings| {
12424 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12425 });
12426 let mut fake_servers = language_registry.register_fake_lsp(
12427 "TypeScript",
12428 FakeLspAdapter {
12429 capabilities: lsp::ServerCapabilities {
12430 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12431 ..Default::default()
12432 },
12433 ..Default::default()
12434 },
12435 );
12436
12437 let buffer = project
12438 .update(cx, |project, cx| {
12439 project.open_local_buffer(path!("/file.ts"), cx)
12440 })
12441 .await
12442 .unwrap();
12443
12444 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12445 let (editor, cx) = cx.add_window_view(|window, cx| {
12446 build_editor_with_project(project.clone(), buffer, window, cx)
12447 });
12448 editor.update_in(cx, |editor, window, cx| {
12449 editor.set_text(
12450 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12451 window,
12452 cx,
12453 )
12454 });
12455
12456 cx.executor().start_waiting();
12457 let fake_server = fake_servers.next().await.unwrap();
12458
12459 let format = editor
12460 .update_in(cx, |editor, window, cx| {
12461 editor.perform_code_action_kind(
12462 project.clone(),
12463 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12464 window,
12465 cx,
12466 )
12467 })
12468 .unwrap();
12469 fake_server
12470 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12471 assert_eq!(
12472 params.text_document.uri,
12473 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12474 );
12475 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12476 lsp::CodeAction {
12477 title: "Organize Imports".to_string(),
12478 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12479 edit: Some(lsp::WorkspaceEdit {
12480 changes: Some(
12481 [(
12482 params.text_document.uri.clone(),
12483 vec![lsp::TextEdit::new(
12484 lsp::Range::new(
12485 lsp::Position::new(1, 0),
12486 lsp::Position::new(2, 0),
12487 ),
12488 "".to_string(),
12489 )],
12490 )]
12491 .into_iter()
12492 .collect(),
12493 ),
12494 ..Default::default()
12495 }),
12496 ..Default::default()
12497 },
12498 )]))
12499 })
12500 .next()
12501 .await;
12502 cx.executor().start_waiting();
12503 format.await;
12504 assert_eq!(
12505 editor.update(cx, |editor, cx| editor.text(cx)),
12506 "import { a } from 'module';\n\nconst x = a;\n"
12507 );
12508
12509 editor.update_in(cx, |editor, window, cx| {
12510 editor.set_text(
12511 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12512 window,
12513 cx,
12514 )
12515 });
12516 // Ensure we don't lock if code action hangs.
12517 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12518 move |params, _| async move {
12519 assert_eq!(
12520 params.text_document.uri,
12521 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12522 );
12523 futures::future::pending::<()>().await;
12524 unreachable!()
12525 },
12526 );
12527 let format = editor
12528 .update_in(cx, |editor, window, cx| {
12529 editor.perform_code_action_kind(
12530 project,
12531 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12532 window,
12533 cx,
12534 )
12535 })
12536 .unwrap();
12537 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12538 cx.executor().start_waiting();
12539 format.await;
12540 assert_eq!(
12541 editor.update(cx, |editor, cx| editor.text(cx)),
12542 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12543 );
12544}
12545
12546#[gpui::test]
12547async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12548 init_test(cx, |_| {});
12549
12550 let mut cx = EditorLspTestContext::new_rust(
12551 lsp::ServerCapabilities {
12552 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12553 ..Default::default()
12554 },
12555 cx,
12556 )
12557 .await;
12558
12559 cx.set_state(indoc! {"
12560 one.twoˇ
12561 "});
12562
12563 // The format request takes a long time. When it completes, it inserts
12564 // a newline and an indent before the `.`
12565 cx.lsp
12566 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12567 let executor = cx.background_executor().clone();
12568 async move {
12569 executor.timer(Duration::from_millis(100)).await;
12570 Ok(Some(vec![lsp::TextEdit {
12571 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12572 new_text: "\n ".into(),
12573 }]))
12574 }
12575 });
12576
12577 // Submit a format request.
12578 let format_1 = cx
12579 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12580 .unwrap();
12581 cx.executor().run_until_parked();
12582
12583 // Submit a second format request.
12584 let format_2 = cx
12585 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12586 .unwrap();
12587 cx.executor().run_until_parked();
12588
12589 // Wait for both format requests to complete
12590 cx.executor().advance_clock(Duration::from_millis(200));
12591 cx.executor().start_waiting();
12592 format_1.await.unwrap();
12593 cx.executor().start_waiting();
12594 format_2.await.unwrap();
12595
12596 // The formatting edits only happens once.
12597 cx.assert_editor_state(indoc! {"
12598 one
12599 .twoˇ
12600 "});
12601}
12602
12603#[gpui::test]
12604async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12605 init_test(cx, |settings| {
12606 settings.defaults.formatter = Some(FormatterList::default())
12607 });
12608
12609 let mut cx = EditorLspTestContext::new_rust(
12610 lsp::ServerCapabilities {
12611 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12612 ..Default::default()
12613 },
12614 cx,
12615 )
12616 .await;
12617
12618 // Record which buffer changes have been sent to the language server
12619 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12620 cx.lsp
12621 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12622 let buffer_changes = buffer_changes.clone();
12623 move |params, _| {
12624 buffer_changes.lock().extend(
12625 params
12626 .content_changes
12627 .into_iter()
12628 .map(|e| (e.range.unwrap(), e.text)),
12629 );
12630 }
12631 });
12632 // Handle formatting requests to the language server.
12633 cx.lsp
12634 .set_request_handler::<lsp::request::Formatting, _, _>({
12635 let buffer_changes = buffer_changes.clone();
12636 move |_, _| {
12637 let buffer_changes = buffer_changes.clone();
12638 // Insert blank lines between each line of the buffer.
12639 async move {
12640 // When formatting is requested, trailing whitespace has already been stripped,
12641 // and the trailing newline has already been added.
12642 assert_eq!(
12643 &buffer_changes.lock()[1..],
12644 &[
12645 (
12646 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12647 "".into()
12648 ),
12649 (
12650 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12651 "".into()
12652 ),
12653 (
12654 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12655 "\n".into()
12656 ),
12657 ]
12658 );
12659
12660 Ok(Some(vec![
12661 lsp::TextEdit {
12662 range: lsp::Range::new(
12663 lsp::Position::new(1, 0),
12664 lsp::Position::new(1, 0),
12665 ),
12666 new_text: "\n".into(),
12667 },
12668 lsp::TextEdit {
12669 range: lsp::Range::new(
12670 lsp::Position::new(2, 0),
12671 lsp::Position::new(2, 0),
12672 ),
12673 new_text: "\n".into(),
12674 },
12675 ]))
12676 }
12677 }
12678 });
12679
12680 // Set up a buffer white some trailing whitespace and no trailing newline.
12681 cx.set_state(
12682 &[
12683 "one ", //
12684 "twoˇ", //
12685 "three ", //
12686 "four", //
12687 ]
12688 .join("\n"),
12689 );
12690 cx.run_until_parked();
12691
12692 // Submit a format request.
12693 let format = cx
12694 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12695 .unwrap();
12696
12697 cx.run_until_parked();
12698 // After formatting the buffer, the trailing whitespace is stripped,
12699 // a newline is appended, and the edits provided by the language server
12700 // have been applied.
12701 format.await.unwrap();
12702
12703 cx.assert_editor_state(
12704 &[
12705 "one", //
12706 "", //
12707 "twoˇ", //
12708 "", //
12709 "three", //
12710 "four", //
12711 "", //
12712 ]
12713 .join("\n"),
12714 );
12715
12716 // Undoing the formatting undoes the trailing whitespace removal, the
12717 // trailing newline, and the LSP edits.
12718 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12719 cx.assert_editor_state(
12720 &[
12721 "one ", //
12722 "twoˇ", //
12723 "three ", //
12724 "four", //
12725 ]
12726 .join("\n"),
12727 );
12728}
12729
12730#[gpui::test]
12731async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12732 cx: &mut TestAppContext,
12733) {
12734 init_test(cx, |_| {});
12735
12736 cx.update(|cx| {
12737 cx.update_global::<SettingsStore, _>(|settings, cx| {
12738 settings.update_user_settings(cx, |settings| {
12739 settings.editor.auto_signature_help = Some(true);
12740 });
12741 });
12742 });
12743
12744 let mut cx = EditorLspTestContext::new_rust(
12745 lsp::ServerCapabilities {
12746 signature_help_provider: Some(lsp::SignatureHelpOptions {
12747 ..Default::default()
12748 }),
12749 ..Default::default()
12750 },
12751 cx,
12752 )
12753 .await;
12754
12755 let language = Language::new(
12756 LanguageConfig {
12757 name: "Rust".into(),
12758 brackets: BracketPairConfig {
12759 pairs: vec![
12760 BracketPair {
12761 start: "{".to_string(),
12762 end: "}".to_string(),
12763 close: true,
12764 surround: true,
12765 newline: true,
12766 },
12767 BracketPair {
12768 start: "(".to_string(),
12769 end: ")".to_string(),
12770 close: true,
12771 surround: true,
12772 newline: true,
12773 },
12774 BracketPair {
12775 start: "/*".to_string(),
12776 end: " */".to_string(),
12777 close: true,
12778 surround: true,
12779 newline: true,
12780 },
12781 BracketPair {
12782 start: "[".to_string(),
12783 end: "]".to_string(),
12784 close: false,
12785 surround: false,
12786 newline: true,
12787 },
12788 BracketPair {
12789 start: "\"".to_string(),
12790 end: "\"".to_string(),
12791 close: true,
12792 surround: true,
12793 newline: false,
12794 },
12795 BracketPair {
12796 start: "<".to_string(),
12797 end: ">".to_string(),
12798 close: false,
12799 surround: true,
12800 newline: true,
12801 },
12802 ],
12803 ..Default::default()
12804 },
12805 autoclose_before: "})]".to_string(),
12806 ..Default::default()
12807 },
12808 Some(tree_sitter_rust::LANGUAGE.into()),
12809 );
12810 let language = Arc::new(language);
12811
12812 cx.language_registry().add(language.clone());
12813 cx.update_buffer(|buffer, cx| {
12814 buffer.set_language(Some(language), cx);
12815 });
12816
12817 cx.set_state(
12818 &r#"
12819 fn main() {
12820 sampleˇ
12821 }
12822 "#
12823 .unindent(),
12824 );
12825
12826 cx.update_editor(|editor, window, cx| {
12827 editor.handle_input("(", window, cx);
12828 });
12829 cx.assert_editor_state(
12830 &"
12831 fn main() {
12832 sample(ˇ)
12833 }
12834 "
12835 .unindent(),
12836 );
12837
12838 let mocked_response = lsp::SignatureHelp {
12839 signatures: vec![lsp::SignatureInformation {
12840 label: "fn sample(param1: u8, param2: u8)".to_string(),
12841 documentation: None,
12842 parameters: Some(vec![
12843 lsp::ParameterInformation {
12844 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12845 documentation: None,
12846 },
12847 lsp::ParameterInformation {
12848 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12849 documentation: None,
12850 },
12851 ]),
12852 active_parameter: None,
12853 }],
12854 active_signature: Some(0),
12855 active_parameter: Some(0),
12856 };
12857 handle_signature_help_request(&mut cx, mocked_response).await;
12858
12859 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12860 .await;
12861
12862 cx.editor(|editor, _, _| {
12863 let signature_help_state = editor.signature_help_state.popover().cloned();
12864 let signature = signature_help_state.unwrap();
12865 assert_eq!(
12866 signature.signatures[signature.current_signature].label,
12867 "fn sample(param1: u8, param2: u8)"
12868 );
12869 });
12870}
12871
12872#[gpui::test]
12873async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12874 init_test(cx, |_| {});
12875
12876 cx.update(|cx| {
12877 cx.update_global::<SettingsStore, _>(|settings, cx| {
12878 settings.update_user_settings(cx, |settings| {
12879 settings.editor.auto_signature_help = Some(false);
12880 settings.editor.show_signature_help_after_edits = Some(false);
12881 });
12882 });
12883 });
12884
12885 let mut cx = EditorLspTestContext::new_rust(
12886 lsp::ServerCapabilities {
12887 signature_help_provider: Some(lsp::SignatureHelpOptions {
12888 ..Default::default()
12889 }),
12890 ..Default::default()
12891 },
12892 cx,
12893 )
12894 .await;
12895
12896 let language = Language::new(
12897 LanguageConfig {
12898 name: "Rust".into(),
12899 brackets: BracketPairConfig {
12900 pairs: vec![
12901 BracketPair {
12902 start: "{".to_string(),
12903 end: "}".to_string(),
12904 close: true,
12905 surround: true,
12906 newline: true,
12907 },
12908 BracketPair {
12909 start: "(".to_string(),
12910 end: ")".to_string(),
12911 close: true,
12912 surround: true,
12913 newline: true,
12914 },
12915 BracketPair {
12916 start: "/*".to_string(),
12917 end: " */".to_string(),
12918 close: true,
12919 surround: true,
12920 newline: true,
12921 },
12922 BracketPair {
12923 start: "[".to_string(),
12924 end: "]".to_string(),
12925 close: false,
12926 surround: false,
12927 newline: true,
12928 },
12929 BracketPair {
12930 start: "\"".to_string(),
12931 end: "\"".to_string(),
12932 close: true,
12933 surround: true,
12934 newline: false,
12935 },
12936 BracketPair {
12937 start: "<".to_string(),
12938 end: ">".to_string(),
12939 close: false,
12940 surround: true,
12941 newline: true,
12942 },
12943 ],
12944 ..Default::default()
12945 },
12946 autoclose_before: "})]".to_string(),
12947 ..Default::default()
12948 },
12949 Some(tree_sitter_rust::LANGUAGE.into()),
12950 );
12951 let language = Arc::new(language);
12952
12953 cx.language_registry().add(language.clone());
12954 cx.update_buffer(|buffer, cx| {
12955 buffer.set_language(Some(language), cx);
12956 });
12957
12958 // Ensure that signature_help is not called when no signature help is enabled.
12959 cx.set_state(
12960 &r#"
12961 fn main() {
12962 sampleˇ
12963 }
12964 "#
12965 .unindent(),
12966 );
12967 cx.update_editor(|editor, window, cx| {
12968 editor.handle_input("(", window, cx);
12969 });
12970 cx.assert_editor_state(
12971 &"
12972 fn main() {
12973 sample(ˇ)
12974 }
12975 "
12976 .unindent(),
12977 );
12978 cx.editor(|editor, _, _| {
12979 assert!(editor.signature_help_state.task().is_none());
12980 });
12981
12982 let mocked_response = lsp::SignatureHelp {
12983 signatures: vec![lsp::SignatureInformation {
12984 label: "fn sample(param1: u8, param2: u8)".to_string(),
12985 documentation: None,
12986 parameters: Some(vec![
12987 lsp::ParameterInformation {
12988 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12989 documentation: None,
12990 },
12991 lsp::ParameterInformation {
12992 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12993 documentation: None,
12994 },
12995 ]),
12996 active_parameter: None,
12997 }],
12998 active_signature: Some(0),
12999 active_parameter: Some(0),
13000 };
13001
13002 // Ensure that signature_help is called when enabled afte edits
13003 cx.update(|_, cx| {
13004 cx.update_global::<SettingsStore, _>(|settings, cx| {
13005 settings.update_user_settings(cx, |settings| {
13006 settings.editor.auto_signature_help = Some(false);
13007 settings.editor.show_signature_help_after_edits = Some(true);
13008 });
13009 });
13010 });
13011 cx.set_state(
13012 &r#"
13013 fn main() {
13014 sampleˇ
13015 }
13016 "#
13017 .unindent(),
13018 );
13019 cx.update_editor(|editor, window, cx| {
13020 editor.handle_input("(", window, cx);
13021 });
13022 cx.assert_editor_state(
13023 &"
13024 fn main() {
13025 sample(ˇ)
13026 }
13027 "
13028 .unindent(),
13029 );
13030 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13031 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13032 .await;
13033 cx.update_editor(|editor, _, _| {
13034 let signature_help_state = editor.signature_help_state.popover().cloned();
13035 assert!(signature_help_state.is_some());
13036 let signature = signature_help_state.unwrap();
13037 assert_eq!(
13038 signature.signatures[signature.current_signature].label,
13039 "fn sample(param1: u8, param2: u8)"
13040 );
13041 editor.signature_help_state = SignatureHelpState::default();
13042 });
13043
13044 // Ensure that signature_help is called when auto signature help override is enabled
13045 cx.update(|_, cx| {
13046 cx.update_global::<SettingsStore, _>(|settings, cx| {
13047 settings.update_user_settings(cx, |settings| {
13048 settings.editor.auto_signature_help = Some(true);
13049 settings.editor.show_signature_help_after_edits = Some(false);
13050 });
13051 });
13052 });
13053 cx.set_state(
13054 &r#"
13055 fn main() {
13056 sampleˇ
13057 }
13058 "#
13059 .unindent(),
13060 );
13061 cx.update_editor(|editor, window, cx| {
13062 editor.handle_input("(", window, cx);
13063 });
13064 cx.assert_editor_state(
13065 &"
13066 fn main() {
13067 sample(ˇ)
13068 }
13069 "
13070 .unindent(),
13071 );
13072 handle_signature_help_request(&mut cx, mocked_response).await;
13073 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13074 .await;
13075 cx.editor(|editor, _, _| {
13076 let signature_help_state = editor.signature_help_state.popover().cloned();
13077 assert!(signature_help_state.is_some());
13078 let signature = signature_help_state.unwrap();
13079 assert_eq!(
13080 signature.signatures[signature.current_signature].label,
13081 "fn sample(param1: u8, param2: u8)"
13082 );
13083 });
13084}
13085
13086#[gpui::test]
13087async fn test_signature_help(cx: &mut TestAppContext) {
13088 init_test(cx, |_| {});
13089 cx.update(|cx| {
13090 cx.update_global::<SettingsStore, _>(|settings, cx| {
13091 settings.update_user_settings(cx, |settings| {
13092 settings.editor.auto_signature_help = Some(true);
13093 });
13094 });
13095 });
13096
13097 let mut cx = EditorLspTestContext::new_rust(
13098 lsp::ServerCapabilities {
13099 signature_help_provider: Some(lsp::SignatureHelpOptions {
13100 ..Default::default()
13101 }),
13102 ..Default::default()
13103 },
13104 cx,
13105 )
13106 .await;
13107
13108 // A test that directly calls `show_signature_help`
13109 cx.update_editor(|editor, window, cx| {
13110 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13111 });
13112
13113 let mocked_response = lsp::SignatureHelp {
13114 signatures: vec![lsp::SignatureInformation {
13115 label: "fn sample(param1: u8, param2: u8)".to_string(),
13116 documentation: None,
13117 parameters: Some(vec![
13118 lsp::ParameterInformation {
13119 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13120 documentation: None,
13121 },
13122 lsp::ParameterInformation {
13123 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13124 documentation: None,
13125 },
13126 ]),
13127 active_parameter: None,
13128 }],
13129 active_signature: Some(0),
13130 active_parameter: Some(0),
13131 };
13132 handle_signature_help_request(&mut cx, mocked_response).await;
13133
13134 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13135 .await;
13136
13137 cx.editor(|editor, _, _| {
13138 let signature_help_state = editor.signature_help_state.popover().cloned();
13139 assert!(signature_help_state.is_some());
13140 let signature = signature_help_state.unwrap();
13141 assert_eq!(
13142 signature.signatures[signature.current_signature].label,
13143 "fn sample(param1: u8, param2: u8)"
13144 );
13145 });
13146
13147 // When exiting outside from inside the brackets, `signature_help` is closed.
13148 cx.set_state(indoc! {"
13149 fn main() {
13150 sample(ˇ);
13151 }
13152
13153 fn sample(param1: u8, param2: u8) {}
13154 "});
13155
13156 cx.update_editor(|editor, window, cx| {
13157 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13158 s.select_ranges([0..0])
13159 });
13160 });
13161
13162 let mocked_response = lsp::SignatureHelp {
13163 signatures: Vec::new(),
13164 active_signature: None,
13165 active_parameter: None,
13166 };
13167 handle_signature_help_request(&mut cx, mocked_response).await;
13168
13169 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13170 .await;
13171
13172 cx.editor(|editor, _, _| {
13173 assert!(!editor.signature_help_state.is_shown());
13174 });
13175
13176 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13177 cx.set_state(indoc! {"
13178 fn main() {
13179 sample(ˇ);
13180 }
13181
13182 fn sample(param1: u8, param2: u8) {}
13183 "});
13184
13185 let mocked_response = lsp::SignatureHelp {
13186 signatures: vec![lsp::SignatureInformation {
13187 label: "fn sample(param1: u8, param2: u8)".to_string(),
13188 documentation: None,
13189 parameters: Some(vec![
13190 lsp::ParameterInformation {
13191 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13192 documentation: None,
13193 },
13194 lsp::ParameterInformation {
13195 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13196 documentation: None,
13197 },
13198 ]),
13199 active_parameter: None,
13200 }],
13201 active_signature: Some(0),
13202 active_parameter: Some(0),
13203 };
13204 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13205 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13206 .await;
13207 cx.editor(|editor, _, _| {
13208 assert!(editor.signature_help_state.is_shown());
13209 });
13210
13211 // Restore the popover with more parameter input
13212 cx.set_state(indoc! {"
13213 fn main() {
13214 sample(param1, param2ˇ);
13215 }
13216
13217 fn sample(param1: u8, param2: u8) {}
13218 "});
13219
13220 let mocked_response = lsp::SignatureHelp {
13221 signatures: vec![lsp::SignatureInformation {
13222 label: "fn sample(param1: u8, param2: u8)".to_string(),
13223 documentation: None,
13224 parameters: Some(vec![
13225 lsp::ParameterInformation {
13226 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13227 documentation: None,
13228 },
13229 lsp::ParameterInformation {
13230 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13231 documentation: None,
13232 },
13233 ]),
13234 active_parameter: None,
13235 }],
13236 active_signature: Some(0),
13237 active_parameter: Some(1),
13238 };
13239 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13240 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13241 .await;
13242
13243 // When selecting a range, the popover is gone.
13244 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13245 cx.update_editor(|editor, window, cx| {
13246 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13247 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13248 })
13249 });
13250 cx.assert_editor_state(indoc! {"
13251 fn main() {
13252 sample(param1, «ˇparam2»);
13253 }
13254
13255 fn sample(param1: u8, param2: u8) {}
13256 "});
13257 cx.editor(|editor, _, _| {
13258 assert!(!editor.signature_help_state.is_shown());
13259 });
13260
13261 // When unselecting again, the popover is back if within the brackets.
13262 cx.update_editor(|editor, window, cx| {
13263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13264 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13265 })
13266 });
13267 cx.assert_editor_state(indoc! {"
13268 fn main() {
13269 sample(param1, ˇparam2);
13270 }
13271
13272 fn sample(param1: u8, param2: u8) {}
13273 "});
13274 handle_signature_help_request(&mut cx, mocked_response).await;
13275 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13276 .await;
13277 cx.editor(|editor, _, _| {
13278 assert!(editor.signature_help_state.is_shown());
13279 });
13280
13281 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13282 cx.update_editor(|editor, window, cx| {
13283 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13284 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13285 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13286 })
13287 });
13288 cx.assert_editor_state(indoc! {"
13289 fn main() {
13290 sample(param1, ˇparam2);
13291 }
13292
13293 fn sample(param1: u8, param2: u8) {}
13294 "});
13295
13296 let mocked_response = lsp::SignatureHelp {
13297 signatures: vec![lsp::SignatureInformation {
13298 label: "fn sample(param1: u8, param2: u8)".to_string(),
13299 documentation: None,
13300 parameters: Some(vec![
13301 lsp::ParameterInformation {
13302 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13303 documentation: None,
13304 },
13305 lsp::ParameterInformation {
13306 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13307 documentation: None,
13308 },
13309 ]),
13310 active_parameter: None,
13311 }],
13312 active_signature: Some(0),
13313 active_parameter: Some(1),
13314 };
13315 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13316 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13317 .await;
13318 cx.update_editor(|editor, _, cx| {
13319 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13320 });
13321 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13322 .await;
13323 cx.update_editor(|editor, window, cx| {
13324 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13325 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13326 })
13327 });
13328 cx.assert_editor_state(indoc! {"
13329 fn main() {
13330 sample(param1, «ˇparam2»);
13331 }
13332
13333 fn sample(param1: u8, param2: u8) {}
13334 "});
13335 cx.update_editor(|editor, window, cx| {
13336 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13337 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13338 })
13339 });
13340 cx.assert_editor_state(indoc! {"
13341 fn main() {
13342 sample(param1, ˇparam2);
13343 }
13344
13345 fn sample(param1: u8, param2: u8) {}
13346 "});
13347 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13348 .await;
13349}
13350
13351#[gpui::test]
13352async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13353 init_test(cx, |_| {});
13354
13355 let mut cx = EditorLspTestContext::new_rust(
13356 lsp::ServerCapabilities {
13357 signature_help_provider: Some(lsp::SignatureHelpOptions {
13358 ..Default::default()
13359 }),
13360 ..Default::default()
13361 },
13362 cx,
13363 )
13364 .await;
13365
13366 cx.set_state(indoc! {"
13367 fn main() {
13368 overloadedˇ
13369 }
13370 "});
13371
13372 cx.update_editor(|editor, window, cx| {
13373 editor.handle_input("(", window, cx);
13374 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13375 });
13376
13377 // Mock response with 3 signatures
13378 let mocked_response = lsp::SignatureHelp {
13379 signatures: vec![
13380 lsp::SignatureInformation {
13381 label: "fn overloaded(x: i32)".to_string(),
13382 documentation: None,
13383 parameters: Some(vec![lsp::ParameterInformation {
13384 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13385 documentation: None,
13386 }]),
13387 active_parameter: None,
13388 },
13389 lsp::SignatureInformation {
13390 label: "fn overloaded(x: i32, y: i32)".to_string(),
13391 documentation: None,
13392 parameters: Some(vec![
13393 lsp::ParameterInformation {
13394 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13395 documentation: None,
13396 },
13397 lsp::ParameterInformation {
13398 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13399 documentation: None,
13400 },
13401 ]),
13402 active_parameter: None,
13403 },
13404 lsp::SignatureInformation {
13405 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13406 documentation: None,
13407 parameters: Some(vec![
13408 lsp::ParameterInformation {
13409 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13410 documentation: None,
13411 },
13412 lsp::ParameterInformation {
13413 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13414 documentation: None,
13415 },
13416 lsp::ParameterInformation {
13417 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13418 documentation: None,
13419 },
13420 ]),
13421 active_parameter: None,
13422 },
13423 ],
13424 active_signature: Some(1),
13425 active_parameter: Some(0),
13426 };
13427 handle_signature_help_request(&mut cx, mocked_response).await;
13428
13429 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13430 .await;
13431
13432 // Verify we have multiple signatures and the right one is selected
13433 cx.editor(|editor, _, _| {
13434 let popover = editor.signature_help_state.popover().cloned().unwrap();
13435 assert_eq!(popover.signatures.len(), 3);
13436 // active_signature was 1, so that should be the current
13437 assert_eq!(popover.current_signature, 1);
13438 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13439 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13440 assert_eq!(
13441 popover.signatures[2].label,
13442 "fn overloaded(x: i32, y: i32, z: i32)"
13443 );
13444 });
13445
13446 // Test navigation functionality
13447 cx.update_editor(|editor, window, cx| {
13448 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13449 });
13450
13451 cx.editor(|editor, _, _| {
13452 let popover = editor.signature_help_state.popover().cloned().unwrap();
13453 assert_eq!(popover.current_signature, 2);
13454 });
13455
13456 // Test wrap around
13457 cx.update_editor(|editor, window, cx| {
13458 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13459 });
13460
13461 cx.editor(|editor, _, _| {
13462 let popover = editor.signature_help_state.popover().cloned().unwrap();
13463 assert_eq!(popover.current_signature, 0);
13464 });
13465
13466 // Test previous navigation
13467 cx.update_editor(|editor, window, cx| {
13468 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13469 });
13470
13471 cx.editor(|editor, _, _| {
13472 let popover = editor.signature_help_state.popover().cloned().unwrap();
13473 assert_eq!(popover.current_signature, 2);
13474 });
13475}
13476
13477#[gpui::test]
13478async fn test_completion_mode(cx: &mut TestAppContext) {
13479 init_test(cx, |_| {});
13480 let mut cx = EditorLspTestContext::new_rust(
13481 lsp::ServerCapabilities {
13482 completion_provider: Some(lsp::CompletionOptions {
13483 resolve_provider: Some(true),
13484 ..Default::default()
13485 }),
13486 ..Default::default()
13487 },
13488 cx,
13489 )
13490 .await;
13491
13492 struct Run {
13493 run_description: &'static str,
13494 initial_state: String,
13495 buffer_marked_text: String,
13496 completion_label: &'static str,
13497 completion_text: &'static str,
13498 expected_with_insert_mode: String,
13499 expected_with_replace_mode: String,
13500 expected_with_replace_subsequence_mode: String,
13501 expected_with_replace_suffix_mode: String,
13502 }
13503
13504 let runs = [
13505 Run {
13506 run_description: "Start of word matches completion text",
13507 initial_state: "before ediˇ after".into(),
13508 buffer_marked_text: "before <edi|> after".into(),
13509 completion_label: "editor",
13510 completion_text: "editor",
13511 expected_with_insert_mode: "before editorˇ after".into(),
13512 expected_with_replace_mode: "before editorˇ after".into(),
13513 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13514 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13515 },
13516 Run {
13517 run_description: "Accept same text at the middle of the word",
13518 initial_state: "before ediˇtor after".into(),
13519 buffer_marked_text: "before <edi|tor> after".into(),
13520 completion_label: "editor",
13521 completion_text: "editor",
13522 expected_with_insert_mode: "before editorˇtor after".into(),
13523 expected_with_replace_mode: "before editorˇ after".into(),
13524 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13525 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13526 },
13527 Run {
13528 run_description: "End of word matches completion text -- cursor at end",
13529 initial_state: "before torˇ after".into(),
13530 buffer_marked_text: "before <tor|> after".into(),
13531 completion_label: "editor",
13532 completion_text: "editor",
13533 expected_with_insert_mode: "before editorˇ after".into(),
13534 expected_with_replace_mode: "before editorˇ after".into(),
13535 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13536 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13537 },
13538 Run {
13539 run_description: "End of word matches completion text -- cursor at start",
13540 initial_state: "before ˇtor after".into(),
13541 buffer_marked_text: "before <|tor> after".into(),
13542 completion_label: "editor",
13543 completion_text: "editor",
13544 expected_with_insert_mode: "before editorˇtor after".into(),
13545 expected_with_replace_mode: "before editorˇ after".into(),
13546 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13547 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13548 },
13549 Run {
13550 run_description: "Prepend text containing whitespace",
13551 initial_state: "pˇfield: bool".into(),
13552 buffer_marked_text: "<p|field>: bool".into(),
13553 completion_label: "pub ",
13554 completion_text: "pub ",
13555 expected_with_insert_mode: "pub ˇfield: bool".into(),
13556 expected_with_replace_mode: "pub ˇ: bool".into(),
13557 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13558 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13559 },
13560 Run {
13561 run_description: "Add element to start of list",
13562 initial_state: "[element_ˇelement_2]".into(),
13563 buffer_marked_text: "[<element_|element_2>]".into(),
13564 completion_label: "element_1",
13565 completion_text: "element_1",
13566 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13567 expected_with_replace_mode: "[element_1ˇ]".into(),
13568 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13569 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13570 },
13571 Run {
13572 run_description: "Add element to start of list -- first and second elements are equal",
13573 initial_state: "[elˇelement]".into(),
13574 buffer_marked_text: "[<el|element>]".into(),
13575 completion_label: "element",
13576 completion_text: "element",
13577 expected_with_insert_mode: "[elementˇelement]".into(),
13578 expected_with_replace_mode: "[elementˇ]".into(),
13579 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13580 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13581 },
13582 Run {
13583 run_description: "Ends with matching suffix",
13584 initial_state: "SubˇError".into(),
13585 buffer_marked_text: "<Sub|Error>".into(),
13586 completion_label: "SubscriptionError",
13587 completion_text: "SubscriptionError",
13588 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13589 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13590 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13591 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13592 },
13593 Run {
13594 run_description: "Suffix is a subsequence -- contiguous",
13595 initial_state: "SubˇErr".into(),
13596 buffer_marked_text: "<Sub|Err>".into(),
13597 completion_label: "SubscriptionError",
13598 completion_text: "SubscriptionError",
13599 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13600 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13601 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13602 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13603 },
13604 Run {
13605 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13606 initial_state: "Suˇscrirr".into(),
13607 buffer_marked_text: "<Su|scrirr>".into(),
13608 completion_label: "SubscriptionError",
13609 completion_text: "SubscriptionError",
13610 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13611 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13612 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13613 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13614 },
13615 Run {
13616 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13617 initial_state: "foo(indˇix)".into(),
13618 buffer_marked_text: "foo(<ind|ix>)".into(),
13619 completion_label: "node_index",
13620 completion_text: "node_index",
13621 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13622 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13623 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13624 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13625 },
13626 Run {
13627 run_description: "Replace range ends before cursor - should extend to cursor",
13628 initial_state: "before editˇo after".into(),
13629 buffer_marked_text: "before <{ed}>it|o after".into(),
13630 completion_label: "editor",
13631 completion_text: "editor",
13632 expected_with_insert_mode: "before editorˇo after".into(),
13633 expected_with_replace_mode: "before editorˇo after".into(),
13634 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13635 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13636 },
13637 Run {
13638 run_description: "Uses label for suffix matching",
13639 initial_state: "before ediˇtor after".into(),
13640 buffer_marked_text: "before <edi|tor> after".into(),
13641 completion_label: "editor",
13642 completion_text: "editor()",
13643 expected_with_insert_mode: "before editor()ˇtor after".into(),
13644 expected_with_replace_mode: "before editor()ˇ after".into(),
13645 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13646 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13647 },
13648 Run {
13649 run_description: "Case insensitive subsequence and suffix matching",
13650 initial_state: "before EDiˇtoR after".into(),
13651 buffer_marked_text: "before <EDi|toR> after".into(),
13652 completion_label: "editor",
13653 completion_text: "editor",
13654 expected_with_insert_mode: "before editorˇtoR after".into(),
13655 expected_with_replace_mode: "before editorˇ after".into(),
13656 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13657 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13658 },
13659 ];
13660
13661 for run in runs {
13662 let run_variations = [
13663 (LspInsertMode::Insert, run.expected_with_insert_mode),
13664 (LspInsertMode::Replace, run.expected_with_replace_mode),
13665 (
13666 LspInsertMode::ReplaceSubsequence,
13667 run.expected_with_replace_subsequence_mode,
13668 ),
13669 (
13670 LspInsertMode::ReplaceSuffix,
13671 run.expected_with_replace_suffix_mode,
13672 ),
13673 ];
13674
13675 for (lsp_insert_mode, expected_text) in run_variations {
13676 eprintln!(
13677 "run = {:?}, mode = {lsp_insert_mode:.?}",
13678 run.run_description,
13679 );
13680
13681 update_test_language_settings(&mut cx, |settings| {
13682 settings.defaults.completions = Some(CompletionSettingsContent {
13683 lsp_insert_mode: Some(lsp_insert_mode),
13684 words: Some(WordsCompletionMode::Disabled),
13685 words_min_length: Some(0),
13686 ..Default::default()
13687 });
13688 });
13689
13690 cx.set_state(&run.initial_state);
13691 cx.update_editor(|editor, window, cx| {
13692 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13693 });
13694
13695 let counter = Arc::new(AtomicUsize::new(0));
13696 handle_completion_request_with_insert_and_replace(
13697 &mut cx,
13698 &run.buffer_marked_text,
13699 vec![(run.completion_label, run.completion_text)],
13700 counter.clone(),
13701 )
13702 .await;
13703 cx.condition(|editor, _| editor.context_menu_visible())
13704 .await;
13705 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13706
13707 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13708 editor
13709 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13710 .unwrap()
13711 });
13712 cx.assert_editor_state(&expected_text);
13713 handle_resolve_completion_request(&mut cx, None).await;
13714 apply_additional_edits.await.unwrap();
13715 }
13716 }
13717}
13718
13719#[gpui::test]
13720async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13721 init_test(cx, |_| {});
13722 let mut cx = EditorLspTestContext::new_rust(
13723 lsp::ServerCapabilities {
13724 completion_provider: Some(lsp::CompletionOptions {
13725 resolve_provider: Some(true),
13726 ..Default::default()
13727 }),
13728 ..Default::default()
13729 },
13730 cx,
13731 )
13732 .await;
13733
13734 let initial_state = "SubˇError";
13735 let buffer_marked_text = "<Sub|Error>";
13736 let completion_text = "SubscriptionError";
13737 let expected_with_insert_mode = "SubscriptionErrorˇError";
13738 let expected_with_replace_mode = "SubscriptionErrorˇ";
13739
13740 update_test_language_settings(&mut cx, |settings| {
13741 settings.defaults.completions = Some(CompletionSettingsContent {
13742 words: Some(WordsCompletionMode::Disabled),
13743 words_min_length: Some(0),
13744 // set the opposite here to ensure that the action is overriding the default behavior
13745 lsp_insert_mode: Some(LspInsertMode::Insert),
13746 ..Default::default()
13747 });
13748 });
13749
13750 cx.set_state(initial_state);
13751 cx.update_editor(|editor, window, cx| {
13752 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13753 });
13754
13755 let counter = Arc::new(AtomicUsize::new(0));
13756 handle_completion_request_with_insert_and_replace(
13757 &mut cx,
13758 buffer_marked_text,
13759 vec![(completion_text, completion_text)],
13760 counter.clone(),
13761 )
13762 .await;
13763 cx.condition(|editor, _| editor.context_menu_visible())
13764 .await;
13765 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13766
13767 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13768 editor
13769 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13770 .unwrap()
13771 });
13772 cx.assert_editor_state(expected_with_replace_mode);
13773 handle_resolve_completion_request(&mut cx, None).await;
13774 apply_additional_edits.await.unwrap();
13775
13776 update_test_language_settings(&mut cx, |settings| {
13777 settings.defaults.completions = Some(CompletionSettingsContent {
13778 words: Some(WordsCompletionMode::Disabled),
13779 words_min_length: Some(0),
13780 // set the opposite here to ensure that the action is overriding the default behavior
13781 lsp_insert_mode: Some(LspInsertMode::Replace),
13782 ..Default::default()
13783 });
13784 });
13785
13786 cx.set_state(initial_state);
13787 cx.update_editor(|editor, window, cx| {
13788 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13789 });
13790 handle_completion_request_with_insert_and_replace(
13791 &mut cx,
13792 buffer_marked_text,
13793 vec![(completion_text, completion_text)],
13794 counter.clone(),
13795 )
13796 .await;
13797 cx.condition(|editor, _| editor.context_menu_visible())
13798 .await;
13799 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13800
13801 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13802 editor
13803 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13804 .unwrap()
13805 });
13806 cx.assert_editor_state(expected_with_insert_mode);
13807 handle_resolve_completion_request(&mut cx, None).await;
13808 apply_additional_edits.await.unwrap();
13809}
13810
13811#[gpui::test]
13812async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13813 init_test(cx, |_| {});
13814 let mut cx = EditorLspTestContext::new_rust(
13815 lsp::ServerCapabilities {
13816 completion_provider: Some(lsp::CompletionOptions {
13817 resolve_provider: Some(true),
13818 ..Default::default()
13819 }),
13820 ..Default::default()
13821 },
13822 cx,
13823 )
13824 .await;
13825
13826 // scenario: surrounding text matches completion text
13827 let completion_text = "to_offset";
13828 let initial_state = indoc! {"
13829 1. buf.to_offˇsuffix
13830 2. buf.to_offˇsuf
13831 3. buf.to_offˇfix
13832 4. buf.to_offˇ
13833 5. into_offˇensive
13834 6. ˇsuffix
13835 7. let ˇ //
13836 8. aaˇzz
13837 9. buf.to_off«zzzzzˇ»suffix
13838 10. buf.«ˇzzzzz»suffix
13839 11. to_off«ˇzzzzz»
13840
13841 buf.to_offˇsuffix // newest cursor
13842 "};
13843 let completion_marked_buffer = indoc! {"
13844 1. buf.to_offsuffix
13845 2. buf.to_offsuf
13846 3. buf.to_offfix
13847 4. buf.to_off
13848 5. into_offensive
13849 6. suffix
13850 7. let //
13851 8. aazz
13852 9. buf.to_offzzzzzsuffix
13853 10. buf.zzzzzsuffix
13854 11. to_offzzzzz
13855
13856 buf.<to_off|suffix> // newest cursor
13857 "};
13858 let expected = indoc! {"
13859 1. buf.to_offsetˇ
13860 2. buf.to_offsetˇsuf
13861 3. buf.to_offsetˇfix
13862 4. buf.to_offsetˇ
13863 5. into_offsetˇensive
13864 6. to_offsetˇsuffix
13865 7. let to_offsetˇ //
13866 8. aato_offsetˇzz
13867 9. buf.to_offsetˇ
13868 10. buf.to_offsetˇsuffix
13869 11. to_offsetˇ
13870
13871 buf.to_offsetˇ // newest cursor
13872 "};
13873 cx.set_state(initial_state);
13874 cx.update_editor(|editor, window, cx| {
13875 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13876 });
13877 handle_completion_request_with_insert_and_replace(
13878 &mut cx,
13879 completion_marked_buffer,
13880 vec![(completion_text, completion_text)],
13881 Arc::new(AtomicUsize::new(0)),
13882 )
13883 .await;
13884 cx.condition(|editor, _| editor.context_menu_visible())
13885 .await;
13886 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13887 editor
13888 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13889 .unwrap()
13890 });
13891 cx.assert_editor_state(expected);
13892 handle_resolve_completion_request(&mut cx, None).await;
13893 apply_additional_edits.await.unwrap();
13894
13895 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13896 let completion_text = "foo_and_bar";
13897 let initial_state = indoc! {"
13898 1. ooanbˇ
13899 2. zooanbˇ
13900 3. ooanbˇz
13901 4. zooanbˇz
13902 5. ooanˇ
13903 6. oanbˇ
13904
13905 ooanbˇ
13906 "};
13907 let completion_marked_buffer = indoc! {"
13908 1. ooanb
13909 2. zooanb
13910 3. ooanbz
13911 4. zooanbz
13912 5. ooan
13913 6. oanb
13914
13915 <ooanb|>
13916 "};
13917 let expected = indoc! {"
13918 1. foo_and_barˇ
13919 2. zfoo_and_barˇ
13920 3. foo_and_barˇz
13921 4. zfoo_and_barˇz
13922 5. ooanfoo_and_barˇ
13923 6. oanbfoo_and_barˇ
13924
13925 foo_and_barˇ
13926 "};
13927 cx.set_state(initial_state);
13928 cx.update_editor(|editor, window, cx| {
13929 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13930 });
13931 handle_completion_request_with_insert_and_replace(
13932 &mut cx,
13933 completion_marked_buffer,
13934 vec![(completion_text, completion_text)],
13935 Arc::new(AtomicUsize::new(0)),
13936 )
13937 .await;
13938 cx.condition(|editor, _| editor.context_menu_visible())
13939 .await;
13940 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13941 editor
13942 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13943 .unwrap()
13944 });
13945 cx.assert_editor_state(expected);
13946 handle_resolve_completion_request(&mut cx, None).await;
13947 apply_additional_edits.await.unwrap();
13948
13949 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13950 // (expects the same as if it was inserted at the end)
13951 let completion_text = "foo_and_bar";
13952 let initial_state = indoc! {"
13953 1. ooˇanb
13954 2. zooˇanb
13955 3. ooˇanbz
13956 4. zooˇanbz
13957
13958 ooˇanb
13959 "};
13960 let completion_marked_buffer = indoc! {"
13961 1. ooanb
13962 2. zooanb
13963 3. ooanbz
13964 4. zooanbz
13965
13966 <oo|anb>
13967 "};
13968 let expected = indoc! {"
13969 1. foo_and_barˇ
13970 2. zfoo_and_barˇ
13971 3. foo_and_barˇz
13972 4. zfoo_and_barˇz
13973
13974 foo_and_barˇ
13975 "};
13976 cx.set_state(initial_state);
13977 cx.update_editor(|editor, window, cx| {
13978 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13979 });
13980 handle_completion_request_with_insert_and_replace(
13981 &mut cx,
13982 completion_marked_buffer,
13983 vec![(completion_text, completion_text)],
13984 Arc::new(AtomicUsize::new(0)),
13985 )
13986 .await;
13987 cx.condition(|editor, _| editor.context_menu_visible())
13988 .await;
13989 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13990 editor
13991 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13992 .unwrap()
13993 });
13994 cx.assert_editor_state(expected);
13995 handle_resolve_completion_request(&mut cx, None).await;
13996 apply_additional_edits.await.unwrap();
13997}
13998
13999// This used to crash
14000#[gpui::test]
14001async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14002 init_test(cx, |_| {});
14003
14004 let buffer_text = indoc! {"
14005 fn main() {
14006 10.satu;
14007
14008 //
14009 // separate cursors so they open in different excerpts (manually reproducible)
14010 //
14011
14012 10.satu20;
14013 }
14014 "};
14015 let multibuffer_text_with_selections = indoc! {"
14016 fn main() {
14017 10.satuˇ;
14018
14019 //
14020
14021 //
14022
14023 10.satuˇ20;
14024 }
14025 "};
14026 let expected_multibuffer = indoc! {"
14027 fn main() {
14028 10.saturating_sub()ˇ;
14029
14030 //
14031
14032 //
14033
14034 10.saturating_sub()ˇ;
14035 }
14036 "};
14037
14038 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14039 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14040
14041 let fs = FakeFs::new(cx.executor());
14042 fs.insert_tree(
14043 path!("/a"),
14044 json!({
14045 "main.rs": buffer_text,
14046 }),
14047 )
14048 .await;
14049
14050 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14051 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14052 language_registry.add(rust_lang());
14053 let mut fake_servers = language_registry.register_fake_lsp(
14054 "Rust",
14055 FakeLspAdapter {
14056 capabilities: lsp::ServerCapabilities {
14057 completion_provider: Some(lsp::CompletionOptions {
14058 resolve_provider: None,
14059 ..lsp::CompletionOptions::default()
14060 }),
14061 ..lsp::ServerCapabilities::default()
14062 },
14063 ..FakeLspAdapter::default()
14064 },
14065 );
14066 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14067 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14068 let buffer = project
14069 .update(cx, |project, cx| {
14070 project.open_local_buffer(path!("/a/main.rs"), cx)
14071 })
14072 .await
14073 .unwrap();
14074
14075 let multi_buffer = cx.new(|cx| {
14076 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14077 multi_buffer.push_excerpts(
14078 buffer.clone(),
14079 [ExcerptRange::new(0..first_excerpt_end)],
14080 cx,
14081 );
14082 multi_buffer.push_excerpts(
14083 buffer.clone(),
14084 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14085 cx,
14086 );
14087 multi_buffer
14088 });
14089
14090 let editor = workspace
14091 .update(cx, |_, window, cx| {
14092 cx.new(|cx| {
14093 Editor::new(
14094 EditorMode::Full {
14095 scale_ui_elements_with_buffer_font_size: false,
14096 show_active_line_background: false,
14097 sized_by_content: false,
14098 },
14099 multi_buffer.clone(),
14100 Some(project.clone()),
14101 window,
14102 cx,
14103 )
14104 })
14105 })
14106 .unwrap();
14107
14108 let pane = workspace
14109 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14110 .unwrap();
14111 pane.update_in(cx, |pane, window, cx| {
14112 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14113 });
14114
14115 let fake_server = fake_servers.next().await.unwrap();
14116
14117 editor.update_in(cx, |editor, window, cx| {
14118 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14119 s.select_ranges([
14120 Point::new(1, 11)..Point::new(1, 11),
14121 Point::new(7, 11)..Point::new(7, 11),
14122 ])
14123 });
14124
14125 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14126 });
14127
14128 editor.update_in(cx, |editor, window, cx| {
14129 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14130 });
14131
14132 fake_server
14133 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14134 let completion_item = lsp::CompletionItem {
14135 label: "saturating_sub()".into(),
14136 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14137 lsp::InsertReplaceEdit {
14138 new_text: "saturating_sub()".to_owned(),
14139 insert: lsp::Range::new(
14140 lsp::Position::new(7, 7),
14141 lsp::Position::new(7, 11),
14142 ),
14143 replace: lsp::Range::new(
14144 lsp::Position::new(7, 7),
14145 lsp::Position::new(7, 13),
14146 ),
14147 },
14148 )),
14149 ..lsp::CompletionItem::default()
14150 };
14151
14152 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14153 })
14154 .next()
14155 .await
14156 .unwrap();
14157
14158 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14159 .await;
14160
14161 editor
14162 .update_in(cx, |editor, window, cx| {
14163 editor
14164 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14165 .unwrap()
14166 })
14167 .await
14168 .unwrap();
14169
14170 editor.update(cx, |editor, cx| {
14171 assert_text_with_selections(editor, expected_multibuffer, cx);
14172 })
14173}
14174
14175#[gpui::test]
14176async fn test_completion(cx: &mut TestAppContext) {
14177 init_test(cx, |_| {});
14178
14179 let mut cx = EditorLspTestContext::new_rust(
14180 lsp::ServerCapabilities {
14181 completion_provider: Some(lsp::CompletionOptions {
14182 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14183 resolve_provider: Some(true),
14184 ..Default::default()
14185 }),
14186 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14187 ..Default::default()
14188 },
14189 cx,
14190 )
14191 .await;
14192 let counter = Arc::new(AtomicUsize::new(0));
14193
14194 cx.set_state(indoc! {"
14195 oneˇ
14196 two
14197 three
14198 "});
14199 cx.simulate_keystroke(".");
14200 handle_completion_request(
14201 indoc! {"
14202 one.|<>
14203 two
14204 three
14205 "},
14206 vec!["first_completion", "second_completion"],
14207 true,
14208 counter.clone(),
14209 &mut cx,
14210 )
14211 .await;
14212 cx.condition(|editor, _| editor.context_menu_visible())
14213 .await;
14214 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14215
14216 let _handler = handle_signature_help_request(
14217 &mut cx,
14218 lsp::SignatureHelp {
14219 signatures: vec![lsp::SignatureInformation {
14220 label: "test signature".to_string(),
14221 documentation: None,
14222 parameters: Some(vec![lsp::ParameterInformation {
14223 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14224 documentation: None,
14225 }]),
14226 active_parameter: None,
14227 }],
14228 active_signature: None,
14229 active_parameter: None,
14230 },
14231 );
14232 cx.update_editor(|editor, window, cx| {
14233 assert!(
14234 !editor.signature_help_state.is_shown(),
14235 "No signature help was called for"
14236 );
14237 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14238 });
14239 cx.run_until_parked();
14240 cx.update_editor(|editor, _, _| {
14241 assert!(
14242 !editor.signature_help_state.is_shown(),
14243 "No signature help should be shown when completions menu is open"
14244 );
14245 });
14246
14247 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14248 editor.context_menu_next(&Default::default(), window, cx);
14249 editor
14250 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14251 .unwrap()
14252 });
14253 cx.assert_editor_state(indoc! {"
14254 one.second_completionˇ
14255 two
14256 three
14257 "});
14258
14259 handle_resolve_completion_request(
14260 &mut cx,
14261 Some(vec![
14262 (
14263 //This overlaps with the primary completion edit which is
14264 //misbehavior from the LSP spec, test that we filter it out
14265 indoc! {"
14266 one.second_ˇcompletion
14267 two
14268 threeˇ
14269 "},
14270 "overlapping additional edit",
14271 ),
14272 (
14273 indoc! {"
14274 one.second_completion
14275 two
14276 threeˇ
14277 "},
14278 "\nadditional edit",
14279 ),
14280 ]),
14281 )
14282 .await;
14283 apply_additional_edits.await.unwrap();
14284 cx.assert_editor_state(indoc! {"
14285 one.second_completionˇ
14286 two
14287 three
14288 additional edit
14289 "});
14290
14291 cx.set_state(indoc! {"
14292 one.second_completion
14293 twoˇ
14294 threeˇ
14295 additional edit
14296 "});
14297 cx.simulate_keystroke(" ");
14298 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14299 cx.simulate_keystroke("s");
14300 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14301
14302 cx.assert_editor_state(indoc! {"
14303 one.second_completion
14304 two sˇ
14305 three sˇ
14306 additional edit
14307 "});
14308 handle_completion_request(
14309 indoc! {"
14310 one.second_completion
14311 two s
14312 three <s|>
14313 additional edit
14314 "},
14315 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14316 true,
14317 counter.clone(),
14318 &mut cx,
14319 )
14320 .await;
14321 cx.condition(|editor, _| editor.context_menu_visible())
14322 .await;
14323 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14324
14325 cx.simulate_keystroke("i");
14326
14327 handle_completion_request(
14328 indoc! {"
14329 one.second_completion
14330 two si
14331 three <si|>
14332 additional edit
14333 "},
14334 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14335 true,
14336 counter.clone(),
14337 &mut cx,
14338 )
14339 .await;
14340 cx.condition(|editor, _| editor.context_menu_visible())
14341 .await;
14342 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14343
14344 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14345 editor
14346 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14347 .unwrap()
14348 });
14349 cx.assert_editor_state(indoc! {"
14350 one.second_completion
14351 two sixth_completionˇ
14352 three sixth_completionˇ
14353 additional edit
14354 "});
14355
14356 apply_additional_edits.await.unwrap();
14357
14358 update_test_language_settings(&mut cx, |settings| {
14359 settings.defaults.show_completions_on_input = Some(false);
14360 });
14361 cx.set_state("editorˇ");
14362 cx.simulate_keystroke(".");
14363 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14364 cx.simulate_keystrokes("c l o");
14365 cx.assert_editor_state("editor.cloˇ");
14366 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14367 cx.update_editor(|editor, window, cx| {
14368 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14369 });
14370 handle_completion_request(
14371 "editor.<clo|>",
14372 vec!["close", "clobber"],
14373 true,
14374 counter.clone(),
14375 &mut cx,
14376 )
14377 .await;
14378 cx.condition(|editor, _| editor.context_menu_visible())
14379 .await;
14380 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14381
14382 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14383 editor
14384 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14385 .unwrap()
14386 });
14387 cx.assert_editor_state("editor.clobberˇ");
14388 handle_resolve_completion_request(&mut cx, None).await;
14389 apply_additional_edits.await.unwrap();
14390}
14391
14392#[gpui::test]
14393async fn test_completion_reuse(cx: &mut TestAppContext) {
14394 init_test(cx, |_| {});
14395
14396 let mut cx = EditorLspTestContext::new_rust(
14397 lsp::ServerCapabilities {
14398 completion_provider: Some(lsp::CompletionOptions {
14399 trigger_characters: Some(vec![".".to_string()]),
14400 ..Default::default()
14401 }),
14402 ..Default::default()
14403 },
14404 cx,
14405 )
14406 .await;
14407
14408 let counter = Arc::new(AtomicUsize::new(0));
14409 cx.set_state("objˇ");
14410 cx.simulate_keystroke(".");
14411
14412 // Initial completion request returns complete results
14413 let is_incomplete = false;
14414 handle_completion_request(
14415 "obj.|<>",
14416 vec!["a", "ab", "abc"],
14417 is_incomplete,
14418 counter.clone(),
14419 &mut cx,
14420 )
14421 .await;
14422 cx.run_until_parked();
14423 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14424 cx.assert_editor_state("obj.ˇ");
14425 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14426
14427 // Type "a" - filters existing completions
14428 cx.simulate_keystroke("a");
14429 cx.run_until_parked();
14430 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14431 cx.assert_editor_state("obj.aˇ");
14432 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14433
14434 // Type "b" - filters existing completions
14435 cx.simulate_keystroke("b");
14436 cx.run_until_parked();
14437 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14438 cx.assert_editor_state("obj.abˇ");
14439 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14440
14441 // Type "c" - filters existing completions
14442 cx.simulate_keystroke("c");
14443 cx.run_until_parked();
14444 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14445 cx.assert_editor_state("obj.abcˇ");
14446 check_displayed_completions(vec!["abc"], &mut cx);
14447
14448 // Backspace to delete "c" - filters existing completions
14449 cx.update_editor(|editor, window, cx| {
14450 editor.backspace(&Backspace, window, cx);
14451 });
14452 cx.run_until_parked();
14453 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14454 cx.assert_editor_state("obj.abˇ");
14455 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14456
14457 // Moving cursor to the left dismisses menu.
14458 cx.update_editor(|editor, window, cx| {
14459 editor.move_left(&MoveLeft, window, cx);
14460 });
14461 cx.run_until_parked();
14462 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14463 cx.assert_editor_state("obj.aˇb");
14464 cx.update_editor(|editor, _, _| {
14465 assert_eq!(editor.context_menu_visible(), false);
14466 });
14467
14468 // Type "b" - new request
14469 cx.simulate_keystroke("b");
14470 let is_incomplete = false;
14471 handle_completion_request(
14472 "obj.<ab|>a",
14473 vec!["ab", "abc"],
14474 is_incomplete,
14475 counter.clone(),
14476 &mut cx,
14477 )
14478 .await;
14479 cx.run_until_parked();
14480 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14481 cx.assert_editor_state("obj.abˇb");
14482 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14483
14484 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14485 cx.update_editor(|editor, window, cx| {
14486 editor.backspace(&Backspace, window, cx);
14487 });
14488 let is_incomplete = false;
14489 handle_completion_request(
14490 "obj.<a|>b",
14491 vec!["a", "ab", "abc"],
14492 is_incomplete,
14493 counter.clone(),
14494 &mut cx,
14495 )
14496 .await;
14497 cx.run_until_parked();
14498 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14499 cx.assert_editor_state("obj.aˇb");
14500 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14501
14502 // Backspace to delete "a" - dismisses menu.
14503 cx.update_editor(|editor, window, cx| {
14504 editor.backspace(&Backspace, window, cx);
14505 });
14506 cx.run_until_parked();
14507 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14508 cx.assert_editor_state("obj.ˇb");
14509 cx.update_editor(|editor, _, _| {
14510 assert_eq!(editor.context_menu_visible(), false);
14511 });
14512}
14513
14514#[gpui::test]
14515async fn test_word_completion(cx: &mut TestAppContext) {
14516 let lsp_fetch_timeout_ms = 10;
14517 init_test(cx, |language_settings| {
14518 language_settings.defaults.completions = Some(CompletionSettingsContent {
14519 words_min_length: Some(0),
14520 lsp_fetch_timeout_ms: Some(10),
14521 lsp_insert_mode: Some(LspInsertMode::Insert),
14522 ..Default::default()
14523 });
14524 });
14525
14526 let mut cx = EditorLspTestContext::new_rust(
14527 lsp::ServerCapabilities {
14528 completion_provider: Some(lsp::CompletionOptions {
14529 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14530 ..lsp::CompletionOptions::default()
14531 }),
14532 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14533 ..lsp::ServerCapabilities::default()
14534 },
14535 cx,
14536 )
14537 .await;
14538
14539 let throttle_completions = Arc::new(AtomicBool::new(false));
14540
14541 let lsp_throttle_completions = throttle_completions.clone();
14542 let _completion_requests_handler =
14543 cx.lsp
14544 .server
14545 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14546 let lsp_throttle_completions = lsp_throttle_completions.clone();
14547 let cx = cx.clone();
14548 async move {
14549 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14550 cx.background_executor()
14551 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14552 .await;
14553 }
14554 Ok(Some(lsp::CompletionResponse::Array(vec![
14555 lsp::CompletionItem {
14556 label: "first".into(),
14557 ..lsp::CompletionItem::default()
14558 },
14559 lsp::CompletionItem {
14560 label: "last".into(),
14561 ..lsp::CompletionItem::default()
14562 },
14563 ])))
14564 }
14565 });
14566
14567 cx.set_state(indoc! {"
14568 oneˇ
14569 two
14570 three
14571 "});
14572 cx.simulate_keystroke(".");
14573 cx.executor().run_until_parked();
14574 cx.condition(|editor, _| editor.context_menu_visible())
14575 .await;
14576 cx.update_editor(|editor, window, cx| {
14577 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14578 {
14579 assert_eq!(
14580 completion_menu_entries(menu),
14581 &["first", "last"],
14582 "When LSP server is fast to reply, no fallback word completions are used"
14583 );
14584 } else {
14585 panic!("expected completion menu to be open");
14586 }
14587 editor.cancel(&Cancel, window, cx);
14588 });
14589 cx.executor().run_until_parked();
14590 cx.condition(|editor, _| !editor.context_menu_visible())
14591 .await;
14592
14593 throttle_completions.store(true, atomic::Ordering::Release);
14594 cx.simulate_keystroke(".");
14595 cx.executor()
14596 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14597 cx.executor().run_until_parked();
14598 cx.condition(|editor, _| editor.context_menu_visible())
14599 .await;
14600 cx.update_editor(|editor, _, _| {
14601 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14602 {
14603 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14604 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14605 } else {
14606 panic!("expected completion menu to be open");
14607 }
14608 });
14609}
14610
14611#[gpui::test]
14612async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14613 init_test(cx, |language_settings| {
14614 language_settings.defaults.completions = Some(CompletionSettingsContent {
14615 words: Some(WordsCompletionMode::Enabled),
14616 words_min_length: Some(0),
14617 lsp_insert_mode: Some(LspInsertMode::Insert),
14618 ..Default::default()
14619 });
14620 });
14621
14622 let mut cx = EditorLspTestContext::new_rust(
14623 lsp::ServerCapabilities {
14624 completion_provider: Some(lsp::CompletionOptions {
14625 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14626 ..lsp::CompletionOptions::default()
14627 }),
14628 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14629 ..lsp::ServerCapabilities::default()
14630 },
14631 cx,
14632 )
14633 .await;
14634
14635 let _completion_requests_handler =
14636 cx.lsp
14637 .server
14638 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14639 Ok(Some(lsp::CompletionResponse::Array(vec![
14640 lsp::CompletionItem {
14641 label: "first".into(),
14642 ..lsp::CompletionItem::default()
14643 },
14644 lsp::CompletionItem {
14645 label: "last".into(),
14646 ..lsp::CompletionItem::default()
14647 },
14648 ])))
14649 });
14650
14651 cx.set_state(indoc! {"ˇ
14652 first
14653 last
14654 second
14655 "});
14656 cx.simulate_keystroke(".");
14657 cx.executor().run_until_parked();
14658 cx.condition(|editor, _| editor.context_menu_visible())
14659 .await;
14660 cx.update_editor(|editor, _, _| {
14661 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14662 {
14663 assert_eq!(
14664 completion_menu_entries(menu),
14665 &["first", "last", "second"],
14666 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14667 );
14668 } else {
14669 panic!("expected completion menu to be open");
14670 }
14671 });
14672}
14673
14674#[gpui::test]
14675async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14676 init_test(cx, |language_settings| {
14677 language_settings.defaults.completions = Some(CompletionSettingsContent {
14678 words: Some(WordsCompletionMode::Disabled),
14679 words_min_length: Some(0),
14680 lsp_insert_mode: Some(LspInsertMode::Insert),
14681 ..Default::default()
14682 });
14683 });
14684
14685 let mut cx = EditorLspTestContext::new_rust(
14686 lsp::ServerCapabilities {
14687 completion_provider: Some(lsp::CompletionOptions {
14688 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14689 ..lsp::CompletionOptions::default()
14690 }),
14691 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14692 ..lsp::ServerCapabilities::default()
14693 },
14694 cx,
14695 )
14696 .await;
14697
14698 let _completion_requests_handler =
14699 cx.lsp
14700 .server
14701 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14702 panic!("LSP completions should not be queried when dealing with word completions")
14703 });
14704
14705 cx.set_state(indoc! {"ˇ
14706 first
14707 last
14708 second
14709 "});
14710 cx.update_editor(|editor, window, cx| {
14711 editor.show_word_completions(&ShowWordCompletions, window, cx);
14712 });
14713 cx.executor().run_until_parked();
14714 cx.condition(|editor, _| editor.context_menu_visible())
14715 .await;
14716 cx.update_editor(|editor, _, _| {
14717 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14718 {
14719 assert_eq!(
14720 completion_menu_entries(menu),
14721 &["first", "last", "second"],
14722 "`ShowWordCompletions` action should show word completions"
14723 );
14724 } else {
14725 panic!("expected completion menu to be open");
14726 }
14727 });
14728
14729 cx.simulate_keystroke("l");
14730 cx.executor().run_until_parked();
14731 cx.condition(|editor, _| editor.context_menu_visible())
14732 .await;
14733 cx.update_editor(|editor, _, _| {
14734 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14735 {
14736 assert_eq!(
14737 completion_menu_entries(menu),
14738 &["last"],
14739 "After showing word completions, further editing should filter them and not query the LSP"
14740 );
14741 } else {
14742 panic!("expected completion menu to be open");
14743 }
14744 });
14745}
14746
14747#[gpui::test]
14748async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14749 init_test(cx, |language_settings| {
14750 language_settings.defaults.completions = Some(CompletionSettingsContent {
14751 words_min_length: Some(0),
14752 lsp: Some(false),
14753 lsp_insert_mode: Some(LspInsertMode::Insert),
14754 ..Default::default()
14755 });
14756 });
14757
14758 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14759
14760 cx.set_state(indoc! {"ˇ
14761 0_usize
14762 let
14763 33
14764 4.5f32
14765 "});
14766 cx.update_editor(|editor, window, cx| {
14767 editor.show_completions(&ShowCompletions::default(), window, cx);
14768 });
14769 cx.executor().run_until_parked();
14770 cx.condition(|editor, _| editor.context_menu_visible())
14771 .await;
14772 cx.update_editor(|editor, window, cx| {
14773 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14774 {
14775 assert_eq!(
14776 completion_menu_entries(menu),
14777 &["let"],
14778 "With no digits in the completion query, no digits should be in the word completions"
14779 );
14780 } else {
14781 panic!("expected completion menu to be open");
14782 }
14783 editor.cancel(&Cancel, window, cx);
14784 });
14785
14786 cx.set_state(indoc! {"3ˇ
14787 0_usize
14788 let
14789 3
14790 33.35f32
14791 "});
14792 cx.update_editor(|editor, window, cx| {
14793 editor.show_completions(&ShowCompletions::default(), window, cx);
14794 });
14795 cx.executor().run_until_parked();
14796 cx.condition(|editor, _| editor.context_menu_visible())
14797 .await;
14798 cx.update_editor(|editor, _, _| {
14799 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14800 {
14801 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14802 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14803 } else {
14804 panic!("expected completion menu to be open");
14805 }
14806 });
14807}
14808
14809#[gpui::test]
14810async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14811 init_test(cx, |language_settings| {
14812 language_settings.defaults.completions = Some(CompletionSettingsContent {
14813 words: Some(WordsCompletionMode::Enabled),
14814 words_min_length: Some(3),
14815 lsp_insert_mode: Some(LspInsertMode::Insert),
14816 ..Default::default()
14817 });
14818 });
14819
14820 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14821 cx.set_state(indoc! {"ˇ
14822 wow
14823 wowen
14824 wowser
14825 "});
14826 cx.simulate_keystroke("w");
14827 cx.executor().run_until_parked();
14828 cx.update_editor(|editor, _, _| {
14829 if editor.context_menu.borrow_mut().is_some() {
14830 panic!(
14831 "expected completion menu to be hidden, as words completion threshold is not met"
14832 );
14833 }
14834 });
14835
14836 cx.update_editor(|editor, window, cx| {
14837 editor.show_word_completions(&ShowWordCompletions, window, cx);
14838 });
14839 cx.executor().run_until_parked();
14840 cx.update_editor(|editor, window, cx| {
14841 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14842 {
14843 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");
14844 } else {
14845 panic!("expected completion menu to be open after the word completions are called with an action");
14846 }
14847
14848 editor.cancel(&Cancel, window, cx);
14849 });
14850 cx.update_editor(|editor, _, _| {
14851 if editor.context_menu.borrow_mut().is_some() {
14852 panic!("expected completion menu to be hidden after canceling");
14853 }
14854 });
14855
14856 cx.simulate_keystroke("o");
14857 cx.executor().run_until_parked();
14858 cx.update_editor(|editor, _, _| {
14859 if editor.context_menu.borrow_mut().is_some() {
14860 panic!(
14861 "expected completion menu to be hidden, as words completion threshold is not met still"
14862 );
14863 }
14864 });
14865
14866 cx.simulate_keystroke("w");
14867 cx.executor().run_until_parked();
14868 cx.update_editor(|editor, _, _| {
14869 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14870 {
14871 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14872 } else {
14873 panic!("expected completion menu to be open after the word completions threshold is met");
14874 }
14875 });
14876}
14877
14878#[gpui::test]
14879async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14880 init_test(cx, |language_settings| {
14881 language_settings.defaults.completions = Some(CompletionSettingsContent {
14882 words: Some(WordsCompletionMode::Enabled),
14883 words_min_length: Some(0),
14884 lsp_insert_mode: Some(LspInsertMode::Insert),
14885 ..Default::default()
14886 });
14887 });
14888
14889 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14890 cx.update_editor(|editor, _, _| {
14891 editor.disable_word_completions();
14892 });
14893 cx.set_state(indoc! {"ˇ
14894 wow
14895 wowen
14896 wowser
14897 "});
14898 cx.simulate_keystroke("w");
14899 cx.executor().run_until_parked();
14900 cx.update_editor(|editor, _, _| {
14901 if editor.context_menu.borrow_mut().is_some() {
14902 panic!(
14903 "expected completion menu to be hidden, as words completion are disabled for this editor"
14904 );
14905 }
14906 });
14907
14908 cx.update_editor(|editor, window, cx| {
14909 editor.show_word_completions(&ShowWordCompletions, window, cx);
14910 });
14911 cx.executor().run_until_parked();
14912 cx.update_editor(|editor, _, _| {
14913 if editor.context_menu.borrow_mut().is_some() {
14914 panic!(
14915 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14916 );
14917 }
14918 });
14919}
14920
14921fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14922 let position = || lsp::Position {
14923 line: params.text_document_position.position.line,
14924 character: params.text_document_position.position.character,
14925 };
14926 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14927 range: lsp::Range {
14928 start: position(),
14929 end: position(),
14930 },
14931 new_text: text.to_string(),
14932 }))
14933}
14934
14935#[gpui::test]
14936async fn test_multiline_completion(cx: &mut TestAppContext) {
14937 init_test(cx, |_| {});
14938
14939 let fs = FakeFs::new(cx.executor());
14940 fs.insert_tree(
14941 path!("/a"),
14942 json!({
14943 "main.ts": "a",
14944 }),
14945 )
14946 .await;
14947
14948 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14949 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14950 let typescript_language = Arc::new(Language::new(
14951 LanguageConfig {
14952 name: "TypeScript".into(),
14953 matcher: LanguageMatcher {
14954 path_suffixes: vec!["ts".to_string()],
14955 ..LanguageMatcher::default()
14956 },
14957 line_comments: vec!["// ".into()],
14958 ..LanguageConfig::default()
14959 },
14960 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14961 ));
14962 language_registry.add(typescript_language.clone());
14963 let mut fake_servers = language_registry.register_fake_lsp(
14964 "TypeScript",
14965 FakeLspAdapter {
14966 capabilities: lsp::ServerCapabilities {
14967 completion_provider: Some(lsp::CompletionOptions {
14968 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14969 ..lsp::CompletionOptions::default()
14970 }),
14971 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14972 ..lsp::ServerCapabilities::default()
14973 },
14974 // Emulate vtsls label generation
14975 label_for_completion: Some(Box::new(|item, _| {
14976 let text = if let Some(description) = item
14977 .label_details
14978 .as_ref()
14979 .and_then(|label_details| label_details.description.as_ref())
14980 {
14981 format!("{} {}", item.label, description)
14982 } else if let Some(detail) = &item.detail {
14983 format!("{} {}", item.label, detail)
14984 } else {
14985 item.label.clone()
14986 };
14987 Some(language::CodeLabel::plain(text, None))
14988 })),
14989 ..FakeLspAdapter::default()
14990 },
14991 );
14992 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14993 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14994 let worktree_id = workspace
14995 .update(cx, |workspace, _window, cx| {
14996 workspace.project().update(cx, |project, cx| {
14997 project.worktrees(cx).next().unwrap().read(cx).id()
14998 })
14999 })
15000 .unwrap();
15001 let _buffer = project
15002 .update(cx, |project, cx| {
15003 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15004 })
15005 .await
15006 .unwrap();
15007 let editor = workspace
15008 .update(cx, |workspace, window, cx| {
15009 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15010 })
15011 .unwrap()
15012 .await
15013 .unwrap()
15014 .downcast::<Editor>()
15015 .unwrap();
15016 let fake_server = fake_servers.next().await.unwrap();
15017
15018 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15019 let multiline_label_2 = "a\nb\nc\n";
15020 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15021 let multiline_description = "d\ne\nf\n";
15022 let multiline_detail_2 = "g\nh\ni\n";
15023
15024 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15025 move |params, _| async move {
15026 Ok(Some(lsp::CompletionResponse::Array(vec![
15027 lsp::CompletionItem {
15028 label: multiline_label.to_string(),
15029 text_edit: gen_text_edit(¶ms, "new_text_1"),
15030 ..lsp::CompletionItem::default()
15031 },
15032 lsp::CompletionItem {
15033 label: "single line label 1".to_string(),
15034 detail: Some(multiline_detail.to_string()),
15035 text_edit: gen_text_edit(¶ms, "new_text_2"),
15036 ..lsp::CompletionItem::default()
15037 },
15038 lsp::CompletionItem {
15039 label: "single line label 2".to_string(),
15040 label_details: Some(lsp::CompletionItemLabelDetails {
15041 description: Some(multiline_description.to_string()),
15042 detail: None,
15043 }),
15044 text_edit: gen_text_edit(¶ms, "new_text_2"),
15045 ..lsp::CompletionItem::default()
15046 },
15047 lsp::CompletionItem {
15048 label: multiline_label_2.to_string(),
15049 detail: Some(multiline_detail_2.to_string()),
15050 text_edit: gen_text_edit(¶ms, "new_text_3"),
15051 ..lsp::CompletionItem::default()
15052 },
15053 lsp::CompletionItem {
15054 label: "Label with many spaces and \t but without newlines".to_string(),
15055 detail: Some(
15056 "Details with many spaces and \t but without newlines".to_string(),
15057 ),
15058 text_edit: gen_text_edit(¶ms, "new_text_4"),
15059 ..lsp::CompletionItem::default()
15060 },
15061 ])))
15062 },
15063 );
15064
15065 editor.update_in(cx, |editor, window, cx| {
15066 cx.focus_self(window);
15067 editor.move_to_end(&MoveToEnd, window, cx);
15068 editor.handle_input(".", window, cx);
15069 });
15070 cx.run_until_parked();
15071 completion_handle.next().await.unwrap();
15072
15073 editor.update(cx, |editor, _| {
15074 assert!(editor.context_menu_visible());
15075 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15076 {
15077 let completion_labels = menu
15078 .completions
15079 .borrow()
15080 .iter()
15081 .map(|c| c.label.text.clone())
15082 .collect::<Vec<_>>();
15083 assert_eq!(
15084 completion_labels,
15085 &[
15086 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15087 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15088 "single line label 2 d e f ",
15089 "a b c g h i ",
15090 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15091 ],
15092 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15093 );
15094
15095 for completion in menu
15096 .completions
15097 .borrow()
15098 .iter() {
15099 assert_eq!(
15100 completion.label.filter_range,
15101 0..completion.label.text.len(),
15102 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15103 );
15104 }
15105 } else {
15106 panic!("expected completion menu to be open");
15107 }
15108 });
15109}
15110
15111#[gpui::test]
15112async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15113 init_test(cx, |_| {});
15114 let mut cx = EditorLspTestContext::new_rust(
15115 lsp::ServerCapabilities {
15116 completion_provider: Some(lsp::CompletionOptions {
15117 trigger_characters: Some(vec![".".to_string()]),
15118 ..Default::default()
15119 }),
15120 ..Default::default()
15121 },
15122 cx,
15123 )
15124 .await;
15125 cx.lsp
15126 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15127 Ok(Some(lsp::CompletionResponse::Array(vec![
15128 lsp::CompletionItem {
15129 label: "first".into(),
15130 ..Default::default()
15131 },
15132 lsp::CompletionItem {
15133 label: "last".into(),
15134 ..Default::default()
15135 },
15136 ])))
15137 });
15138 cx.set_state("variableˇ");
15139 cx.simulate_keystroke(".");
15140 cx.executor().run_until_parked();
15141
15142 cx.update_editor(|editor, _, _| {
15143 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15144 {
15145 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15146 } else {
15147 panic!("expected completion menu to be open");
15148 }
15149 });
15150
15151 cx.update_editor(|editor, window, cx| {
15152 editor.move_page_down(&MovePageDown::default(), window, cx);
15153 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15154 {
15155 assert!(
15156 menu.selected_item == 1,
15157 "expected PageDown to select the last item from the context menu"
15158 );
15159 } else {
15160 panic!("expected completion menu to stay open after PageDown");
15161 }
15162 });
15163
15164 cx.update_editor(|editor, window, cx| {
15165 editor.move_page_up(&MovePageUp::default(), window, cx);
15166 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15167 {
15168 assert!(
15169 menu.selected_item == 0,
15170 "expected PageUp to select the first item from the context menu"
15171 );
15172 } else {
15173 panic!("expected completion menu to stay open after PageUp");
15174 }
15175 });
15176}
15177
15178#[gpui::test]
15179async fn test_as_is_completions(cx: &mut TestAppContext) {
15180 init_test(cx, |_| {});
15181 let mut cx = EditorLspTestContext::new_rust(
15182 lsp::ServerCapabilities {
15183 completion_provider: Some(lsp::CompletionOptions {
15184 ..Default::default()
15185 }),
15186 ..Default::default()
15187 },
15188 cx,
15189 )
15190 .await;
15191 cx.lsp
15192 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15193 Ok(Some(lsp::CompletionResponse::Array(vec![
15194 lsp::CompletionItem {
15195 label: "unsafe".into(),
15196 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15197 range: lsp::Range {
15198 start: lsp::Position {
15199 line: 1,
15200 character: 2,
15201 },
15202 end: lsp::Position {
15203 line: 1,
15204 character: 3,
15205 },
15206 },
15207 new_text: "unsafe".to_string(),
15208 })),
15209 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15210 ..Default::default()
15211 },
15212 ])))
15213 });
15214 cx.set_state("fn a() {}\n nˇ");
15215 cx.executor().run_until_parked();
15216 cx.update_editor(|editor, window, cx| {
15217 editor.show_completions(
15218 &ShowCompletions {
15219 trigger: Some("\n".into()),
15220 },
15221 window,
15222 cx,
15223 );
15224 });
15225 cx.executor().run_until_parked();
15226
15227 cx.update_editor(|editor, window, cx| {
15228 editor.confirm_completion(&Default::default(), window, cx)
15229 });
15230 cx.executor().run_until_parked();
15231 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15232}
15233
15234#[gpui::test]
15235async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15236 init_test(cx, |_| {});
15237 let language =
15238 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15239 let mut cx = EditorLspTestContext::new(
15240 language,
15241 lsp::ServerCapabilities {
15242 completion_provider: Some(lsp::CompletionOptions {
15243 ..lsp::CompletionOptions::default()
15244 }),
15245 ..lsp::ServerCapabilities::default()
15246 },
15247 cx,
15248 )
15249 .await;
15250
15251 cx.set_state(
15252 "#ifndef BAR_H
15253#define BAR_H
15254
15255#include <stdbool.h>
15256
15257int fn_branch(bool do_branch1, bool do_branch2);
15258
15259#endif // BAR_H
15260ˇ",
15261 );
15262 cx.executor().run_until_parked();
15263 cx.update_editor(|editor, window, cx| {
15264 editor.handle_input("#", window, cx);
15265 });
15266 cx.executor().run_until_parked();
15267 cx.update_editor(|editor, window, cx| {
15268 editor.handle_input("i", window, cx);
15269 });
15270 cx.executor().run_until_parked();
15271 cx.update_editor(|editor, window, cx| {
15272 editor.handle_input("n", window, cx);
15273 });
15274 cx.executor().run_until_parked();
15275 cx.assert_editor_state(
15276 "#ifndef BAR_H
15277#define BAR_H
15278
15279#include <stdbool.h>
15280
15281int fn_branch(bool do_branch1, bool do_branch2);
15282
15283#endif // BAR_H
15284#inˇ",
15285 );
15286
15287 cx.lsp
15288 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15289 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15290 is_incomplete: false,
15291 item_defaults: None,
15292 items: vec![lsp::CompletionItem {
15293 kind: Some(lsp::CompletionItemKind::SNIPPET),
15294 label_details: Some(lsp::CompletionItemLabelDetails {
15295 detail: Some("header".to_string()),
15296 description: None,
15297 }),
15298 label: " include".to_string(),
15299 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15300 range: lsp::Range {
15301 start: lsp::Position {
15302 line: 8,
15303 character: 1,
15304 },
15305 end: lsp::Position {
15306 line: 8,
15307 character: 1,
15308 },
15309 },
15310 new_text: "include \"$0\"".to_string(),
15311 })),
15312 sort_text: Some("40b67681include".to_string()),
15313 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15314 filter_text: Some("include".to_string()),
15315 insert_text: Some("include \"$0\"".to_string()),
15316 ..lsp::CompletionItem::default()
15317 }],
15318 })))
15319 });
15320 cx.update_editor(|editor, window, cx| {
15321 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15322 });
15323 cx.executor().run_until_parked();
15324 cx.update_editor(|editor, window, cx| {
15325 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15326 });
15327 cx.executor().run_until_parked();
15328 cx.assert_editor_state(
15329 "#ifndef BAR_H
15330#define BAR_H
15331
15332#include <stdbool.h>
15333
15334int fn_branch(bool do_branch1, bool do_branch2);
15335
15336#endif // BAR_H
15337#include \"ˇ\"",
15338 );
15339
15340 cx.lsp
15341 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15342 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15343 is_incomplete: true,
15344 item_defaults: None,
15345 items: vec![lsp::CompletionItem {
15346 kind: Some(lsp::CompletionItemKind::FILE),
15347 label: "AGL/".to_string(),
15348 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15349 range: lsp::Range {
15350 start: lsp::Position {
15351 line: 8,
15352 character: 10,
15353 },
15354 end: lsp::Position {
15355 line: 8,
15356 character: 11,
15357 },
15358 },
15359 new_text: "AGL/".to_string(),
15360 })),
15361 sort_text: Some("40b67681AGL/".to_string()),
15362 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15363 filter_text: Some("AGL/".to_string()),
15364 insert_text: Some("AGL/".to_string()),
15365 ..lsp::CompletionItem::default()
15366 }],
15367 })))
15368 });
15369 cx.update_editor(|editor, window, cx| {
15370 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15371 });
15372 cx.executor().run_until_parked();
15373 cx.update_editor(|editor, window, cx| {
15374 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15375 });
15376 cx.executor().run_until_parked();
15377 cx.assert_editor_state(
15378 r##"#ifndef BAR_H
15379#define BAR_H
15380
15381#include <stdbool.h>
15382
15383int fn_branch(bool do_branch1, bool do_branch2);
15384
15385#endif // BAR_H
15386#include "AGL/ˇ"##,
15387 );
15388
15389 cx.update_editor(|editor, window, cx| {
15390 editor.handle_input("\"", window, cx);
15391 });
15392 cx.executor().run_until_parked();
15393 cx.assert_editor_state(
15394 r##"#ifndef BAR_H
15395#define BAR_H
15396
15397#include <stdbool.h>
15398
15399int fn_branch(bool do_branch1, bool do_branch2);
15400
15401#endif // BAR_H
15402#include "AGL/"ˇ"##,
15403 );
15404}
15405
15406#[gpui::test]
15407async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15408 init_test(cx, |_| {});
15409
15410 let mut cx = EditorLspTestContext::new_rust(
15411 lsp::ServerCapabilities {
15412 completion_provider: Some(lsp::CompletionOptions {
15413 trigger_characters: Some(vec![".".to_string()]),
15414 resolve_provider: Some(true),
15415 ..Default::default()
15416 }),
15417 ..Default::default()
15418 },
15419 cx,
15420 )
15421 .await;
15422
15423 cx.set_state("fn main() { let a = 2ˇ; }");
15424 cx.simulate_keystroke(".");
15425 let completion_item = lsp::CompletionItem {
15426 label: "Some".into(),
15427 kind: Some(lsp::CompletionItemKind::SNIPPET),
15428 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15429 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15430 kind: lsp::MarkupKind::Markdown,
15431 value: "```rust\nSome(2)\n```".to_string(),
15432 })),
15433 deprecated: Some(false),
15434 sort_text: Some("Some".to_string()),
15435 filter_text: Some("Some".to_string()),
15436 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15437 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15438 range: lsp::Range {
15439 start: lsp::Position {
15440 line: 0,
15441 character: 22,
15442 },
15443 end: lsp::Position {
15444 line: 0,
15445 character: 22,
15446 },
15447 },
15448 new_text: "Some(2)".to_string(),
15449 })),
15450 additional_text_edits: Some(vec![lsp::TextEdit {
15451 range: lsp::Range {
15452 start: lsp::Position {
15453 line: 0,
15454 character: 20,
15455 },
15456 end: lsp::Position {
15457 line: 0,
15458 character: 22,
15459 },
15460 },
15461 new_text: "".to_string(),
15462 }]),
15463 ..Default::default()
15464 };
15465
15466 let closure_completion_item = completion_item.clone();
15467 let counter = Arc::new(AtomicUsize::new(0));
15468 let counter_clone = counter.clone();
15469 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15470 let task_completion_item = closure_completion_item.clone();
15471 counter_clone.fetch_add(1, atomic::Ordering::Release);
15472 async move {
15473 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15474 is_incomplete: true,
15475 item_defaults: None,
15476 items: vec![task_completion_item],
15477 })))
15478 }
15479 });
15480
15481 cx.condition(|editor, _| editor.context_menu_visible())
15482 .await;
15483 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15484 assert!(request.next().await.is_some());
15485 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15486
15487 cx.simulate_keystrokes("S o m");
15488 cx.condition(|editor, _| editor.context_menu_visible())
15489 .await;
15490 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15491 assert!(request.next().await.is_some());
15492 assert!(request.next().await.is_some());
15493 assert!(request.next().await.is_some());
15494 request.close();
15495 assert!(request.next().await.is_none());
15496 assert_eq!(
15497 counter.load(atomic::Ordering::Acquire),
15498 4,
15499 "With the completions menu open, only one LSP request should happen per input"
15500 );
15501}
15502
15503#[gpui::test]
15504async fn test_toggle_comment(cx: &mut TestAppContext) {
15505 init_test(cx, |_| {});
15506 let mut cx = EditorTestContext::new(cx).await;
15507 let language = Arc::new(Language::new(
15508 LanguageConfig {
15509 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15510 ..Default::default()
15511 },
15512 Some(tree_sitter_rust::LANGUAGE.into()),
15513 ));
15514 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15515
15516 // If multiple selections intersect a line, the line is only toggled once.
15517 cx.set_state(indoc! {"
15518 fn a() {
15519 «//b();
15520 ˇ»// «c();
15521 //ˇ» d();
15522 }
15523 "});
15524
15525 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15526
15527 cx.assert_editor_state(indoc! {"
15528 fn a() {
15529 «b();
15530 c();
15531 ˇ» d();
15532 }
15533 "});
15534
15535 // The comment prefix is inserted at the same column for every line in a
15536 // selection.
15537 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15538
15539 cx.assert_editor_state(indoc! {"
15540 fn a() {
15541 // «b();
15542 // c();
15543 ˇ»// d();
15544 }
15545 "});
15546
15547 // If a selection ends at the beginning of a line, that line is not toggled.
15548 cx.set_selections_state(indoc! {"
15549 fn a() {
15550 // b();
15551 «// c();
15552 ˇ» // d();
15553 }
15554 "});
15555
15556 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15557
15558 cx.assert_editor_state(indoc! {"
15559 fn a() {
15560 // b();
15561 «c();
15562 ˇ» // d();
15563 }
15564 "});
15565
15566 // If a selection span a single line and is empty, the line is toggled.
15567 cx.set_state(indoc! {"
15568 fn a() {
15569 a();
15570 b();
15571 ˇ
15572 }
15573 "});
15574
15575 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15576
15577 cx.assert_editor_state(indoc! {"
15578 fn a() {
15579 a();
15580 b();
15581 //•ˇ
15582 }
15583 "});
15584
15585 // If a selection span multiple lines, empty lines are not toggled.
15586 cx.set_state(indoc! {"
15587 fn a() {
15588 «a();
15589
15590 c();ˇ»
15591 }
15592 "});
15593
15594 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15595
15596 cx.assert_editor_state(indoc! {"
15597 fn a() {
15598 // «a();
15599
15600 // c();ˇ»
15601 }
15602 "});
15603
15604 // If a selection includes multiple comment prefixes, all lines are uncommented.
15605 cx.set_state(indoc! {"
15606 fn a() {
15607 «// a();
15608 /// b();
15609 //! c();ˇ»
15610 }
15611 "});
15612
15613 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15614
15615 cx.assert_editor_state(indoc! {"
15616 fn a() {
15617 «a();
15618 b();
15619 c();ˇ»
15620 }
15621 "});
15622}
15623
15624#[gpui::test]
15625async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15626 init_test(cx, |_| {});
15627 let mut cx = EditorTestContext::new(cx).await;
15628 let language = Arc::new(Language::new(
15629 LanguageConfig {
15630 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15631 ..Default::default()
15632 },
15633 Some(tree_sitter_rust::LANGUAGE.into()),
15634 ));
15635 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15636
15637 let toggle_comments = &ToggleComments {
15638 advance_downwards: false,
15639 ignore_indent: true,
15640 };
15641
15642 // If multiple selections intersect a line, the line is only toggled once.
15643 cx.set_state(indoc! {"
15644 fn a() {
15645 // «b();
15646 // c();
15647 // ˇ» d();
15648 }
15649 "});
15650
15651 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15652
15653 cx.assert_editor_state(indoc! {"
15654 fn a() {
15655 «b();
15656 c();
15657 ˇ» d();
15658 }
15659 "});
15660
15661 // The comment prefix is inserted at the beginning of each line
15662 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15663
15664 cx.assert_editor_state(indoc! {"
15665 fn a() {
15666 // «b();
15667 // c();
15668 // ˇ» d();
15669 }
15670 "});
15671
15672 // If a selection ends at the beginning of a line, that line is not toggled.
15673 cx.set_selections_state(indoc! {"
15674 fn a() {
15675 // b();
15676 // «c();
15677 ˇ»// d();
15678 }
15679 "});
15680
15681 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15682
15683 cx.assert_editor_state(indoc! {"
15684 fn a() {
15685 // b();
15686 «c();
15687 ˇ»// d();
15688 }
15689 "});
15690
15691 // If a selection span a single line and is empty, the line is toggled.
15692 cx.set_state(indoc! {"
15693 fn a() {
15694 a();
15695 b();
15696 ˇ
15697 }
15698 "});
15699
15700 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15701
15702 cx.assert_editor_state(indoc! {"
15703 fn a() {
15704 a();
15705 b();
15706 //ˇ
15707 }
15708 "});
15709
15710 // If a selection span multiple lines, empty lines are not toggled.
15711 cx.set_state(indoc! {"
15712 fn a() {
15713 «a();
15714
15715 c();ˇ»
15716 }
15717 "});
15718
15719 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15720
15721 cx.assert_editor_state(indoc! {"
15722 fn a() {
15723 // «a();
15724
15725 // c();ˇ»
15726 }
15727 "});
15728
15729 // If a selection includes multiple comment prefixes, all lines are uncommented.
15730 cx.set_state(indoc! {"
15731 fn a() {
15732 // «a();
15733 /// b();
15734 //! c();ˇ»
15735 }
15736 "});
15737
15738 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15739
15740 cx.assert_editor_state(indoc! {"
15741 fn a() {
15742 «a();
15743 b();
15744 c();ˇ»
15745 }
15746 "});
15747}
15748
15749#[gpui::test]
15750async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15751 init_test(cx, |_| {});
15752
15753 let language = Arc::new(Language::new(
15754 LanguageConfig {
15755 line_comments: vec!["// ".into()],
15756 ..Default::default()
15757 },
15758 Some(tree_sitter_rust::LANGUAGE.into()),
15759 ));
15760
15761 let mut cx = EditorTestContext::new(cx).await;
15762
15763 cx.language_registry().add(language.clone());
15764 cx.update_buffer(|buffer, cx| {
15765 buffer.set_language(Some(language), cx);
15766 });
15767
15768 let toggle_comments = &ToggleComments {
15769 advance_downwards: true,
15770 ignore_indent: false,
15771 };
15772
15773 // Single cursor on one line -> advance
15774 // Cursor moves horizontally 3 characters as well on non-blank line
15775 cx.set_state(indoc!(
15776 "fn a() {
15777 ˇdog();
15778 cat();
15779 }"
15780 ));
15781 cx.update_editor(|editor, window, cx| {
15782 editor.toggle_comments(toggle_comments, window, cx);
15783 });
15784 cx.assert_editor_state(indoc!(
15785 "fn a() {
15786 // dog();
15787 catˇ();
15788 }"
15789 ));
15790
15791 // Single selection on one line -> don't advance
15792 cx.set_state(indoc!(
15793 "fn a() {
15794 «dog()ˇ»;
15795 cat();
15796 }"
15797 ));
15798 cx.update_editor(|editor, window, cx| {
15799 editor.toggle_comments(toggle_comments, window, cx);
15800 });
15801 cx.assert_editor_state(indoc!(
15802 "fn a() {
15803 // «dog()ˇ»;
15804 cat();
15805 }"
15806 ));
15807
15808 // Multiple cursors on one line -> advance
15809 cx.set_state(indoc!(
15810 "fn a() {
15811 ˇdˇog();
15812 cat();
15813 }"
15814 ));
15815 cx.update_editor(|editor, window, cx| {
15816 editor.toggle_comments(toggle_comments, window, cx);
15817 });
15818 cx.assert_editor_state(indoc!(
15819 "fn a() {
15820 // dog();
15821 catˇ(ˇ);
15822 }"
15823 ));
15824
15825 // Multiple cursors on one line, with selection -> don't advance
15826 cx.set_state(indoc!(
15827 "fn a() {
15828 ˇdˇog«()ˇ»;
15829 cat();
15830 }"
15831 ));
15832 cx.update_editor(|editor, window, cx| {
15833 editor.toggle_comments(toggle_comments, window, cx);
15834 });
15835 cx.assert_editor_state(indoc!(
15836 "fn a() {
15837 // ˇdˇog«()ˇ»;
15838 cat();
15839 }"
15840 ));
15841
15842 // Single cursor on one line -> advance
15843 // Cursor moves to column 0 on blank line
15844 cx.set_state(indoc!(
15845 "fn a() {
15846 ˇdog();
15847
15848 cat();
15849 }"
15850 ));
15851 cx.update_editor(|editor, window, cx| {
15852 editor.toggle_comments(toggle_comments, window, cx);
15853 });
15854 cx.assert_editor_state(indoc!(
15855 "fn a() {
15856 // dog();
15857 ˇ
15858 cat();
15859 }"
15860 ));
15861
15862 // Single cursor on one line -> advance
15863 // Cursor starts and ends at column 0
15864 cx.set_state(indoc!(
15865 "fn a() {
15866 ˇ dog();
15867 cat();
15868 }"
15869 ));
15870 cx.update_editor(|editor, window, cx| {
15871 editor.toggle_comments(toggle_comments, window, cx);
15872 });
15873 cx.assert_editor_state(indoc!(
15874 "fn a() {
15875 // dog();
15876 ˇ cat();
15877 }"
15878 ));
15879}
15880
15881#[gpui::test]
15882async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15883 init_test(cx, |_| {});
15884
15885 let mut cx = EditorTestContext::new(cx).await;
15886
15887 let html_language = Arc::new(
15888 Language::new(
15889 LanguageConfig {
15890 name: "HTML".into(),
15891 block_comment: Some(BlockCommentConfig {
15892 start: "<!-- ".into(),
15893 prefix: "".into(),
15894 end: " -->".into(),
15895 tab_size: 0,
15896 }),
15897 ..Default::default()
15898 },
15899 Some(tree_sitter_html::LANGUAGE.into()),
15900 )
15901 .with_injection_query(
15902 r#"
15903 (script_element
15904 (raw_text) @injection.content
15905 (#set! injection.language "javascript"))
15906 "#,
15907 )
15908 .unwrap(),
15909 );
15910
15911 let javascript_language = Arc::new(Language::new(
15912 LanguageConfig {
15913 name: "JavaScript".into(),
15914 line_comments: vec!["// ".into()],
15915 ..Default::default()
15916 },
15917 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15918 ));
15919
15920 cx.language_registry().add(html_language.clone());
15921 cx.language_registry().add(javascript_language);
15922 cx.update_buffer(|buffer, cx| {
15923 buffer.set_language(Some(html_language), cx);
15924 });
15925
15926 // Toggle comments for empty selections
15927 cx.set_state(
15928 &r#"
15929 <p>A</p>ˇ
15930 <p>B</p>ˇ
15931 <p>C</p>ˇ
15932 "#
15933 .unindent(),
15934 );
15935 cx.update_editor(|editor, window, cx| {
15936 editor.toggle_comments(&ToggleComments::default(), window, cx)
15937 });
15938 cx.assert_editor_state(
15939 &r#"
15940 <!-- <p>A</p>ˇ -->
15941 <!-- <p>B</p>ˇ -->
15942 <!-- <p>C</p>ˇ -->
15943 "#
15944 .unindent(),
15945 );
15946 cx.update_editor(|editor, window, cx| {
15947 editor.toggle_comments(&ToggleComments::default(), window, cx)
15948 });
15949 cx.assert_editor_state(
15950 &r#"
15951 <p>A</p>ˇ
15952 <p>B</p>ˇ
15953 <p>C</p>ˇ
15954 "#
15955 .unindent(),
15956 );
15957
15958 // Toggle comments for mixture of empty and non-empty selections, where
15959 // multiple selections occupy a given line.
15960 cx.set_state(
15961 &r#"
15962 <p>A«</p>
15963 <p>ˇ»B</p>ˇ
15964 <p>C«</p>
15965 <p>ˇ»D</p>ˇ
15966 "#
15967 .unindent(),
15968 );
15969
15970 cx.update_editor(|editor, window, cx| {
15971 editor.toggle_comments(&ToggleComments::default(), window, cx)
15972 });
15973 cx.assert_editor_state(
15974 &r#"
15975 <!-- <p>A«</p>
15976 <p>ˇ»B</p>ˇ -->
15977 <!-- <p>C«</p>
15978 <p>ˇ»D</p>ˇ -->
15979 "#
15980 .unindent(),
15981 );
15982 cx.update_editor(|editor, window, cx| {
15983 editor.toggle_comments(&ToggleComments::default(), window, cx)
15984 });
15985 cx.assert_editor_state(
15986 &r#"
15987 <p>A«</p>
15988 <p>ˇ»B</p>ˇ
15989 <p>C«</p>
15990 <p>ˇ»D</p>ˇ
15991 "#
15992 .unindent(),
15993 );
15994
15995 // Toggle comments when different languages are active for different
15996 // selections.
15997 cx.set_state(
15998 &r#"
15999 ˇ<script>
16000 ˇvar x = new Y();
16001 ˇ</script>
16002 "#
16003 .unindent(),
16004 );
16005 cx.executor().run_until_parked();
16006 cx.update_editor(|editor, window, cx| {
16007 editor.toggle_comments(&ToggleComments::default(), window, cx)
16008 });
16009 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16010 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16011 cx.assert_editor_state(
16012 &r#"
16013 <!-- ˇ<script> -->
16014 // ˇvar x = new Y();
16015 <!-- ˇ</script> -->
16016 "#
16017 .unindent(),
16018 );
16019}
16020
16021#[gpui::test]
16022fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16023 init_test(cx, |_| {});
16024
16025 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16026 let multibuffer = cx.new(|cx| {
16027 let mut multibuffer = MultiBuffer::new(ReadWrite);
16028 multibuffer.push_excerpts(
16029 buffer.clone(),
16030 [
16031 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16032 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16033 ],
16034 cx,
16035 );
16036 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16037 multibuffer
16038 });
16039
16040 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16041 editor.update_in(cx, |editor, window, cx| {
16042 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16044 s.select_ranges([
16045 Point::new(0, 0)..Point::new(0, 0),
16046 Point::new(1, 0)..Point::new(1, 0),
16047 ])
16048 });
16049
16050 editor.handle_input("X", window, cx);
16051 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16052 assert_eq!(
16053 editor.selections.ranges(&editor.display_snapshot(cx)),
16054 [
16055 Point::new(0, 1)..Point::new(0, 1),
16056 Point::new(1, 1)..Point::new(1, 1),
16057 ]
16058 );
16059
16060 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16062 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16063 });
16064 editor.backspace(&Default::default(), window, cx);
16065 assert_eq!(editor.text(cx), "Xa\nbbb");
16066 assert_eq!(
16067 editor.selections.ranges(&editor.display_snapshot(cx)),
16068 [Point::new(1, 0)..Point::new(1, 0)]
16069 );
16070
16071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16072 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16073 });
16074 editor.backspace(&Default::default(), window, cx);
16075 assert_eq!(editor.text(cx), "X\nbb");
16076 assert_eq!(
16077 editor.selections.ranges(&editor.display_snapshot(cx)),
16078 [Point::new(0, 1)..Point::new(0, 1)]
16079 );
16080 });
16081}
16082
16083#[gpui::test]
16084fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16085 init_test(cx, |_| {});
16086
16087 let markers = vec![('[', ']').into(), ('(', ')').into()];
16088 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16089 indoc! {"
16090 [aaaa
16091 (bbbb]
16092 cccc)",
16093 },
16094 markers.clone(),
16095 );
16096 let excerpt_ranges = markers.into_iter().map(|marker| {
16097 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16098 ExcerptRange::new(context)
16099 });
16100 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16101 let multibuffer = cx.new(|cx| {
16102 let mut multibuffer = MultiBuffer::new(ReadWrite);
16103 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16104 multibuffer
16105 });
16106
16107 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16108 editor.update_in(cx, |editor, window, cx| {
16109 let (expected_text, selection_ranges) = marked_text_ranges(
16110 indoc! {"
16111 aaaa
16112 bˇbbb
16113 bˇbbˇb
16114 cccc"
16115 },
16116 true,
16117 );
16118 assert_eq!(editor.text(cx), expected_text);
16119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16120 s.select_ranges(selection_ranges)
16121 });
16122
16123 editor.handle_input("X", window, cx);
16124
16125 let (expected_text, expected_selections) = marked_text_ranges(
16126 indoc! {"
16127 aaaa
16128 bXˇbbXb
16129 bXˇbbXˇb
16130 cccc"
16131 },
16132 false,
16133 );
16134 assert_eq!(editor.text(cx), expected_text);
16135 assert_eq!(
16136 editor.selections.ranges(&editor.display_snapshot(cx)),
16137 expected_selections
16138 );
16139
16140 editor.newline(&Newline, window, cx);
16141 let (expected_text, expected_selections) = marked_text_ranges(
16142 indoc! {"
16143 aaaa
16144 bX
16145 ˇbbX
16146 b
16147 bX
16148 ˇbbX
16149 ˇb
16150 cccc"
16151 },
16152 false,
16153 );
16154 assert_eq!(editor.text(cx), expected_text);
16155 assert_eq!(
16156 editor.selections.ranges(&editor.display_snapshot(cx)),
16157 expected_selections
16158 );
16159 });
16160}
16161
16162#[gpui::test]
16163fn test_refresh_selections(cx: &mut TestAppContext) {
16164 init_test(cx, |_| {});
16165
16166 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16167 let mut excerpt1_id = None;
16168 let multibuffer = cx.new(|cx| {
16169 let mut multibuffer = MultiBuffer::new(ReadWrite);
16170 excerpt1_id = multibuffer
16171 .push_excerpts(
16172 buffer.clone(),
16173 [
16174 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16175 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16176 ],
16177 cx,
16178 )
16179 .into_iter()
16180 .next();
16181 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16182 multibuffer
16183 });
16184
16185 let editor = cx.add_window(|window, cx| {
16186 let mut editor = build_editor(multibuffer.clone(), window, cx);
16187 let snapshot = editor.snapshot(window, cx);
16188 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16189 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16190 });
16191 editor.begin_selection(
16192 Point::new(2, 1).to_display_point(&snapshot),
16193 true,
16194 1,
16195 window,
16196 cx,
16197 );
16198 assert_eq!(
16199 editor.selections.ranges(&editor.display_snapshot(cx)),
16200 [
16201 Point::new(1, 3)..Point::new(1, 3),
16202 Point::new(2, 1)..Point::new(2, 1),
16203 ]
16204 );
16205 editor
16206 });
16207
16208 // Refreshing selections is a no-op when excerpts haven't changed.
16209 _ = editor.update(cx, |editor, window, cx| {
16210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16211 assert_eq!(
16212 editor.selections.ranges(&editor.display_snapshot(cx)),
16213 [
16214 Point::new(1, 3)..Point::new(1, 3),
16215 Point::new(2, 1)..Point::new(2, 1),
16216 ]
16217 );
16218 });
16219
16220 multibuffer.update(cx, |multibuffer, cx| {
16221 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16222 });
16223 _ = editor.update(cx, |editor, window, cx| {
16224 // Removing an excerpt causes the first selection to become degenerate.
16225 assert_eq!(
16226 editor.selections.ranges(&editor.display_snapshot(cx)),
16227 [
16228 Point::new(0, 0)..Point::new(0, 0),
16229 Point::new(0, 1)..Point::new(0, 1)
16230 ]
16231 );
16232
16233 // Refreshing selections will relocate the first selection to the original buffer
16234 // location.
16235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16236 assert_eq!(
16237 editor.selections.ranges(&editor.display_snapshot(cx)),
16238 [
16239 Point::new(0, 1)..Point::new(0, 1),
16240 Point::new(0, 3)..Point::new(0, 3)
16241 ]
16242 );
16243 assert!(editor.selections.pending_anchor().is_some());
16244 });
16245}
16246
16247#[gpui::test]
16248fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16249 init_test(cx, |_| {});
16250
16251 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16252 let mut excerpt1_id = None;
16253 let multibuffer = cx.new(|cx| {
16254 let mut multibuffer = MultiBuffer::new(ReadWrite);
16255 excerpt1_id = multibuffer
16256 .push_excerpts(
16257 buffer.clone(),
16258 [
16259 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16260 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16261 ],
16262 cx,
16263 )
16264 .into_iter()
16265 .next();
16266 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16267 multibuffer
16268 });
16269
16270 let editor = cx.add_window(|window, cx| {
16271 let mut editor = build_editor(multibuffer.clone(), window, cx);
16272 let snapshot = editor.snapshot(window, cx);
16273 editor.begin_selection(
16274 Point::new(1, 3).to_display_point(&snapshot),
16275 false,
16276 1,
16277 window,
16278 cx,
16279 );
16280 assert_eq!(
16281 editor.selections.ranges(&editor.display_snapshot(cx)),
16282 [Point::new(1, 3)..Point::new(1, 3)]
16283 );
16284 editor
16285 });
16286
16287 multibuffer.update(cx, |multibuffer, cx| {
16288 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16289 });
16290 _ = editor.update(cx, |editor, window, cx| {
16291 assert_eq!(
16292 editor.selections.ranges(&editor.display_snapshot(cx)),
16293 [Point::new(0, 0)..Point::new(0, 0)]
16294 );
16295
16296 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16297 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16298 assert_eq!(
16299 editor.selections.ranges(&editor.display_snapshot(cx)),
16300 [Point::new(0, 3)..Point::new(0, 3)]
16301 );
16302 assert!(editor.selections.pending_anchor().is_some());
16303 });
16304}
16305
16306#[gpui::test]
16307async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16308 init_test(cx, |_| {});
16309
16310 let language = Arc::new(
16311 Language::new(
16312 LanguageConfig {
16313 brackets: BracketPairConfig {
16314 pairs: vec![
16315 BracketPair {
16316 start: "{".to_string(),
16317 end: "}".to_string(),
16318 close: true,
16319 surround: true,
16320 newline: true,
16321 },
16322 BracketPair {
16323 start: "/* ".to_string(),
16324 end: " */".to_string(),
16325 close: true,
16326 surround: true,
16327 newline: true,
16328 },
16329 ],
16330 ..Default::default()
16331 },
16332 ..Default::default()
16333 },
16334 Some(tree_sitter_rust::LANGUAGE.into()),
16335 )
16336 .with_indents_query("")
16337 .unwrap(),
16338 );
16339
16340 let text = concat!(
16341 "{ }\n", //
16342 " x\n", //
16343 " /* */\n", //
16344 "x\n", //
16345 "{{} }\n", //
16346 );
16347
16348 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16349 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16350 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16351 editor
16352 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16353 .await;
16354
16355 editor.update_in(cx, |editor, window, cx| {
16356 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16357 s.select_display_ranges([
16358 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16359 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16360 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16361 ])
16362 });
16363 editor.newline(&Newline, window, cx);
16364
16365 assert_eq!(
16366 editor.buffer().read(cx).read(cx).text(),
16367 concat!(
16368 "{ \n", // Suppress rustfmt
16369 "\n", //
16370 "}\n", //
16371 " x\n", //
16372 " /* \n", //
16373 " \n", //
16374 " */\n", //
16375 "x\n", //
16376 "{{} \n", //
16377 "}\n", //
16378 )
16379 );
16380 });
16381}
16382
16383#[gpui::test]
16384fn test_highlighted_ranges(cx: &mut TestAppContext) {
16385 init_test(cx, |_| {});
16386
16387 let editor = cx.add_window(|window, cx| {
16388 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16389 build_editor(buffer, window, cx)
16390 });
16391
16392 _ = editor.update(cx, |editor, window, cx| {
16393 struct Type1;
16394 struct Type2;
16395
16396 let buffer = editor.buffer.read(cx).snapshot(cx);
16397
16398 let anchor_range =
16399 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16400
16401 editor.highlight_background::<Type1>(
16402 &[
16403 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16404 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16405 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16406 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16407 ],
16408 |_| Hsla::red(),
16409 cx,
16410 );
16411 editor.highlight_background::<Type2>(
16412 &[
16413 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16414 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16415 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16416 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16417 ],
16418 |_| Hsla::green(),
16419 cx,
16420 );
16421
16422 let snapshot = editor.snapshot(window, cx);
16423 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16424 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16425 &snapshot,
16426 cx.theme(),
16427 );
16428 assert_eq!(
16429 highlighted_ranges,
16430 &[
16431 (
16432 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16433 Hsla::green(),
16434 ),
16435 (
16436 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16437 Hsla::red(),
16438 ),
16439 (
16440 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16441 Hsla::green(),
16442 ),
16443 (
16444 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16445 Hsla::red(),
16446 ),
16447 ]
16448 );
16449 assert_eq!(
16450 editor.sorted_background_highlights_in_range(
16451 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16452 &snapshot,
16453 cx.theme(),
16454 ),
16455 &[(
16456 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16457 Hsla::red(),
16458 )]
16459 );
16460 });
16461}
16462
16463#[gpui::test]
16464async fn test_following(cx: &mut TestAppContext) {
16465 init_test(cx, |_| {});
16466
16467 let fs = FakeFs::new(cx.executor());
16468 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16469
16470 let buffer = project.update(cx, |project, cx| {
16471 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16472 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16473 });
16474 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16475 let follower = cx.update(|cx| {
16476 cx.open_window(
16477 WindowOptions {
16478 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16479 gpui::Point::new(px(0.), px(0.)),
16480 gpui::Point::new(px(10.), px(80.)),
16481 ))),
16482 ..Default::default()
16483 },
16484 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16485 )
16486 .unwrap()
16487 });
16488
16489 let is_still_following = Rc::new(RefCell::new(true));
16490 let follower_edit_event_count = Rc::new(RefCell::new(0));
16491 let pending_update = Rc::new(RefCell::new(None));
16492 let leader_entity = leader.root(cx).unwrap();
16493 let follower_entity = follower.root(cx).unwrap();
16494 _ = follower.update(cx, {
16495 let update = pending_update.clone();
16496 let is_still_following = is_still_following.clone();
16497 let follower_edit_event_count = follower_edit_event_count.clone();
16498 |_, window, cx| {
16499 cx.subscribe_in(
16500 &leader_entity,
16501 window,
16502 move |_, leader, event, window, cx| {
16503 leader.read(cx).add_event_to_update_proto(
16504 event,
16505 &mut update.borrow_mut(),
16506 window,
16507 cx,
16508 );
16509 },
16510 )
16511 .detach();
16512
16513 cx.subscribe_in(
16514 &follower_entity,
16515 window,
16516 move |_, _, event: &EditorEvent, _window, _cx| {
16517 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16518 *is_still_following.borrow_mut() = false;
16519 }
16520
16521 if let EditorEvent::BufferEdited = event {
16522 *follower_edit_event_count.borrow_mut() += 1;
16523 }
16524 },
16525 )
16526 .detach();
16527 }
16528 });
16529
16530 // Update the selections only
16531 _ = leader.update(cx, |leader, window, cx| {
16532 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16533 s.select_ranges([1..1])
16534 });
16535 });
16536 follower
16537 .update(cx, |follower, window, cx| {
16538 follower.apply_update_proto(
16539 &project,
16540 pending_update.borrow_mut().take().unwrap(),
16541 window,
16542 cx,
16543 )
16544 })
16545 .unwrap()
16546 .await
16547 .unwrap();
16548 _ = follower.update(cx, |follower, _, cx| {
16549 assert_eq!(
16550 follower.selections.ranges(&follower.display_snapshot(cx)),
16551 vec![1..1]
16552 );
16553 });
16554 assert!(*is_still_following.borrow());
16555 assert_eq!(*follower_edit_event_count.borrow(), 0);
16556
16557 // Update the scroll position only
16558 _ = leader.update(cx, |leader, window, cx| {
16559 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16560 });
16561 follower
16562 .update(cx, |follower, window, cx| {
16563 follower.apply_update_proto(
16564 &project,
16565 pending_update.borrow_mut().take().unwrap(),
16566 window,
16567 cx,
16568 )
16569 })
16570 .unwrap()
16571 .await
16572 .unwrap();
16573 assert_eq!(
16574 follower
16575 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16576 .unwrap(),
16577 gpui::Point::new(1.5, 3.5)
16578 );
16579 assert!(*is_still_following.borrow());
16580 assert_eq!(*follower_edit_event_count.borrow(), 0);
16581
16582 // Update the selections and scroll position. The follower's scroll position is updated
16583 // via autoscroll, not via the leader's exact scroll position.
16584 _ = leader.update(cx, |leader, window, cx| {
16585 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16586 s.select_ranges([0..0])
16587 });
16588 leader.request_autoscroll(Autoscroll::newest(), cx);
16589 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16590 });
16591 follower
16592 .update(cx, |follower, window, cx| {
16593 follower.apply_update_proto(
16594 &project,
16595 pending_update.borrow_mut().take().unwrap(),
16596 window,
16597 cx,
16598 )
16599 })
16600 .unwrap()
16601 .await
16602 .unwrap();
16603 _ = follower.update(cx, |follower, _, cx| {
16604 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16605 assert_eq!(
16606 follower.selections.ranges(&follower.display_snapshot(cx)),
16607 vec![0..0]
16608 );
16609 });
16610 assert!(*is_still_following.borrow());
16611
16612 // Creating a pending selection that precedes another selection
16613 _ = leader.update(cx, |leader, window, cx| {
16614 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16615 s.select_ranges([1..1])
16616 });
16617 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16618 });
16619 follower
16620 .update(cx, |follower, window, cx| {
16621 follower.apply_update_proto(
16622 &project,
16623 pending_update.borrow_mut().take().unwrap(),
16624 window,
16625 cx,
16626 )
16627 })
16628 .unwrap()
16629 .await
16630 .unwrap();
16631 _ = follower.update(cx, |follower, _, cx| {
16632 assert_eq!(
16633 follower.selections.ranges(&follower.display_snapshot(cx)),
16634 vec![0..0, 1..1]
16635 );
16636 });
16637 assert!(*is_still_following.borrow());
16638
16639 // Extend the pending selection so that it surrounds another selection
16640 _ = leader.update(cx, |leader, window, cx| {
16641 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16642 });
16643 follower
16644 .update(cx, |follower, window, cx| {
16645 follower.apply_update_proto(
16646 &project,
16647 pending_update.borrow_mut().take().unwrap(),
16648 window,
16649 cx,
16650 )
16651 })
16652 .unwrap()
16653 .await
16654 .unwrap();
16655 _ = follower.update(cx, |follower, _, cx| {
16656 assert_eq!(
16657 follower.selections.ranges(&follower.display_snapshot(cx)),
16658 vec![0..2]
16659 );
16660 });
16661
16662 // Scrolling locally breaks the follow
16663 _ = follower.update(cx, |follower, window, cx| {
16664 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16665 follower.set_scroll_anchor(
16666 ScrollAnchor {
16667 anchor: top_anchor,
16668 offset: gpui::Point::new(0.0, 0.5),
16669 },
16670 window,
16671 cx,
16672 );
16673 });
16674 assert!(!(*is_still_following.borrow()));
16675}
16676
16677#[gpui::test]
16678async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16679 init_test(cx, |_| {});
16680
16681 let fs = FakeFs::new(cx.executor());
16682 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16684 let pane = workspace
16685 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16686 .unwrap();
16687
16688 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16689
16690 let leader = pane.update_in(cx, |_, window, cx| {
16691 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16692 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16693 });
16694
16695 // Start following the editor when it has no excerpts.
16696 let mut state_message =
16697 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16698 let workspace_entity = workspace.root(cx).unwrap();
16699 let follower_1 = cx
16700 .update_window(*workspace.deref(), |_, window, cx| {
16701 Editor::from_state_proto(
16702 workspace_entity,
16703 ViewId {
16704 creator: CollaboratorId::PeerId(PeerId::default()),
16705 id: 0,
16706 },
16707 &mut state_message,
16708 window,
16709 cx,
16710 )
16711 })
16712 .unwrap()
16713 .unwrap()
16714 .await
16715 .unwrap();
16716
16717 let update_message = Rc::new(RefCell::new(None));
16718 follower_1.update_in(cx, {
16719 let update = update_message.clone();
16720 |_, window, cx| {
16721 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16722 leader.read(cx).add_event_to_update_proto(
16723 event,
16724 &mut update.borrow_mut(),
16725 window,
16726 cx,
16727 );
16728 })
16729 .detach();
16730 }
16731 });
16732
16733 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16734 (
16735 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16736 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16737 )
16738 });
16739
16740 // Insert some excerpts.
16741 leader.update(cx, |leader, cx| {
16742 leader.buffer.update(cx, |multibuffer, cx| {
16743 multibuffer.set_excerpts_for_path(
16744 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16745 buffer_1.clone(),
16746 vec![
16747 Point::row_range(0..3),
16748 Point::row_range(1..6),
16749 Point::row_range(12..15),
16750 ],
16751 0,
16752 cx,
16753 );
16754 multibuffer.set_excerpts_for_path(
16755 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16756 buffer_2.clone(),
16757 vec![Point::row_range(0..6), Point::row_range(8..12)],
16758 0,
16759 cx,
16760 );
16761 });
16762 });
16763
16764 // Apply the update of adding the excerpts.
16765 follower_1
16766 .update_in(cx, |follower, window, cx| {
16767 follower.apply_update_proto(
16768 &project,
16769 update_message.borrow().clone().unwrap(),
16770 window,
16771 cx,
16772 )
16773 })
16774 .await
16775 .unwrap();
16776 assert_eq!(
16777 follower_1.update(cx, |editor, cx| editor.text(cx)),
16778 leader.update(cx, |editor, cx| editor.text(cx))
16779 );
16780 update_message.borrow_mut().take();
16781
16782 // Start following separately after it already has excerpts.
16783 let mut state_message =
16784 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16785 let workspace_entity = workspace.root(cx).unwrap();
16786 let follower_2 = cx
16787 .update_window(*workspace.deref(), |_, window, cx| {
16788 Editor::from_state_proto(
16789 workspace_entity,
16790 ViewId {
16791 creator: CollaboratorId::PeerId(PeerId::default()),
16792 id: 0,
16793 },
16794 &mut state_message,
16795 window,
16796 cx,
16797 )
16798 })
16799 .unwrap()
16800 .unwrap()
16801 .await
16802 .unwrap();
16803 assert_eq!(
16804 follower_2.update(cx, |editor, cx| editor.text(cx)),
16805 leader.update(cx, |editor, cx| editor.text(cx))
16806 );
16807
16808 // Remove some excerpts.
16809 leader.update(cx, |leader, cx| {
16810 leader.buffer.update(cx, |multibuffer, cx| {
16811 let excerpt_ids = multibuffer.excerpt_ids();
16812 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16813 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16814 });
16815 });
16816
16817 // Apply the update of removing the excerpts.
16818 follower_1
16819 .update_in(cx, |follower, window, cx| {
16820 follower.apply_update_proto(
16821 &project,
16822 update_message.borrow().clone().unwrap(),
16823 window,
16824 cx,
16825 )
16826 })
16827 .await
16828 .unwrap();
16829 follower_2
16830 .update_in(cx, |follower, window, cx| {
16831 follower.apply_update_proto(
16832 &project,
16833 update_message.borrow().clone().unwrap(),
16834 window,
16835 cx,
16836 )
16837 })
16838 .await
16839 .unwrap();
16840 update_message.borrow_mut().take();
16841 assert_eq!(
16842 follower_1.update(cx, |editor, cx| editor.text(cx)),
16843 leader.update(cx, |editor, cx| editor.text(cx))
16844 );
16845}
16846
16847#[gpui::test]
16848async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16849 init_test(cx, |_| {});
16850
16851 let mut cx = EditorTestContext::new(cx).await;
16852 let lsp_store =
16853 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16854
16855 cx.set_state(indoc! {"
16856 ˇfn func(abc def: i32) -> u32 {
16857 }
16858 "});
16859
16860 cx.update(|_, cx| {
16861 lsp_store.update(cx, |lsp_store, cx| {
16862 lsp_store
16863 .update_diagnostics(
16864 LanguageServerId(0),
16865 lsp::PublishDiagnosticsParams {
16866 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16867 version: None,
16868 diagnostics: vec![
16869 lsp::Diagnostic {
16870 range: lsp::Range::new(
16871 lsp::Position::new(0, 11),
16872 lsp::Position::new(0, 12),
16873 ),
16874 severity: Some(lsp::DiagnosticSeverity::ERROR),
16875 ..Default::default()
16876 },
16877 lsp::Diagnostic {
16878 range: lsp::Range::new(
16879 lsp::Position::new(0, 12),
16880 lsp::Position::new(0, 15),
16881 ),
16882 severity: Some(lsp::DiagnosticSeverity::ERROR),
16883 ..Default::default()
16884 },
16885 lsp::Diagnostic {
16886 range: lsp::Range::new(
16887 lsp::Position::new(0, 25),
16888 lsp::Position::new(0, 28),
16889 ),
16890 severity: Some(lsp::DiagnosticSeverity::ERROR),
16891 ..Default::default()
16892 },
16893 ],
16894 },
16895 None,
16896 DiagnosticSourceKind::Pushed,
16897 &[],
16898 cx,
16899 )
16900 .unwrap()
16901 });
16902 });
16903
16904 executor.run_until_parked();
16905
16906 cx.update_editor(|editor, window, cx| {
16907 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16908 });
16909
16910 cx.assert_editor_state(indoc! {"
16911 fn func(abc def: i32) -> ˇu32 {
16912 }
16913 "});
16914
16915 cx.update_editor(|editor, window, cx| {
16916 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16917 });
16918
16919 cx.assert_editor_state(indoc! {"
16920 fn func(abc ˇdef: i32) -> u32 {
16921 }
16922 "});
16923
16924 cx.update_editor(|editor, window, cx| {
16925 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16926 });
16927
16928 cx.assert_editor_state(indoc! {"
16929 fn func(abcˇ def: i32) -> u32 {
16930 }
16931 "});
16932
16933 cx.update_editor(|editor, window, cx| {
16934 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16935 });
16936
16937 cx.assert_editor_state(indoc! {"
16938 fn func(abc def: i32) -> ˇu32 {
16939 }
16940 "});
16941}
16942
16943#[gpui::test]
16944async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16945 init_test(cx, |_| {});
16946
16947 let mut cx = EditorTestContext::new(cx).await;
16948
16949 let diff_base = r#"
16950 use some::mod;
16951
16952 const A: u32 = 42;
16953
16954 fn main() {
16955 println!("hello");
16956
16957 println!("world");
16958 }
16959 "#
16960 .unindent();
16961
16962 // Edits are modified, removed, modified, added
16963 cx.set_state(
16964 &r#"
16965 use some::modified;
16966
16967 ˇ
16968 fn main() {
16969 println!("hello there");
16970
16971 println!("around the");
16972 println!("world");
16973 }
16974 "#
16975 .unindent(),
16976 );
16977
16978 cx.set_head_text(&diff_base);
16979 executor.run_until_parked();
16980
16981 cx.update_editor(|editor, window, cx| {
16982 //Wrap around the bottom of the buffer
16983 for _ in 0..3 {
16984 editor.go_to_next_hunk(&GoToHunk, window, cx);
16985 }
16986 });
16987
16988 cx.assert_editor_state(
16989 &r#"
16990 ˇuse some::modified;
16991
16992
16993 fn main() {
16994 println!("hello there");
16995
16996 println!("around the");
16997 println!("world");
16998 }
16999 "#
17000 .unindent(),
17001 );
17002
17003 cx.update_editor(|editor, window, cx| {
17004 //Wrap around the top of the buffer
17005 for _ in 0..2 {
17006 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17007 }
17008 });
17009
17010 cx.assert_editor_state(
17011 &r#"
17012 use some::modified;
17013
17014
17015 fn main() {
17016 ˇ println!("hello there");
17017
17018 println!("around the");
17019 println!("world");
17020 }
17021 "#
17022 .unindent(),
17023 );
17024
17025 cx.update_editor(|editor, window, cx| {
17026 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17027 });
17028
17029 cx.assert_editor_state(
17030 &r#"
17031 use some::modified;
17032
17033 ˇ
17034 fn main() {
17035 println!("hello there");
17036
17037 println!("around the");
17038 println!("world");
17039 }
17040 "#
17041 .unindent(),
17042 );
17043
17044 cx.update_editor(|editor, window, cx| {
17045 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17046 });
17047
17048 cx.assert_editor_state(
17049 &r#"
17050 ˇuse some::modified;
17051
17052
17053 fn main() {
17054 println!("hello there");
17055
17056 println!("around the");
17057 println!("world");
17058 }
17059 "#
17060 .unindent(),
17061 );
17062
17063 cx.update_editor(|editor, window, cx| {
17064 for _ in 0..2 {
17065 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17066 }
17067 });
17068
17069 cx.assert_editor_state(
17070 &r#"
17071 use some::modified;
17072
17073
17074 fn main() {
17075 ˇ println!("hello there");
17076
17077 println!("around the");
17078 println!("world");
17079 }
17080 "#
17081 .unindent(),
17082 );
17083
17084 cx.update_editor(|editor, window, cx| {
17085 editor.fold(&Fold, window, cx);
17086 });
17087
17088 cx.update_editor(|editor, window, cx| {
17089 editor.go_to_next_hunk(&GoToHunk, window, cx);
17090 });
17091
17092 cx.assert_editor_state(
17093 &r#"
17094 ˇuse some::modified;
17095
17096
17097 fn main() {
17098 println!("hello there");
17099
17100 println!("around the");
17101 println!("world");
17102 }
17103 "#
17104 .unindent(),
17105 );
17106}
17107
17108#[test]
17109fn test_split_words() {
17110 fn split(text: &str) -> Vec<&str> {
17111 split_words(text).collect()
17112 }
17113
17114 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17115 assert_eq!(split("hello_world"), &["hello_", "world"]);
17116 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17117 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17118 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17119 assert_eq!(split("helloworld"), &["helloworld"]);
17120
17121 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17122}
17123
17124#[gpui::test]
17125async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17126 init_test(cx, |_| {});
17127
17128 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17129 let mut assert = |before, after| {
17130 let _state_context = cx.set_state(before);
17131 cx.run_until_parked();
17132 cx.update_editor(|editor, window, cx| {
17133 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17134 });
17135 cx.run_until_parked();
17136 cx.assert_editor_state(after);
17137 };
17138
17139 // Outside bracket jumps to outside of matching bracket
17140 assert("console.logˇ(var);", "console.log(var)ˇ;");
17141 assert("console.log(var)ˇ;", "console.logˇ(var);");
17142
17143 // Inside bracket jumps to inside of matching bracket
17144 assert("console.log(ˇvar);", "console.log(varˇ);");
17145 assert("console.log(varˇ);", "console.log(ˇvar);");
17146
17147 // When outside a bracket and inside, favor jumping to the inside bracket
17148 assert(
17149 "console.log('foo', [1, 2, 3]ˇ);",
17150 "console.log(ˇ'foo', [1, 2, 3]);",
17151 );
17152 assert(
17153 "console.log(ˇ'foo', [1, 2, 3]);",
17154 "console.log('foo', [1, 2, 3]ˇ);",
17155 );
17156
17157 // Bias forward if two options are equally likely
17158 assert(
17159 "let result = curried_fun()ˇ();",
17160 "let result = curried_fun()()ˇ;",
17161 );
17162
17163 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17164 assert(
17165 indoc! {"
17166 function test() {
17167 console.log('test')ˇ
17168 }"},
17169 indoc! {"
17170 function test() {
17171 console.logˇ('test')
17172 }"},
17173 );
17174}
17175
17176#[gpui::test]
17177async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17178 init_test(cx, |_| {});
17179
17180 let fs = FakeFs::new(cx.executor());
17181 fs.insert_tree(
17182 path!("/a"),
17183 json!({
17184 "main.rs": "fn main() { let a = 5; }",
17185 "other.rs": "// Test file",
17186 }),
17187 )
17188 .await;
17189 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17190
17191 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17192 language_registry.add(Arc::new(Language::new(
17193 LanguageConfig {
17194 name: "Rust".into(),
17195 matcher: LanguageMatcher {
17196 path_suffixes: vec!["rs".to_string()],
17197 ..Default::default()
17198 },
17199 brackets: BracketPairConfig {
17200 pairs: vec![BracketPair {
17201 start: "{".to_string(),
17202 end: "}".to_string(),
17203 close: true,
17204 surround: true,
17205 newline: true,
17206 }],
17207 disabled_scopes_by_bracket_ix: Vec::new(),
17208 },
17209 ..Default::default()
17210 },
17211 Some(tree_sitter_rust::LANGUAGE.into()),
17212 )));
17213 let mut fake_servers = language_registry.register_fake_lsp(
17214 "Rust",
17215 FakeLspAdapter {
17216 capabilities: lsp::ServerCapabilities {
17217 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17218 first_trigger_character: "{".to_string(),
17219 more_trigger_character: None,
17220 }),
17221 ..Default::default()
17222 },
17223 ..Default::default()
17224 },
17225 );
17226
17227 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17228
17229 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17230
17231 let worktree_id = workspace
17232 .update(cx, |workspace, _, cx| {
17233 workspace.project().update(cx, |project, cx| {
17234 project.worktrees(cx).next().unwrap().read(cx).id()
17235 })
17236 })
17237 .unwrap();
17238
17239 let buffer = project
17240 .update(cx, |project, cx| {
17241 project.open_local_buffer(path!("/a/main.rs"), cx)
17242 })
17243 .await
17244 .unwrap();
17245 let editor_handle = workspace
17246 .update(cx, |workspace, window, cx| {
17247 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17248 })
17249 .unwrap()
17250 .await
17251 .unwrap()
17252 .downcast::<Editor>()
17253 .unwrap();
17254
17255 cx.executor().start_waiting();
17256 let fake_server = fake_servers.next().await.unwrap();
17257
17258 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17259 |params, _| async move {
17260 assert_eq!(
17261 params.text_document_position.text_document.uri,
17262 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17263 );
17264 assert_eq!(
17265 params.text_document_position.position,
17266 lsp::Position::new(0, 21),
17267 );
17268
17269 Ok(Some(vec![lsp::TextEdit {
17270 new_text: "]".to_string(),
17271 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17272 }]))
17273 },
17274 );
17275
17276 editor_handle.update_in(cx, |editor, window, cx| {
17277 window.focus(&editor.focus_handle(cx));
17278 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17279 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17280 });
17281 editor.handle_input("{", window, cx);
17282 });
17283
17284 cx.executor().run_until_parked();
17285
17286 buffer.update(cx, |buffer, _| {
17287 assert_eq!(
17288 buffer.text(),
17289 "fn main() { let a = {5}; }",
17290 "No extra braces from on type formatting should appear in the buffer"
17291 )
17292 });
17293}
17294
17295#[gpui::test(iterations = 20, seeds(31))]
17296async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17297 init_test(cx, |_| {});
17298
17299 let mut cx = EditorLspTestContext::new_rust(
17300 lsp::ServerCapabilities {
17301 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17302 first_trigger_character: ".".to_string(),
17303 more_trigger_character: None,
17304 }),
17305 ..Default::default()
17306 },
17307 cx,
17308 )
17309 .await;
17310
17311 cx.update_buffer(|buffer, _| {
17312 // This causes autoindent to be async.
17313 buffer.set_sync_parse_timeout(Duration::ZERO)
17314 });
17315
17316 cx.set_state("fn c() {\n d()ˇ\n}\n");
17317 cx.simulate_keystroke("\n");
17318 cx.run_until_parked();
17319
17320 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17321 let mut request =
17322 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17323 let buffer_cloned = buffer_cloned.clone();
17324 async move {
17325 buffer_cloned.update(&mut cx, |buffer, _| {
17326 assert_eq!(
17327 buffer.text(),
17328 "fn c() {\n d()\n .\n}\n",
17329 "OnTypeFormatting should triggered after autoindent applied"
17330 )
17331 })?;
17332
17333 Ok(Some(vec![]))
17334 }
17335 });
17336
17337 cx.simulate_keystroke(".");
17338 cx.run_until_parked();
17339
17340 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17341 assert!(request.next().await.is_some());
17342 request.close();
17343 assert!(request.next().await.is_none());
17344}
17345
17346#[gpui::test]
17347async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17348 init_test(cx, |_| {});
17349
17350 let fs = FakeFs::new(cx.executor());
17351 fs.insert_tree(
17352 path!("/a"),
17353 json!({
17354 "main.rs": "fn main() { let a = 5; }",
17355 "other.rs": "// Test file",
17356 }),
17357 )
17358 .await;
17359
17360 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17361
17362 let server_restarts = Arc::new(AtomicUsize::new(0));
17363 let closure_restarts = Arc::clone(&server_restarts);
17364 let language_server_name = "test language server";
17365 let language_name: LanguageName = "Rust".into();
17366
17367 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17368 language_registry.add(Arc::new(Language::new(
17369 LanguageConfig {
17370 name: language_name.clone(),
17371 matcher: LanguageMatcher {
17372 path_suffixes: vec!["rs".to_string()],
17373 ..Default::default()
17374 },
17375 ..Default::default()
17376 },
17377 Some(tree_sitter_rust::LANGUAGE.into()),
17378 )));
17379 let mut fake_servers = language_registry.register_fake_lsp(
17380 "Rust",
17381 FakeLspAdapter {
17382 name: language_server_name,
17383 initialization_options: Some(json!({
17384 "testOptionValue": true
17385 })),
17386 initializer: Some(Box::new(move |fake_server| {
17387 let task_restarts = Arc::clone(&closure_restarts);
17388 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17389 task_restarts.fetch_add(1, atomic::Ordering::Release);
17390 futures::future::ready(Ok(()))
17391 });
17392 })),
17393 ..Default::default()
17394 },
17395 );
17396
17397 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17398 let _buffer = project
17399 .update(cx, |project, cx| {
17400 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17401 })
17402 .await
17403 .unwrap();
17404 let _fake_server = fake_servers.next().await.unwrap();
17405 update_test_language_settings(cx, |language_settings| {
17406 language_settings.languages.0.insert(
17407 language_name.clone().0,
17408 LanguageSettingsContent {
17409 tab_size: NonZeroU32::new(8),
17410 ..Default::default()
17411 },
17412 );
17413 });
17414 cx.executor().run_until_parked();
17415 assert_eq!(
17416 server_restarts.load(atomic::Ordering::Acquire),
17417 0,
17418 "Should not restart LSP server on an unrelated change"
17419 );
17420
17421 update_test_project_settings(cx, |project_settings| {
17422 project_settings.lsp.insert(
17423 "Some other server name".into(),
17424 LspSettings {
17425 binary: None,
17426 settings: None,
17427 initialization_options: Some(json!({
17428 "some other init value": false
17429 })),
17430 enable_lsp_tasks: false,
17431 fetch: None,
17432 },
17433 );
17434 });
17435 cx.executor().run_until_parked();
17436 assert_eq!(
17437 server_restarts.load(atomic::Ordering::Acquire),
17438 0,
17439 "Should not restart LSP server on an unrelated LSP settings change"
17440 );
17441
17442 update_test_project_settings(cx, |project_settings| {
17443 project_settings.lsp.insert(
17444 language_server_name.into(),
17445 LspSettings {
17446 binary: None,
17447 settings: None,
17448 initialization_options: Some(json!({
17449 "anotherInitValue": false
17450 })),
17451 enable_lsp_tasks: false,
17452 fetch: None,
17453 },
17454 );
17455 });
17456 cx.executor().run_until_parked();
17457 assert_eq!(
17458 server_restarts.load(atomic::Ordering::Acquire),
17459 1,
17460 "Should restart LSP server on a related LSP settings change"
17461 );
17462
17463 update_test_project_settings(cx, |project_settings| {
17464 project_settings.lsp.insert(
17465 language_server_name.into(),
17466 LspSettings {
17467 binary: None,
17468 settings: None,
17469 initialization_options: Some(json!({
17470 "anotherInitValue": false
17471 })),
17472 enable_lsp_tasks: false,
17473 fetch: None,
17474 },
17475 );
17476 });
17477 cx.executor().run_until_parked();
17478 assert_eq!(
17479 server_restarts.load(atomic::Ordering::Acquire),
17480 1,
17481 "Should not restart LSP server on a related LSP settings change that is the same"
17482 );
17483
17484 update_test_project_settings(cx, |project_settings| {
17485 project_settings.lsp.insert(
17486 language_server_name.into(),
17487 LspSettings {
17488 binary: None,
17489 settings: None,
17490 initialization_options: None,
17491 enable_lsp_tasks: false,
17492 fetch: None,
17493 },
17494 );
17495 });
17496 cx.executor().run_until_parked();
17497 assert_eq!(
17498 server_restarts.load(atomic::Ordering::Acquire),
17499 2,
17500 "Should restart LSP server on another related LSP settings change"
17501 );
17502}
17503
17504#[gpui::test]
17505async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17506 init_test(cx, |_| {});
17507
17508 let mut cx = EditorLspTestContext::new_rust(
17509 lsp::ServerCapabilities {
17510 completion_provider: Some(lsp::CompletionOptions {
17511 trigger_characters: Some(vec![".".to_string()]),
17512 resolve_provider: Some(true),
17513 ..Default::default()
17514 }),
17515 ..Default::default()
17516 },
17517 cx,
17518 )
17519 .await;
17520
17521 cx.set_state("fn main() { let a = 2ˇ; }");
17522 cx.simulate_keystroke(".");
17523 let completion_item = lsp::CompletionItem {
17524 label: "some".into(),
17525 kind: Some(lsp::CompletionItemKind::SNIPPET),
17526 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17527 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17528 kind: lsp::MarkupKind::Markdown,
17529 value: "```rust\nSome(2)\n```".to_string(),
17530 })),
17531 deprecated: Some(false),
17532 sort_text: Some("fffffff2".to_string()),
17533 filter_text: Some("some".to_string()),
17534 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17535 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17536 range: lsp::Range {
17537 start: lsp::Position {
17538 line: 0,
17539 character: 22,
17540 },
17541 end: lsp::Position {
17542 line: 0,
17543 character: 22,
17544 },
17545 },
17546 new_text: "Some(2)".to_string(),
17547 })),
17548 additional_text_edits: Some(vec![lsp::TextEdit {
17549 range: lsp::Range {
17550 start: lsp::Position {
17551 line: 0,
17552 character: 20,
17553 },
17554 end: lsp::Position {
17555 line: 0,
17556 character: 22,
17557 },
17558 },
17559 new_text: "".to_string(),
17560 }]),
17561 ..Default::default()
17562 };
17563
17564 let closure_completion_item = completion_item.clone();
17565 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17566 let task_completion_item = closure_completion_item.clone();
17567 async move {
17568 Ok(Some(lsp::CompletionResponse::Array(vec![
17569 task_completion_item,
17570 ])))
17571 }
17572 });
17573
17574 request.next().await;
17575
17576 cx.condition(|editor, _| editor.context_menu_visible())
17577 .await;
17578 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17579 editor
17580 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17581 .unwrap()
17582 });
17583 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17584
17585 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17586 let task_completion_item = completion_item.clone();
17587 async move { Ok(task_completion_item) }
17588 })
17589 .next()
17590 .await
17591 .unwrap();
17592 apply_additional_edits.await.unwrap();
17593 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17594}
17595
17596#[gpui::test]
17597async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17598 init_test(cx, |_| {});
17599
17600 let mut cx = EditorLspTestContext::new_rust(
17601 lsp::ServerCapabilities {
17602 completion_provider: Some(lsp::CompletionOptions {
17603 trigger_characters: Some(vec![".".to_string()]),
17604 resolve_provider: Some(true),
17605 ..Default::default()
17606 }),
17607 ..Default::default()
17608 },
17609 cx,
17610 )
17611 .await;
17612
17613 cx.set_state("fn main() { let a = 2ˇ; }");
17614 cx.simulate_keystroke(".");
17615
17616 let item1 = lsp::CompletionItem {
17617 label: "method id()".to_string(),
17618 filter_text: Some("id".to_string()),
17619 detail: None,
17620 documentation: None,
17621 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17622 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17623 new_text: ".id".to_string(),
17624 })),
17625 ..lsp::CompletionItem::default()
17626 };
17627
17628 let item2 = lsp::CompletionItem {
17629 label: "other".to_string(),
17630 filter_text: Some("other".to_string()),
17631 detail: None,
17632 documentation: None,
17633 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17634 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17635 new_text: ".other".to_string(),
17636 })),
17637 ..lsp::CompletionItem::default()
17638 };
17639
17640 let item1 = item1.clone();
17641 cx.set_request_handler::<lsp::request::Completion, _, _>({
17642 let item1 = item1.clone();
17643 move |_, _, _| {
17644 let item1 = item1.clone();
17645 let item2 = item2.clone();
17646 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17647 }
17648 })
17649 .next()
17650 .await;
17651
17652 cx.condition(|editor, _| editor.context_menu_visible())
17653 .await;
17654 cx.update_editor(|editor, _, _| {
17655 let context_menu = editor.context_menu.borrow_mut();
17656 let context_menu = context_menu
17657 .as_ref()
17658 .expect("Should have the context menu deployed");
17659 match context_menu {
17660 CodeContextMenu::Completions(completions_menu) => {
17661 let completions = completions_menu.completions.borrow_mut();
17662 assert_eq!(
17663 completions
17664 .iter()
17665 .map(|completion| &completion.label.text)
17666 .collect::<Vec<_>>(),
17667 vec!["method id()", "other"]
17668 )
17669 }
17670 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17671 }
17672 });
17673
17674 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17675 let item1 = item1.clone();
17676 move |_, item_to_resolve, _| {
17677 let item1 = item1.clone();
17678 async move {
17679 if item1 == item_to_resolve {
17680 Ok(lsp::CompletionItem {
17681 label: "method id()".to_string(),
17682 filter_text: Some("id".to_string()),
17683 detail: Some("Now resolved!".to_string()),
17684 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17685 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17686 range: lsp::Range::new(
17687 lsp::Position::new(0, 22),
17688 lsp::Position::new(0, 22),
17689 ),
17690 new_text: ".id".to_string(),
17691 })),
17692 ..lsp::CompletionItem::default()
17693 })
17694 } else {
17695 Ok(item_to_resolve)
17696 }
17697 }
17698 }
17699 })
17700 .next()
17701 .await
17702 .unwrap();
17703 cx.run_until_parked();
17704
17705 cx.update_editor(|editor, window, cx| {
17706 editor.context_menu_next(&Default::default(), window, cx);
17707 });
17708
17709 cx.update_editor(|editor, _, _| {
17710 let context_menu = editor.context_menu.borrow_mut();
17711 let context_menu = context_menu
17712 .as_ref()
17713 .expect("Should have the context menu deployed");
17714 match context_menu {
17715 CodeContextMenu::Completions(completions_menu) => {
17716 let completions = completions_menu.completions.borrow_mut();
17717 assert_eq!(
17718 completions
17719 .iter()
17720 .map(|completion| &completion.label.text)
17721 .collect::<Vec<_>>(),
17722 vec!["method id() Now resolved!", "other"],
17723 "Should update first completion label, but not second as the filter text did not match."
17724 );
17725 }
17726 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17727 }
17728 });
17729}
17730
17731#[gpui::test]
17732async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17733 init_test(cx, |_| {});
17734 let mut cx = EditorLspTestContext::new_rust(
17735 lsp::ServerCapabilities {
17736 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17737 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17738 completion_provider: Some(lsp::CompletionOptions {
17739 resolve_provider: Some(true),
17740 ..Default::default()
17741 }),
17742 ..Default::default()
17743 },
17744 cx,
17745 )
17746 .await;
17747 cx.set_state(indoc! {"
17748 struct TestStruct {
17749 field: i32
17750 }
17751
17752 fn mainˇ() {
17753 let unused_var = 42;
17754 let test_struct = TestStruct { field: 42 };
17755 }
17756 "});
17757 let symbol_range = cx.lsp_range(indoc! {"
17758 struct TestStruct {
17759 field: i32
17760 }
17761
17762 «fn main»() {
17763 let unused_var = 42;
17764 let test_struct = TestStruct { field: 42 };
17765 }
17766 "});
17767 let mut hover_requests =
17768 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17769 Ok(Some(lsp::Hover {
17770 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17771 kind: lsp::MarkupKind::Markdown,
17772 value: "Function documentation".to_string(),
17773 }),
17774 range: Some(symbol_range),
17775 }))
17776 });
17777
17778 // Case 1: Test that code action menu hide hover popover
17779 cx.dispatch_action(Hover);
17780 hover_requests.next().await;
17781 cx.condition(|editor, _| editor.hover_state.visible()).await;
17782 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17783 move |_, _, _| async move {
17784 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17785 lsp::CodeAction {
17786 title: "Remove unused variable".to_string(),
17787 kind: Some(CodeActionKind::QUICKFIX),
17788 edit: Some(lsp::WorkspaceEdit {
17789 changes: Some(
17790 [(
17791 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17792 vec![lsp::TextEdit {
17793 range: lsp::Range::new(
17794 lsp::Position::new(5, 4),
17795 lsp::Position::new(5, 27),
17796 ),
17797 new_text: "".to_string(),
17798 }],
17799 )]
17800 .into_iter()
17801 .collect(),
17802 ),
17803 ..Default::default()
17804 }),
17805 ..Default::default()
17806 },
17807 )]))
17808 },
17809 );
17810 cx.update_editor(|editor, window, cx| {
17811 editor.toggle_code_actions(
17812 &ToggleCodeActions {
17813 deployed_from: None,
17814 quick_launch: false,
17815 },
17816 window,
17817 cx,
17818 );
17819 });
17820 code_action_requests.next().await;
17821 cx.run_until_parked();
17822 cx.condition(|editor, _| editor.context_menu_visible())
17823 .await;
17824 cx.update_editor(|editor, _, _| {
17825 assert!(
17826 !editor.hover_state.visible(),
17827 "Hover popover should be hidden when code action menu is shown"
17828 );
17829 // Hide code actions
17830 editor.context_menu.take();
17831 });
17832
17833 // Case 2: Test that code completions hide hover popover
17834 cx.dispatch_action(Hover);
17835 hover_requests.next().await;
17836 cx.condition(|editor, _| editor.hover_state.visible()).await;
17837 let counter = Arc::new(AtomicUsize::new(0));
17838 let mut completion_requests =
17839 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17840 let counter = counter.clone();
17841 async move {
17842 counter.fetch_add(1, atomic::Ordering::Release);
17843 Ok(Some(lsp::CompletionResponse::Array(vec![
17844 lsp::CompletionItem {
17845 label: "main".into(),
17846 kind: Some(lsp::CompletionItemKind::FUNCTION),
17847 detail: Some("() -> ()".to_string()),
17848 ..Default::default()
17849 },
17850 lsp::CompletionItem {
17851 label: "TestStruct".into(),
17852 kind: Some(lsp::CompletionItemKind::STRUCT),
17853 detail: Some("struct TestStruct".to_string()),
17854 ..Default::default()
17855 },
17856 ])))
17857 }
17858 });
17859 cx.update_editor(|editor, window, cx| {
17860 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17861 });
17862 completion_requests.next().await;
17863 cx.condition(|editor, _| editor.context_menu_visible())
17864 .await;
17865 cx.update_editor(|editor, _, _| {
17866 assert!(
17867 !editor.hover_state.visible(),
17868 "Hover popover should be hidden when completion menu is shown"
17869 );
17870 });
17871}
17872
17873#[gpui::test]
17874async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17875 init_test(cx, |_| {});
17876
17877 let mut cx = EditorLspTestContext::new_rust(
17878 lsp::ServerCapabilities {
17879 completion_provider: Some(lsp::CompletionOptions {
17880 trigger_characters: Some(vec![".".to_string()]),
17881 resolve_provider: Some(true),
17882 ..Default::default()
17883 }),
17884 ..Default::default()
17885 },
17886 cx,
17887 )
17888 .await;
17889
17890 cx.set_state("fn main() { let a = 2ˇ; }");
17891 cx.simulate_keystroke(".");
17892
17893 let unresolved_item_1 = lsp::CompletionItem {
17894 label: "id".to_string(),
17895 filter_text: Some("id".to_string()),
17896 detail: None,
17897 documentation: None,
17898 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17899 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17900 new_text: ".id".to_string(),
17901 })),
17902 ..lsp::CompletionItem::default()
17903 };
17904 let resolved_item_1 = lsp::CompletionItem {
17905 additional_text_edits: Some(vec![lsp::TextEdit {
17906 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17907 new_text: "!!".to_string(),
17908 }]),
17909 ..unresolved_item_1.clone()
17910 };
17911 let unresolved_item_2 = lsp::CompletionItem {
17912 label: "other".to_string(),
17913 filter_text: Some("other".to_string()),
17914 detail: None,
17915 documentation: None,
17916 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17917 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17918 new_text: ".other".to_string(),
17919 })),
17920 ..lsp::CompletionItem::default()
17921 };
17922 let resolved_item_2 = lsp::CompletionItem {
17923 additional_text_edits: Some(vec![lsp::TextEdit {
17924 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17925 new_text: "??".to_string(),
17926 }]),
17927 ..unresolved_item_2.clone()
17928 };
17929
17930 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17931 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17932 cx.lsp
17933 .server
17934 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17935 let unresolved_item_1 = unresolved_item_1.clone();
17936 let resolved_item_1 = resolved_item_1.clone();
17937 let unresolved_item_2 = unresolved_item_2.clone();
17938 let resolved_item_2 = resolved_item_2.clone();
17939 let resolve_requests_1 = resolve_requests_1.clone();
17940 let resolve_requests_2 = resolve_requests_2.clone();
17941 move |unresolved_request, _| {
17942 let unresolved_item_1 = unresolved_item_1.clone();
17943 let resolved_item_1 = resolved_item_1.clone();
17944 let unresolved_item_2 = unresolved_item_2.clone();
17945 let resolved_item_2 = resolved_item_2.clone();
17946 let resolve_requests_1 = resolve_requests_1.clone();
17947 let resolve_requests_2 = resolve_requests_2.clone();
17948 async move {
17949 if unresolved_request == unresolved_item_1 {
17950 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17951 Ok(resolved_item_1.clone())
17952 } else if unresolved_request == unresolved_item_2 {
17953 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17954 Ok(resolved_item_2.clone())
17955 } else {
17956 panic!("Unexpected completion item {unresolved_request:?}")
17957 }
17958 }
17959 }
17960 })
17961 .detach();
17962
17963 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17964 let unresolved_item_1 = unresolved_item_1.clone();
17965 let unresolved_item_2 = unresolved_item_2.clone();
17966 async move {
17967 Ok(Some(lsp::CompletionResponse::Array(vec![
17968 unresolved_item_1,
17969 unresolved_item_2,
17970 ])))
17971 }
17972 })
17973 .next()
17974 .await;
17975
17976 cx.condition(|editor, _| editor.context_menu_visible())
17977 .await;
17978 cx.update_editor(|editor, _, _| {
17979 let context_menu = editor.context_menu.borrow_mut();
17980 let context_menu = context_menu
17981 .as_ref()
17982 .expect("Should have the context menu deployed");
17983 match context_menu {
17984 CodeContextMenu::Completions(completions_menu) => {
17985 let completions = completions_menu.completions.borrow_mut();
17986 assert_eq!(
17987 completions
17988 .iter()
17989 .map(|completion| &completion.label.text)
17990 .collect::<Vec<_>>(),
17991 vec!["id", "other"]
17992 )
17993 }
17994 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17995 }
17996 });
17997 cx.run_until_parked();
17998
17999 cx.update_editor(|editor, window, cx| {
18000 editor.context_menu_next(&ContextMenuNext, window, cx);
18001 });
18002 cx.run_until_parked();
18003 cx.update_editor(|editor, window, cx| {
18004 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18005 });
18006 cx.run_until_parked();
18007 cx.update_editor(|editor, window, cx| {
18008 editor.context_menu_next(&ContextMenuNext, window, cx);
18009 });
18010 cx.run_until_parked();
18011 cx.update_editor(|editor, window, cx| {
18012 editor
18013 .compose_completion(&ComposeCompletion::default(), window, cx)
18014 .expect("No task returned")
18015 })
18016 .await
18017 .expect("Completion failed");
18018 cx.run_until_parked();
18019
18020 cx.update_editor(|editor, _, cx| {
18021 assert_eq!(
18022 resolve_requests_1.load(atomic::Ordering::Acquire),
18023 1,
18024 "Should always resolve once despite multiple selections"
18025 );
18026 assert_eq!(
18027 resolve_requests_2.load(atomic::Ordering::Acquire),
18028 1,
18029 "Should always resolve once after multiple selections and applying the completion"
18030 );
18031 assert_eq!(
18032 editor.text(cx),
18033 "fn main() { let a = ??.other; }",
18034 "Should use resolved data when applying the completion"
18035 );
18036 });
18037}
18038
18039#[gpui::test]
18040async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18041 init_test(cx, |_| {});
18042
18043 let item_0 = lsp::CompletionItem {
18044 label: "abs".into(),
18045 insert_text: Some("abs".into()),
18046 data: Some(json!({ "very": "special"})),
18047 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18048 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18049 lsp::InsertReplaceEdit {
18050 new_text: "abs".to_string(),
18051 insert: lsp::Range::default(),
18052 replace: lsp::Range::default(),
18053 },
18054 )),
18055 ..lsp::CompletionItem::default()
18056 };
18057 let items = iter::once(item_0.clone())
18058 .chain((11..51).map(|i| lsp::CompletionItem {
18059 label: format!("item_{}", i),
18060 insert_text: Some(format!("item_{}", i)),
18061 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18062 ..lsp::CompletionItem::default()
18063 }))
18064 .collect::<Vec<_>>();
18065
18066 let default_commit_characters = vec!["?".to_string()];
18067 let default_data = json!({ "default": "data"});
18068 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18069 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18070 let default_edit_range = lsp::Range {
18071 start: lsp::Position {
18072 line: 0,
18073 character: 5,
18074 },
18075 end: lsp::Position {
18076 line: 0,
18077 character: 5,
18078 },
18079 };
18080
18081 let mut cx = EditorLspTestContext::new_rust(
18082 lsp::ServerCapabilities {
18083 completion_provider: Some(lsp::CompletionOptions {
18084 trigger_characters: Some(vec![".".to_string()]),
18085 resolve_provider: Some(true),
18086 ..Default::default()
18087 }),
18088 ..Default::default()
18089 },
18090 cx,
18091 )
18092 .await;
18093
18094 cx.set_state("fn main() { let a = 2ˇ; }");
18095 cx.simulate_keystroke(".");
18096
18097 let completion_data = default_data.clone();
18098 let completion_characters = default_commit_characters.clone();
18099 let completion_items = items.clone();
18100 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18101 let default_data = completion_data.clone();
18102 let default_commit_characters = completion_characters.clone();
18103 let items = completion_items.clone();
18104 async move {
18105 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18106 items,
18107 item_defaults: Some(lsp::CompletionListItemDefaults {
18108 data: Some(default_data.clone()),
18109 commit_characters: Some(default_commit_characters.clone()),
18110 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18111 default_edit_range,
18112 )),
18113 insert_text_format: Some(default_insert_text_format),
18114 insert_text_mode: Some(default_insert_text_mode),
18115 }),
18116 ..lsp::CompletionList::default()
18117 })))
18118 }
18119 })
18120 .next()
18121 .await;
18122
18123 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18124 cx.lsp
18125 .server
18126 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18127 let closure_resolved_items = resolved_items.clone();
18128 move |item_to_resolve, _| {
18129 let closure_resolved_items = closure_resolved_items.clone();
18130 async move {
18131 closure_resolved_items.lock().push(item_to_resolve.clone());
18132 Ok(item_to_resolve)
18133 }
18134 }
18135 })
18136 .detach();
18137
18138 cx.condition(|editor, _| editor.context_menu_visible())
18139 .await;
18140 cx.run_until_parked();
18141 cx.update_editor(|editor, _, _| {
18142 let menu = editor.context_menu.borrow_mut();
18143 match menu.as_ref().expect("should have the completions menu") {
18144 CodeContextMenu::Completions(completions_menu) => {
18145 assert_eq!(
18146 completions_menu
18147 .entries
18148 .borrow()
18149 .iter()
18150 .map(|mat| mat.string.clone())
18151 .collect::<Vec<String>>(),
18152 items
18153 .iter()
18154 .map(|completion| completion.label.clone())
18155 .collect::<Vec<String>>()
18156 );
18157 }
18158 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18159 }
18160 });
18161 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18162 // with 4 from the end.
18163 assert_eq!(
18164 *resolved_items.lock(),
18165 [&items[0..16], &items[items.len() - 4..items.len()]]
18166 .concat()
18167 .iter()
18168 .cloned()
18169 .map(|mut item| {
18170 if item.data.is_none() {
18171 item.data = Some(default_data.clone());
18172 }
18173 item
18174 })
18175 .collect::<Vec<lsp::CompletionItem>>(),
18176 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18177 );
18178 resolved_items.lock().clear();
18179
18180 cx.update_editor(|editor, window, cx| {
18181 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18182 });
18183 cx.run_until_parked();
18184 // Completions that have already been resolved are skipped.
18185 assert_eq!(
18186 *resolved_items.lock(),
18187 items[items.len() - 17..items.len() - 4]
18188 .iter()
18189 .cloned()
18190 .map(|mut item| {
18191 if item.data.is_none() {
18192 item.data = Some(default_data.clone());
18193 }
18194 item
18195 })
18196 .collect::<Vec<lsp::CompletionItem>>()
18197 );
18198 resolved_items.lock().clear();
18199}
18200
18201#[gpui::test]
18202async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18203 init_test(cx, |_| {});
18204
18205 let mut cx = EditorLspTestContext::new(
18206 Language::new(
18207 LanguageConfig {
18208 matcher: LanguageMatcher {
18209 path_suffixes: vec!["jsx".into()],
18210 ..Default::default()
18211 },
18212 overrides: [(
18213 "element".into(),
18214 LanguageConfigOverride {
18215 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18216 ..Default::default()
18217 },
18218 )]
18219 .into_iter()
18220 .collect(),
18221 ..Default::default()
18222 },
18223 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18224 )
18225 .with_override_query("(jsx_self_closing_element) @element")
18226 .unwrap(),
18227 lsp::ServerCapabilities {
18228 completion_provider: Some(lsp::CompletionOptions {
18229 trigger_characters: Some(vec![":".to_string()]),
18230 ..Default::default()
18231 }),
18232 ..Default::default()
18233 },
18234 cx,
18235 )
18236 .await;
18237
18238 cx.lsp
18239 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18240 Ok(Some(lsp::CompletionResponse::Array(vec![
18241 lsp::CompletionItem {
18242 label: "bg-blue".into(),
18243 ..Default::default()
18244 },
18245 lsp::CompletionItem {
18246 label: "bg-red".into(),
18247 ..Default::default()
18248 },
18249 lsp::CompletionItem {
18250 label: "bg-yellow".into(),
18251 ..Default::default()
18252 },
18253 ])))
18254 });
18255
18256 cx.set_state(r#"<p class="bgˇ" />"#);
18257
18258 // Trigger completion when typing a dash, because the dash is an extra
18259 // word character in the 'element' scope, which contains the cursor.
18260 cx.simulate_keystroke("-");
18261 cx.executor().run_until_parked();
18262 cx.update_editor(|editor, _, _| {
18263 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18264 {
18265 assert_eq!(
18266 completion_menu_entries(menu),
18267 &["bg-blue", "bg-red", "bg-yellow"]
18268 );
18269 } else {
18270 panic!("expected completion menu to be open");
18271 }
18272 });
18273
18274 cx.simulate_keystroke("l");
18275 cx.executor().run_until_parked();
18276 cx.update_editor(|editor, _, _| {
18277 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18278 {
18279 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18280 } else {
18281 panic!("expected completion menu to be open");
18282 }
18283 });
18284
18285 // When filtering completions, consider the character after the '-' to
18286 // be the start of a subword.
18287 cx.set_state(r#"<p class="yelˇ" />"#);
18288 cx.simulate_keystroke("l");
18289 cx.executor().run_until_parked();
18290 cx.update_editor(|editor, _, _| {
18291 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18292 {
18293 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18294 } else {
18295 panic!("expected completion menu to be open");
18296 }
18297 });
18298}
18299
18300fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18301 let entries = menu.entries.borrow();
18302 entries.iter().map(|mat| mat.string.clone()).collect()
18303}
18304
18305#[gpui::test]
18306async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18307 init_test(cx, |settings| {
18308 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18309 });
18310
18311 let fs = FakeFs::new(cx.executor());
18312 fs.insert_file(path!("/file.ts"), Default::default()).await;
18313
18314 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18315 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18316
18317 language_registry.add(Arc::new(Language::new(
18318 LanguageConfig {
18319 name: "TypeScript".into(),
18320 matcher: LanguageMatcher {
18321 path_suffixes: vec!["ts".to_string()],
18322 ..Default::default()
18323 },
18324 ..Default::default()
18325 },
18326 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18327 )));
18328 update_test_language_settings(cx, |settings| {
18329 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18330 });
18331
18332 let test_plugin = "test_plugin";
18333 let _ = language_registry.register_fake_lsp(
18334 "TypeScript",
18335 FakeLspAdapter {
18336 prettier_plugins: vec![test_plugin],
18337 ..Default::default()
18338 },
18339 );
18340
18341 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18342 let buffer = project
18343 .update(cx, |project, cx| {
18344 project.open_local_buffer(path!("/file.ts"), cx)
18345 })
18346 .await
18347 .unwrap();
18348
18349 let buffer_text = "one\ntwo\nthree\n";
18350 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18351 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18352 editor.update_in(cx, |editor, window, cx| {
18353 editor.set_text(buffer_text, window, cx)
18354 });
18355
18356 editor
18357 .update_in(cx, |editor, window, cx| {
18358 editor.perform_format(
18359 project.clone(),
18360 FormatTrigger::Manual,
18361 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18362 window,
18363 cx,
18364 )
18365 })
18366 .unwrap()
18367 .await;
18368 assert_eq!(
18369 editor.update(cx, |editor, cx| editor.text(cx)),
18370 buffer_text.to_string() + prettier_format_suffix,
18371 "Test prettier formatting was not applied to the original buffer text",
18372 );
18373
18374 update_test_language_settings(cx, |settings| {
18375 settings.defaults.formatter = Some(FormatterList::default())
18376 });
18377 let format = editor.update_in(cx, |editor, window, cx| {
18378 editor.perform_format(
18379 project.clone(),
18380 FormatTrigger::Manual,
18381 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18382 window,
18383 cx,
18384 )
18385 });
18386 format.await.unwrap();
18387 assert_eq!(
18388 editor.update(cx, |editor, cx| editor.text(cx)),
18389 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18390 "Autoformatting (via test prettier) was not applied to the original buffer text",
18391 );
18392}
18393
18394#[gpui::test]
18395async fn test_addition_reverts(cx: &mut TestAppContext) {
18396 init_test(cx, |_| {});
18397 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18398 let base_text = indoc! {r#"
18399 struct Row;
18400 struct Row1;
18401 struct Row2;
18402
18403 struct Row4;
18404 struct Row5;
18405 struct Row6;
18406
18407 struct Row8;
18408 struct Row9;
18409 struct Row10;"#};
18410
18411 // When addition hunks are not adjacent to carets, no hunk revert is performed
18412 assert_hunk_revert(
18413 indoc! {r#"struct Row;
18414 struct Row1;
18415 struct Row1.1;
18416 struct Row1.2;
18417 struct Row2;ˇ
18418
18419 struct Row4;
18420 struct Row5;
18421 struct Row6;
18422
18423 struct Row8;
18424 ˇstruct Row9;
18425 struct Row9.1;
18426 struct Row9.2;
18427 struct Row9.3;
18428 struct Row10;"#},
18429 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18430 indoc! {r#"struct Row;
18431 struct Row1;
18432 struct Row1.1;
18433 struct Row1.2;
18434 struct Row2;ˇ
18435
18436 struct Row4;
18437 struct Row5;
18438 struct Row6;
18439
18440 struct Row8;
18441 ˇstruct Row9;
18442 struct Row9.1;
18443 struct Row9.2;
18444 struct Row9.3;
18445 struct Row10;"#},
18446 base_text,
18447 &mut cx,
18448 );
18449 // Same for selections
18450 assert_hunk_revert(
18451 indoc! {r#"struct Row;
18452 struct Row1;
18453 struct Row2;
18454 struct Row2.1;
18455 struct Row2.2;
18456 «ˇ
18457 struct Row4;
18458 struct» Row5;
18459 «struct Row6;
18460 ˇ»
18461 struct Row9.1;
18462 struct Row9.2;
18463 struct Row9.3;
18464 struct Row8;
18465 struct Row9;
18466 struct Row10;"#},
18467 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18468 indoc! {r#"struct Row;
18469 struct Row1;
18470 struct Row2;
18471 struct Row2.1;
18472 struct Row2.2;
18473 «ˇ
18474 struct Row4;
18475 struct» Row5;
18476 «struct Row6;
18477 ˇ»
18478 struct Row9.1;
18479 struct Row9.2;
18480 struct Row9.3;
18481 struct Row8;
18482 struct Row9;
18483 struct Row10;"#},
18484 base_text,
18485 &mut cx,
18486 );
18487
18488 // When carets and selections intersect the addition hunks, those are reverted.
18489 // Adjacent carets got merged.
18490 assert_hunk_revert(
18491 indoc! {r#"struct Row;
18492 ˇ// something on the top
18493 struct Row1;
18494 struct Row2;
18495 struct Roˇw3.1;
18496 struct Row2.2;
18497 struct Row2.3;ˇ
18498
18499 struct Row4;
18500 struct ˇRow5.1;
18501 struct Row5.2;
18502 struct «Rowˇ»5.3;
18503 struct Row5;
18504 struct Row6;
18505 ˇ
18506 struct Row9.1;
18507 struct «Rowˇ»9.2;
18508 struct «ˇRow»9.3;
18509 struct Row8;
18510 struct Row9;
18511 «ˇ// something on bottom»
18512 struct Row10;"#},
18513 vec![
18514 DiffHunkStatusKind::Added,
18515 DiffHunkStatusKind::Added,
18516 DiffHunkStatusKind::Added,
18517 DiffHunkStatusKind::Added,
18518 DiffHunkStatusKind::Added,
18519 ],
18520 indoc! {r#"struct Row;
18521 ˇstruct Row1;
18522 struct Row2;
18523 ˇ
18524 struct Row4;
18525 ˇstruct Row5;
18526 struct Row6;
18527 ˇ
18528 ˇstruct Row8;
18529 struct Row9;
18530 ˇstruct Row10;"#},
18531 base_text,
18532 &mut cx,
18533 );
18534}
18535
18536#[gpui::test]
18537async fn test_modification_reverts(cx: &mut TestAppContext) {
18538 init_test(cx, |_| {});
18539 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18540 let base_text = indoc! {r#"
18541 struct Row;
18542 struct Row1;
18543 struct Row2;
18544
18545 struct Row4;
18546 struct Row5;
18547 struct Row6;
18548
18549 struct Row8;
18550 struct Row9;
18551 struct Row10;"#};
18552
18553 // Modification hunks behave the same as the addition ones.
18554 assert_hunk_revert(
18555 indoc! {r#"struct Row;
18556 struct Row1;
18557 struct Row33;
18558 ˇ
18559 struct Row4;
18560 struct Row5;
18561 struct Row6;
18562 ˇ
18563 struct Row99;
18564 struct Row9;
18565 struct Row10;"#},
18566 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18567 indoc! {r#"struct Row;
18568 struct Row1;
18569 struct Row33;
18570 ˇ
18571 struct Row4;
18572 struct Row5;
18573 struct Row6;
18574 ˇ
18575 struct Row99;
18576 struct Row9;
18577 struct Row10;"#},
18578 base_text,
18579 &mut cx,
18580 );
18581 assert_hunk_revert(
18582 indoc! {r#"struct Row;
18583 struct Row1;
18584 struct Row33;
18585 «ˇ
18586 struct Row4;
18587 struct» Row5;
18588 «struct Row6;
18589 ˇ»
18590 struct Row99;
18591 struct Row9;
18592 struct Row10;"#},
18593 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18594 indoc! {r#"struct Row;
18595 struct Row1;
18596 struct Row33;
18597 «ˇ
18598 struct Row4;
18599 struct» Row5;
18600 «struct Row6;
18601 ˇ»
18602 struct Row99;
18603 struct Row9;
18604 struct Row10;"#},
18605 base_text,
18606 &mut cx,
18607 );
18608
18609 assert_hunk_revert(
18610 indoc! {r#"ˇstruct Row1.1;
18611 struct Row1;
18612 «ˇstr»uct Row22;
18613
18614 struct ˇRow44;
18615 struct Row5;
18616 struct «Rˇ»ow66;ˇ
18617
18618 «struˇ»ct Row88;
18619 struct Row9;
18620 struct Row1011;ˇ"#},
18621 vec![
18622 DiffHunkStatusKind::Modified,
18623 DiffHunkStatusKind::Modified,
18624 DiffHunkStatusKind::Modified,
18625 DiffHunkStatusKind::Modified,
18626 DiffHunkStatusKind::Modified,
18627 DiffHunkStatusKind::Modified,
18628 ],
18629 indoc! {r#"struct Row;
18630 ˇstruct Row1;
18631 struct Row2;
18632 ˇ
18633 struct Row4;
18634 ˇstruct Row5;
18635 struct Row6;
18636 ˇ
18637 struct Row8;
18638 ˇstruct Row9;
18639 struct Row10;ˇ"#},
18640 base_text,
18641 &mut cx,
18642 );
18643}
18644
18645#[gpui::test]
18646async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18647 init_test(cx, |_| {});
18648 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18649 let base_text = indoc! {r#"
18650 one
18651
18652 two
18653 three
18654 "#};
18655
18656 cx.set_head_text(base_text);
18657 cx.set_state("\nˇ\n");
18658 cx.executor().run_until_parked();
18659 cx.update_editor(|editor, _window, cx| {
18660 editor.expand_selected_diff_hunks(cx);
18661 });
18662 cx.executor().run_until_parked();
18663 cx.update_editor(|editor, window, cx| {
18664 editor.backspace(&Default::default(), window, cx);
18665 });
18666 cx.run_until_parked();
18667 cx.assert_state_with_diff(
18668 indoc! {r#"
18669
18670 - two
18671 - threeˇ
18672 +
18673 "#}
18674 .to_string(),
18675 );
18676}
18677
18678#[gpui::test]
18679async fn test_deletion_reverts(cx: &mut TestAppContext) {
18680 init_test(cx, |_| {});
18681 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18682 let base_text = indoc! {r#"struct Row;
18683struct Row1;
18684struct Row2;
18685
18686struct Row4;
18687struct Row5;
18688struct Row6;
18689
18690struct Row8;
18691struct Row9;
18692struct Row10;"#};
18693
18694 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18695 assert_hunk_revert(
18696 indoc! {r#"struct Row;
18697 struct Row2;
18698
18699 ˇstruct Row4;
18700 struct Row5;
18701 struct Row6;
18702 ˇ
18703 struct Row8;
18704 struct Row10;"#},
18705 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18706 indoc! {r#"struct Row;
18707 struct Row2;
18708
18709 ˇstruct Row4;
18710 struct Row5;
18711 struct Row6;
18712 ˇ
18713 struct Row8;
18714 struct Row10;"#},
18715 base_text,
18716 &mut cx,
18717 );
18718 assert_hunk_revert(
18719 indoc! {r#"struct Row;
18720 struct Row2;
18721
18722 «ˇstruct Row4;
18723 struct» Row5;
18724 «struct Row6;
18725 ˇ»
18726 struct Row8;
18727 struct Row10;"#},
18728 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18729 indoc! {r#"struct Row;
18730 struct Row2;
18731
18732 «ˇstruct Row4;
18733 struct» Row5;
18734 «struct Row6;
18735 ˇ»
18736 struct Row8;
18737 struct Row10;"#},
18738 base_text,
18739 &mut cx,
18740 );
18741
18742 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18743 assert_hunk_revert(
18744 indoc! {r#"struct Row;
18745 ˇstruct Row2;
18746
18747 struct Row4;
18748 struct Row5;
18749 struct Row6;
18750
18751 struct Row8;ˇ
18752 struct Row10;"#},
18753 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18754 indoc! {r#"struct Row;
18755 struct Row1;
18756 ˇstruct Row2;
18757
18758 struct Row4;
18759 struct Row5;
18760 struct Row6;
18761
18762 struct Row8;ˇ
18763 struct Row9;
18764 struct Row10;"#},
18765 base_text,
18766 &mut cx,
18767 );
18768 assert_hunk_revert(
18769 indoc! {r#"struct Row;
18770 struct Row2«ˇ;
18771 struct Row4;
18772 struct» Row5;
18773 «struct Row6;
18774
18775 struct Row8;ˇ»
18776 struct Row10;"#},
18777 vec![
18778 DiffHunkStatusKind::Deleted,
18779 DiffHunkStatusKind::Deleted,
18780 DiffHunkStatusKind::Deleted,
18781 ],
18782 indoc! {r#"struct Row;
18783 struct Row1;
18784 struct Row2«ˇ;
18785
18786 struct Row4;
18787 struct» Row5;
18788 «struct Row6;
18789
18790 struct Row8;ˇ»
18791 struct Row9;
18792 struct Row10;"#},
18793 base_text,
18794 &mut cx,
18795 );
18796}
18797
18798#[gpui::test]
18799async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18800 init_test(cx, |_| {});
18801
18802 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18803 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18804 let base_text_3 =
18805 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18806
18807 let text_1 = edit_first_char_of_every_line(base_text_1);
18808 let text_2 = edit_first_char_of_every_line(base_text_2);
18809 let text_3 = edit_first_char_of_every_line(base_text_3);
18810
18811 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18812 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18813 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18814
18815 let multibuffer = cx.new(|cx| {
18816 let mut multibuffer = MultiBuffer::new(ReadWrite);
18817 multibuffer.push_excerpts(
18818 buffer_1.clone(),
18819 [
18820 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18821 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18822 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18823 ],
18824 cx,
18825 );
18826 multibuffer.push_excerpts(
18827 buffer_2.clone(),
18828 [
18829 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18830 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18831 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18832 ],
18833 cx,
18834 );
18835 multibuffer.push_excerpts(
18836 buffer_3.clone(),
18837 [
18838 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18839 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18840 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18841 ],
18842 cx,
18843 );
18844 multibuffer
18845 });
18846
18847 let fs = FakeFs::new(cx.executor());
18848 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18849 let (editor, cx) = cx
18850 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18851 editor.update_in(cx, |editor, _window, cx| {
18852 for (buffer, diff_base) in [
18853 (buffer_1.clone(), base_text_1),
18854 (buffer_2.clone(), base_text_2),
18855 (buffer_3.clone(), base_text_3),
18856 ] {
18857 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18858 editor
18859 .buffer
18860 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18861 }
18862 });
18863 cx.executor().run_until_parked();
18864
18865 editor.update_in(cx, |editor, window, cx| {
18866 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}");
18867 editor.select_all(&SelectAll, window, cx);
18868 editor.git_restore(&Default::default(), window, cx);
18869 });
18870 cx.executor().run_until_parked();
18871
18872 // When all ranges are selected, all buffer hunks are reverted.
18873 editor.update(cx, |editor, cx| {
18874 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");
18875 });
18876 buffer_1.update(cx, |buffer, _| {
18877 assert_eq!(buffer.text(), base_text_1);
18878 });
18879 buffer_2.update(cx, |buffer, _| {
18880 assert_eq!(buffer.text(), base_text_2);
18881 });
18882 buffer_3.update(cx, |buffer, _| {
18883 assert_eq!(buffer.text(), base_text_3);
18884 });
18885
18886 editor.update_in(cx, |editor, window, cx| {
18887 editor.undo(&Default::default(), window, cx);
18888 });
18889
18890 editor.update_in(cx, |editor, window, cx| {
18891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18892 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18893 });
18894 editor.git_restore(&Default::default(), window, cx);
18895 });
18896
18897 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18898 // but not affect buffer_2 and its related excerpts.
18899 editor.update(cx, |editor, cx| {
18900 assert_eq!(
18901 editor.text(cx),
18902 "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}"
18903 );
18904 });
18905 buffer_1.update(cx, |buffer, _| {
18906 assert_eq!(buffer.text(), base_text_1);
18907 });
18908 buffer_2.update(cx, |buffer, _| {
18909 assert_eq!(
18910 buffer.text(),
18911 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18912 );
18913 });
18914 buffer_3.update(cx, |buffer, _| {
18915 assert_eq!(
18916 buffer.text(),
18917 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18918 );
18919 });
18920
18921 fn edit_first_char_of_every_line(text: &str) -> String {
18922 text.split('\n')
18923 .map(|line| format!("X{}", &line[1..]))
18924 .collect::<Vec<_>>()
18925 .join("\n")
18926 }
18927}
18928
18929#[gpui::test]
18930async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18931 init_test(cx, |_| {});
18932
18933 let cols = 4;
18934 let rows = 10;
18935 let sample_text_1 = sample_text(rows, cols, 'a');
18936 assert_eq!(
18937 sample_text_1,
18938 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18939 );
18940 let sample_text_2 = sample_text(rows, cols, 'l');
18941 assert_eq!(
18942 sample_text_2,
18943 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18944 );
18945 let sample_text_3 = sample_text(rows, cols, 'v');
18946 assert_eq!(
18947 sample_text_3,
18948 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18949 );
18950
18951 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18952 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18953 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18954
18955 let multi_buffer = cx.new(|cx| {
18956 let mut multibuffer = MultiBuffer::new(ReadWrite);
18957 multibuffer.push_excerpts(
18958 buffer_1.clone(),
18959 [
18960 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18961 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18962 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18963 ],
18964 cx,
18965 );
18966 multibuffer.push_excerpts(
18967 buffer_2.clone(),
18968 [
18969 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18970 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18971 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18972 ],
18973 cx,
18974 );
18975 multibuffer.push_excerpts(
18976 buffer_3.clone(),
18977 [
18978 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18979 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18980 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18981 ],
18982 cx,
18983 );
18984 multibuffer
18985 });
18986
18987 let fs = FakeFs::new(cx.executor());
18988 fs.insert_tree(
18989 "/a",
18990 json!({
18991 "main.rs": sample_text_1,
18992 "other.rs": sample_text_2,
18993 "lib.rs": sample_text_3,
18994 }),
18995 )
18996 .await;
18997 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18998 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18999 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19000 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19001 Editor::new(
19002 EditorMode::full(),
19003 multi_buffer,
19004 Some(project.clone()),
19005 window,
19006 cx,
19007 )
19008 });
19009 let multibuffer_item_id = workspace
19010 .update(cx, |workspace, window, cx| {
19011 assert!(
19012 workspace.active_item(cx).is_none(),
19013 "active item should be None before the first item is added"
19014 );
19015 workspace.add_item_to_active_pane(
19016 Box::new(multi_buffer_editor.clone()),
19017 None,
19018 true,
19019 window,
19020 cx,
19021 );
19022 let active_item = workspace
19023 .active_item(cx)
19024 .expect("should have an active item after adding the multi buffer");
19025 assert_eq!(
19026 active_item.buffer_kind(cx),
19027 ItemBufferKind::Multibuffer,
19028 "A multi buffer was expected to active after adding"
19029 );
19030 active_item.item_id()
19031 })
19032 .unwrap();
19033 cx.executor().run_until_parked();
19034
19035 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19036 editor.change_selections(
19037 SelectionEffects::scroll(Autoscroll::Next),
19038 window,
19039 cx,
19040 |s| s.select_ranges(Some(1..2)),
19041 );
19042 editor.open_excerpts(&OpenExcerpts, window, cx);
19043 });
19044 cx.executor().run_until_parked();
19045 let first_item_id = workspace
19046 .update(cx, |workspace, window, cx| {
19047 let active_item = workspace
19048 .active_item(cx)
19049 .expect("should have an active item after navigating into the 1st buffer");
19050 let first_item_id = active_item.item_id();
19051 assert_ne!(
19052 first_item_id, multibuffer_item_id,
19053 "Should navigate into the 1st buffer and activate it"
19054 );
19055 assert_eq!(
19056 active_item.buffer_kind(cx),
19057 ItemBufferKind::Singleton,
19058 "New active item should be a singleton buffer"
19059 );
19060 assert_eq!(
19061 active_item
19062 .act_as::<Editor>(cx)
19063 .expect("should have navigated into an editor for the 1st buffer")
19064 .read(cx)
19065 .text(cx),
19066 sample_text_1
19067 );
19068
19069 workspace
19070 .go_back(workspace.active_pane().downgrade(), window, cx)
19071 .detach_and_log_err(cx);
19072
19073 first_item_id
19074 })
19075 .unwrap();
19076 cx.executor().run_until_parked();
19077 workspace
19078 .update(cx, |workspace, _, cx| {
19079 let active_item = workspace
19080 .active_item(cx)
19081 .expect("should have an active item after navigating back");
19082 assert_eq!(
19083 active_item.item_id(),
19084 multibuffer_item_id,
19085 "Should navigate back to the multi buffer"
19086 );
19087 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19088 })
19089 .unwrap();
19090
19091 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19092 editor.change_selections(
19093 SelectionEffects::scroll(Autoscroll::Next),
19094 window,
19095 cx,
19096 |s| s.select_ranges(Some(39..40)),
19097 );
19098 editor.open_excerpts(&OpenExcerpts, window, cx);
19099 });
19100 cx.executor().run_until_parked();
19101 let second_item_id = workspace
19102 .update(cx, |workspace, window, cx| {
19103 let active_item = workspace
19104 .active_item(cx)
19105 .expect("should have an active item after navigating into the 2nd buffer");
19106 let second_item_id = active_item.item_id();
19107 assert_ne!(
19108 second_item_id, multibuffer_item_id,
19109 "Should navigate away from the multibuffer"
19110 );
19111 assert_ne!(
19112 second_item_id, first_item_id,
19113 "Should navigate into the 2nd buffer and activate it"
19114 );
19115 assert_eq!(
19116 active_item.buffer_kind(cx),
19117 ItemBufferKind::Singleton,
19118 "New active item should be a singleton buffer"
19119 );
19120 assert_eq!(
19121 active_item
19122 .act_as::<Editor>(cx)
19123 .expect("should have navigated into an editor")
19124 .read(cx)
19125 .text(cx),
19126 sample_text_2
19127 );
19128
19129 workspace
19130 .go_back(workspace.active_pane().downgrade(), window, cx)
19131 .detach_and_log_err(cx);
19132
19133 second_item_id
19134 })
19135 .unwrap();
19136 cx.executor().run_until_parked();
19137 workspace
19138 .update(cx, |workspace, _, cx| {
19139 let active_item = workspace
19140 .active_item(cx)
19141 .expect("should have an active item after navigating back from the 2nd buffer");
19142 assert_eq!(
19143 active_item.item_id(),
19144 multibuffer_item_id,
19145 "Should navigate back from the 2nd buffer to the multi buffer"
19146 );
19147 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19148 })
19149 .unwrap();
19150
19151 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19152 editor.change_selections(
19153 SelectionEffects::scroll(Autoscroll::Next),
19154 window,
19155 cx,
19156 |s| s.select_ranges(Some(70..70)),
19157 );
19158 editor.open_excerpts(&OpenExcerpts, window, cx);
19159 });
19160 cx.executor().run_until_parked();
19161 workspace
19162 .update(cx, |workspace, window, cx| {
19163 let active_item = workspace
19164 .active_item(cx)
19165 .expect("should have an active item after navigating into the 3rd buffer");
19166 let third_item_id = active_item.item_id();
19167 assert_ne!(
19168 third_item_id, multibuffer_item_id,
19169 "Should navigate into the 3rd buffer and activate it"
19170 );
19171 assert_ne!(third_item_id, first_item_id);
19172 assert_ne!(third_item_id, second_item_id);
19173 assert_eq!(
19174 active_item.buffer_kind(cx),
19175 ItemBufferKind::Singleton,
19176 "New active item should be a singleton buffer"
19177 );
19178 assert_eq!(
19179 active_item
19180 .act_as::<Editor>(cx)
19181 .expect("should have navigated into an editor")
19182 .read(cx)
19183 .text(cx),
19184 sample_text_3
19185 );
19186
19187 workspace
19188 .go_back(workspace.active_pane().downgrade(), window, cx)
19189 .detach_and_log_err(cx);
19190 })
19191 .unwrap();
19192 cx.executor().run_until_parked();
19193 workspace
19194 .update(cx, |workspace, _, cx| {
19195 let active_item = workspace
19196 .active_item(cx)
19197 .expect("should have an active item after navigating back from the 3rd buffer");
19198 assert_eq!(
19199 active_item.item_id(),
19200 multibuffer_item_id,
19201 "Should navigate back from the 3rd buffer to the multi buffer"
19202 );
19203 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19204 })
19205 .unwrap();
19206}
19207
19208#[gpui::test]
19209async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19210 init_test(cx, |_| {});
19211
19212 let mut cx = EditorTestContext::new(cx).await;
19213
19214 let diff_base = r#"
19215 use some::mod;
19216
19217 const A: u32 = 42;
19218
19219 fn main() {
19220 println!("hello");
19221
19222 println!("world");
19223 }
19224 "#
19225 .unindent();
19226
19227 cx.set_state(
19228 &r#"
19229 use some::modified;
19230
19231 ˇ
19232 fn main() {
19233 println!("hello there");
19234
19235 println!("around the");
19236 println!("world");
19237 }
19238 "#
19239 .unindent(),
19240 );
19241
19242 cx.set_head_text(&diff_base);
19243 executor.run_until_parked();
19244
19245 cx.update_editor(|editor, window, cx| {
19246 editor.go_to_next_hunk(&GoToHunk, window, cx);
19247 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19248 });
19249 executor.run_until_parked();
19250 cx.assert_state_with_diff(
19251 r#"
19252 use some::modified;
19253
19254
19255 fn main() {
19256 - println!("hello");
19257 + ˇ println!("hello there");
19258
19259 println!("around the");
19260 println!("world");
19261 }
19262 "#
19263 .unindent(),
19264 );
19265
19266 cx.update_editor(|editor, window, cx| {
19267 for _ in 0..2 {
19268 editor.go_to_next_hunk(&GoToHunk, window, cx);
19269 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19270 }
19271 });
19272 executor.run_until_parked();
19273 cx.assert_state_with_diff(
19274 r#"
19275 - use some::mod;
19276 + ˇuse some::modified;
19277
19278
19279 fn main() {
19280 - println!("hello");
19281 + println!("hello there");
19282
19283 + println!("around the");
19284 println!("world");
19285 }
19286 "#
19287 .unindent(),
19288 );
19289
19290 cx.update_editor(|editor, window, cx| {
19291 editor.go_to_next_hunk(&GoToHunk, window, cx);
19292 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19293 });
19294 executor.run_until_parked();
19295 cx.assert_state_with_diff(
19296 r#"
19297 - use some::mod;
19298 + use some::modified;
19299
19300 - const A: u32 = 42;
19301 ˇ
19302 fn main() {
19303 - println!("hello");
19304 + println!("hello there");
19305
19306 + println!("around the");
19307 println!("world");
19308 }
19309 "#
19310 .unindent(),
19311 );
19312
19313 cx.update_editor(|editor, window, cx| {
19314 editor.cancel(&Cancel, window, cx);
19315 });
19316
19317 cx.assert_state_with_diff(
19318 r#"
19319 use some::modified;
19320
19321 ˇ
19322 fn main() {
19323 println!("hello there");
19324
19325 println!("around the");
19326 println!("world");
19327 }
19328 "#
19329 .unindent(),
19330 );
19331}
19332
19333#[gpui::test]
19334async fn test_diff_base_change_with_expanded_diff_hunks(
19335 executor: BackgroundExecutor,
19336 cx: &mut TestAppContext,
19337) {
19338 init_test(cx, |_| {});
19339
19340 let mut cx = EditorTestContext::new(cx).await;
19341
19342 let diff_base = r#"
19343 use some::mod1;
19344 use some::mod2;
19345
19346 const A: u32 = 42;
19347 const B: u32 = 42;
19348 const C: u32 = 42;
19349
19350 fn main() {
19351 println!("hello");
19352
19353 println!("world");
19354 }
19355 "#
19356 .unindent();
19357
19358 cx.set_state(
19359 &r#"
19360 use some::mod2;
19361
19362 const A: u32 = 42;
19363 const C: u32 = 42;
19364
19365 fn main(ˇ) {
19366 //println!("hello");
19367
19368 println!("world");
19369 //
19370 //
19371 }
19372 "#
19373 .unindent(),
19374 );
19375
19376 cx.set_head_text(&diff_base);
19377 executor.run_until_parked();
19378
19379 cx.update_editor(|editor, window, cx| {
19380 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19381 });
19382 executor.run_until_parked();
19383 cx.assert_state_with_diff(
19384 r#"
19385 - use some::mod1;
19386 use some::mod2;
19387
19388 const A: u32 = 42;
19389 - const B: u32 = 42;
19390 const C: u32 = 42;
19391
19392 fn main(ˇ) {
19393 - println!("hello");
19394 + //println!("hello");
19395
19396 println!("world");
19397 + //
19398 + //
19399 }
19400 "#
19401 .unindent(),
19402 );
19403
19404 cx.set_head_text("new diff base!");
19405 executor.run_until_parked();
19406 cx.assert_state_with_diff(
19407 r#"
19408 - new diff base!
19409 + use some::mod2;
19410 +
19411 + const A: u32 = 42;
19412 + const C: u32 = 42;
19413 +
19414 + fn main(ˇ) {
19415 + //println!("hello");
19416 +
19417 + println!("world");
19418 + //
19419 + //
19420 + }
19421 "#
19422 .unindent(),
19423 );
19424}
19425
19426#[gpui::test]
19427async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19428 init_test(cx, |_| {});
19429
19430 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19431 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19432 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19433 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19434 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19435 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19436
19437 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19438 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19439 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19440
19441 let multi_buffer = cx.new(|cx| {
19442 let mut multibuffer = MultiBuffer::new(ReadWrite);
19443 multibuffer.push_excerpts(
19444 buffer_1.clone(),
19445 [
19446 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19447 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19448 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19449 ],
19450 cx,
19451 );
19452 multibuffer.push_excerpts(
19453 buffer_2.clone(),
19454 [
19455 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19456 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19457 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19458 ],
19459 cx,
19460 );
19461 multibuffer.push_excerpts(
19462 buffer_3.clone(),
19463 [
19464 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19465 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19466 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19467 ],
19468 cx,
19469 );
19470 multibuffer
19471 });
19472
19473 let editor =
19474 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19475 editor
19476 .update(cx, |editor, _window, cx| {
19477 for (buffer, diff_base) in [
19478 (buffer_1.clone(), file_1_old),
19479 (buffer_2.clone(), file_2_old),
19480 (buffer_3.clone(), file_3_old),
19481 ] {
19482 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19483 editor
19484 .buffer
19485 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19486 }
19487 })
19488 .unwrap();
19489
19490 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19491 cx.run_until_parked();
19492
19493 cx.assert_editor_state(
19494 &"
19495 ˇaaa
19496 ccc
19497 ddd
19498
19499 ggg
19500 hhh
19501
19502
19503 lll
19504 mmm
19505 NNN
19506
19507 qqq
19508 rrr
19509
19510 uuu
19511 111
19512 222
19513 333
19514
19515 666
19516 777
19517
19518 000
19519 !!!"
19520 .unindent(),
19521 );
19522
19523 cx.update_editor(|editor, window, cx| {
19524 editor.select_all(&SelectAll, window, cx);
19525 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19526 });
19527 cx.executor().run_until_parked();
19528
19529 cx.assert_state_with_diff(
19530 "
19531 «aaa
19532 - bbb
19533 ccc
19534 ddd
19535
19536 ggg
19537 hhh
19538
19539
19540 lll
19541 mmm
19542 - nnn
19543 + NNN
19544
19545 qqq
19546 rrr
19547
19548 uuu
19549 111
19550 222
19551 333
19552
19553 + 666
19554 777
19555
19556 000
19557 !!!ˇ»"
19558 .unindent(),
19559 );
19560}
19561
19562#[gpui::test]
19563async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19564 init_test(cx, |_| {});
19565
19566 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19567 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19568
19569 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19570 let multi_buffer = cx.new(|cx| {
19571 let mut multibuffer = MultiBuffer::new(ReadWrite);
19572 multibuffer.push_excerpts(
19573 buffer.clone(),
19574 [
19575 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19576 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19577 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19578 ],
19579 cx,
19580 );
19581 multibuffer
19582 });
19583
19584 let editor =
19585 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19586 editor
19587 .update(cx, |editor, _window, cx| {
19588 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19589 editor
19590 .buffer
19591 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19592 })
19593 .unwrap();
19594
19595 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19596 cx.run_until_parked();
19597
19598 cx.update_editor(|editor, window, cx| {
19599 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19600 });
19601 cx.executor().run_until_parked();
19602
19603 // When the start of a hunk coincides with the start of its excerpt,
19604 // the hunk is expanded. When the start of a hunk is earlier than
19605 // the start of its excerpt, the hunk is not expanded.
19606 cx.assert_state_with_diff(
19607 "
19608 ˇaaa
19609 - bbb
19610 + BBB
19611
19612 - ddd
19613 - eee
19614 + DDD
19615 + EEE
19616 fff
19617
19618 iii
19619 "
19620 .unindent(),
19621 );
19622}
19623
19624#[gpui::test]
19625async fn test_edits_around_expanded_insertion_hunks(
19626 executor: BackgroundExecutor,
19627 cx: &mut TestAppContext,
19628) {
19629 init_test(cx, |_| {});
19630
19631 let mut cx = EditorTestContext::new(cx).await;
19632
19633 let diff_base = r#"
19634 use some::mod1;
19635 use some::mod2;
19636
19637 const A: u32 = 42;
19638
19639 fn main() {
19640 println!("hello");
19641
19642 println!("world");
19643 }
19644 "#
19645 .unindent();
19646 executor.run_until_parked();
19647 cx.set_state(
19648 &r#"
19649 use some::mod1;
19650 use some::mod2;
19651
19652 const A: u32 = 42;
19653 const B: u32 = 42;
19654 const C: u32 = 42;
19655 ˇ
19656
19657 fn main() {
19658 println!("hello");
19659
19660 println!("world");
19661 }
19662 "#
19663 .unindent(),
19664 );
19665
19666 cx.set_head_text(&diff_base);
19667 executor.run_until_parked();
19668
19669 cx.update_editor(|editor, window, cx| {
19670 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19671 });
19672 executor.run_until_parked();
19673
19674 cx.assert_state_with_diff(
19675 r#"
19676 use some::mod1;
19677 use some::mod2;
19678
19679 const A: u32 = 42;
19680 + const B: u32 = 42;
19681 + const C: u32 = 42;
19682 + ˇ
19683
19684 fn main() {
19685 println!("hello");
19686
19687 println!("world");
19688 }
19689 "#
19690 .unindent(),
19691 );
19692
19693 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19694 executor.run_until_parked();
19695
19696 cx.assert_state_with_diff(
19697 r#"
19698 use some::mod1;
19699 use some::mod2;
19700
19701 const A: u32 = 42;
19702 + const B: u32 = 42;
19703 + const C: u32 = 42;
19704 + const D: u32 = 42;
19705 + ˇ
19706
19707 fn main() {
19708 println!("hello");
19709
19710 println!("world");
19711 }
19712 "#
19713 .unindent(),
19714 );
19715
19716 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19717 executor.run_until_parked();
19718
19719 cx.assert_state_with_diff(
19720 r#"
19721 use some::mod1;
19722 use some::mod2;
19723
19724 const A: u32 = 42;
19725 + const B: u32 = 42;
19726 + const C: u32 = 42;
19727 + const D: u32 = 42;
19728 + const E: u32 = 42;
19729 + ˇ
19730
19731 fn main() {
19732 println!("hello");
19733
19734 println!("world");
19735 }
19736 "#
19737 .unindent(),
19738 );
19739
19740 cx.update_editor(|editor, window, cx| {
19741 editor.delete_line(&DeleteLine, window, cx);
19742 });
19743 executor.run_until_parked();
19744
19745 cx.assert_state_with_diff(
19746 r#"
19747 use some::mod1;
19748 use some::mod2;
19749
19750 const A: u32 = 42;
19751 + const B: u32 = 42;
19752 + const C: u32 = 42;
19753 + const D: u32 = 42;
19754 + const E: u32 = 42;
19755 ˇ
19756 fn main() {
19757 println!("hello");
19758
19759 println!("world");
19760 }
19761 "#
19762 .unindent(),
19763 );
19764
19765 cx.update_editor(|editor, window, cx| {
19766 editor.move_up(&MoveUp, window, cx);
19767 editor.delete_line(&DeleteLine, window, cx);
19768 editor.move_up(&MoveUp, window, cx);
19769 editor.delete_line(&DeleteLine, window, cx);
19770 editor.move_up(&MoveUp, window, cx);
19771 editor.delete_line(&DeleteLine, window, cx);
19772 });
19773 executor.run_until_parked();
19774 cx.assert_state_with_diff(
19775 r#"
19776 use some::mod1;
19777 use some::mod2;
19778
19779 const A: u32 = 42;
19780 + const B: u32 = 42;
19781 ˇ
19782 fn main() {
19783 println!("hello");
19784
19785 println!("world");
19786 }
19787 "#
19788 .unindent(),
19789 );
19790
19791 cx.update_editor(|editor, window, cx| {
19792 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19793 editor.delete_line(&DeleteLine, window, cx);
19794 });
19795 executor.run_until_parked();
19796 cx.assert_state_with_diff(
19797 r#"
19798 ˇ
19799 fn main() {
19800 println!("hello");
19801
19802 println!("world");
19803 }
19804 "#
19805 .unindent(),
19806 );
19807}
19808
19809#[gpui::test]
19810async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19811 init_test(cx, |_| {});
19812
19813 let mut cx = EditorTestContext::new(cx).await;
19814 cx.set_head_text(indoc! { "
19815 one
19816 two
19817 three
19818 four
19819 five
19820 "
19821 });
19822 cx.set_state(indoc! { "
19823 one
19824 ˇthree
19825 five
19826 "});
19827 cx.run_until_parked();
19828 cx.update_editor(|editor, window, cx| {
19829 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19830 });
19831 cx.assert_state_with_diff(
19832 indoc! { "
19833 one
19834 - two
19835 ˇthree
19836 - four
19837 five
19838 "}
19839 .to_string(),
19840 );
19841 cx.update_editor(|editor, window, cx| {
19842 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19843 });
19844
19845 cx.assert_state_with_diff(
19846 indoc! { "
19847 one
19848 ˇthree
19849 five
19850 "}
19851 .to_string(),
19852 );
19853
19854 cx.set_state(indoc! { "
19855 one
19856 ˇTWO
19857 three
19858 four
19859 five
19860 "});
19861 cx.run_until_parked();
19862 cx.update_editor(|editor, window, cx| {
19863 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19864 });
19865
19866 cx.assert_state_with_diff(
19867 indoc! { "
19868 one
19869 - two
19870 + ˇTWO
19871 three
19872 four
19873 five
19874 "}
19875 .to_string(),
19876 );
19877 cx.update_editor(|editor, window, cx| {
19878 editor.move_up(&Default::default(), window, cx);
19879 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19880 });
19881 cx.assert_state_with_diff(
19882 indoc! { "
19883 one
19884 ˇTWO
19885 three
19886 four
19887 five
19888 "}
19889 .to_string(),
19890 );
19891}
19892
19893#[gpui::test]
19894async fn test_edits_around_expanded_deletion_hunks(
19895 executor: BackgroundExecutor,
19896 cx: &mut TestAppContext,
19897) {
19898 init_test(cx, |_| {});
19899
19900 let mut cx = EditorTestContext::new(cx).await;
19901
19902 let diff_base = r#"
19903 use some::mod1;
19904 use some::mod2;
19905
19906 const A: u32 = 42;
19907 const B: u32 = 42;
19908 const C: u32 = 42;
19909
19910
19911 fn main() {
19912 println!("hello");
19913
19914 println!("world");
19915 }
19916 "#
19917 .unindent();
19918 executor.run_until_parked();
19919 cx.set_state(
19920 &r#"
19921 use some::mod1;
19922 use some::mod2;
19923
19924 ˇconst B: u32 = 42;
19925 const C: u32 = 42;
19926
19927
19928 fn main() {
19929 println!("hello");
19930
19931 println!("world");
19932 }
19933 "#
19934 .unindent(),
19935 );
19936
19937 cx.set_head_text(&diff_base);
19938 executor.run_until_parked();
19939
19940 cx.update_editor(|editor, window, cx| {
19941 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19942 });
19943 executor.run_until_parked();
19944
19945 cx.assert_state_with_diff(
19946 r#"
19947 use some::mod1;
19948 use some::mod2;
19949
19950 - const A: u32 = 42;
19951 ˇconst B: u32 = 42;
19952 const C: u32 = 42;
19953
19954
19955 fn main() {
19956 println!("hello");
19957
19958 println!("world");
19959 }
19960 "#
19961 .unindent(),
19962 );
19963
19964 cx.update_editor(|editor, window, cx| {
19965 editor.delete_line(&DeleteLine, window, cx);
19966 });
19967 executor.run_until_parked();
19968 cx.assert_state_with_diff(
19969 r#"
19970 use some::mod1;
19971 use some::mod2;
19972
19973 - const A: u32 = 42;
19974 - const B: u32 = 42;
19975 ˇconst C: u32 = 42;
19976
19977
19978 fn main() {
19979 println!("hello");
19980
19981 println!("world");
19982 }
19983 "#
19984 .unindent(),
19985 );
19986
19987 cx.update_editor(|editor, window, cx| {
19988 editor.delete_line(&DeleteLine, window, cx);
19989 });
19990 executor.run_until_parked();
19991 cx.assert_state_with_diff(
19992 r#"
19993 use some::mod1;
19994 use some::mod2;
19995
19996 - const A: u32 = 42;
19997 - const B: u32 = 42;
19998 - const C: u32 = 42;
19999 ˇ
20000
20001 fn main() {
20002 println!("hello");
20003
20004 println!("world");
20005 }
20006 "#
20007 .unindent(),
20008 );
20009
20010 cx.update_editor(|editor, window, cx| {
20011 editor.handle_input("replacement", window, cx);
20012 });
20013 executor.run_until_parked();
20014 cx.assert_state_with_diff(
20015 r#"
20016 use some::mod1;
20017 use some::mod2;
20018
20019 - const A: u32 = 42;
20020 - const B: u32 = 42;
20021 - const C: u32 = 42;
20022 -
20023 + replacementˇ
20024
20025 fn main() {
20026 println!("hello");
20027
20028 println!("world");
20029 }
20030 "#
20031 .unindent(),
20032 );
20033}
20034
20035#[gpui::test]
20036async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20037 init_test(cx, |_| {});
20038
20039 let mut cx = EditorTestContext::new(cx).await;
20040
20041 let base_text = r#"
20042 one
20043 two
20044 three
20045 four
20046 five
20047 "#
20048 .unindent();
20049 executor.run_until_parked();
20050 cx.set_state(
20051 &r#"
20052 one
20053 two
20054 fˇour
20055 five
20056 "#
20057 .unindent(),
20058 );
20059
20060 cx.set_head_text(&base_text);
20061 executor.run_until_parked();
20062
20063 cx.update_editor(|editor, window, cx| {
20064 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20065 });
20066 executor.run_until_parked();
20067
20068 cx.assert_state_with_diff(
20069 r#"
20070 one
20071 two
20072 - three
20073 fˇour
20074 five
20075 "#
20076 .unindent(),
20077 );
20078
20079 cx.update_editor(|editor, window, cx| {
20080 editor.backspace(&Backspace, window, cx);
20081 editor.backspace(&Backspace, window, cx);
20082 });
20083 executor.run_until_parked();
20084 cx.assert_state_with_diff(
20085 r#"
20086 one
20087 two
20088 - threeˇ
20089 - four
20090 + our
20091 five
20092 "#
20093 .unindent(),
20094 );
20095}
20096
20097#[gpui::test]
20098async fn test_edit_after_expanded_modification_hunk(
20099 executor: BackgroundExecutor,
20100 cx: &mut TestAppContext,
20101) {
20102 init_test(cx, |_| {});
20103
20104 let mut cx = EditorTestContext::new(cx).await;
20105
20106 let diff_base = 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 const D: u32 = 42;
20114
20115
20116 fn main() {
20117 println!("hello");
20118
20119 println!("world");
20120 }"#
20121 .unindent();
20122
20123 cx.set_state(
20124 &r#"
20125 use some::mod1;
20126 use some::mod2;
20127
20128 const A: u32 = 42;
20129 const B: u32 = 42;
20130 const C: u32 = 43ˇ
20131 const D: u32 = 42;
20132
20133
20134 fn main() {
20135 println!("hello");
20136
20137 println!("world");
20138 }"#
20139 .unindent(),
20140 );
20141
20142 cx.set_head_text(&diff_base);
20143 executor.run_until_parked();
20144 cx.update_editor(|editor, window, cx| {
20145 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20146 });
20147 executor.run_until_parked();
20148
20149 cx.assert_state_with_diff(
20150 r#"
20151 use some::mod1;
20152 use some::mod2;
20153
20154 const A: u32 = 42;
20155 const B: u32 = 42;
20156 - const C: u32 = 42;
20157 + const C: u32 = 43ˇ
20158 const D: u32 = 42;
20159
20160
20161 fn main() {
20162 println!("hello");
20163
20164 println!("world");
20165 }"#
20166 .unindent(),
20167 );
20168
20169 cx.update_editor(|editor, window, cx| {
20170 editor.handle_input("\nnew_line\n", window, cx);
20171 });
20172 executor.run_until_parked();
20173
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 + const C: u32 = 43
20183 + new_line
20184 + ˇ
20185 const D: u32 = 42;
20186
20187
20188 fn main() {
20189 println!("hello");
20190
20191 println!("world");
20192 }"#
20193 .unindent(),
20194 );
20195}
20196
20197#[gpui::test]
20198async fn test_stage_and_unstage_added_file_hunk(
20199 executor: BackgroundExecutor,
20200 cx: &mut TestAppContext,
20201) {
20202 init_test(cx, |_| {});
20203
20204 let mut cx = EditorTestContext::new(cx).await;
20205 cx.update_editor(|editor, _, cx| {
20206 editor.set_expand_all_diff_hunks(cx);
20207 });
20208
20209 let working_copy = r#"
20210 ˇfn main() {
20211 println!("hello, world!");
20212 }
20213 "#
20214 .unindent();
20215
20216 cx.set_state(&working_copy);
20217 executor.run_until_parked();
20218
20219 cx.assert_state_with_diff(
20220 r#"
20221 + ˇfn main() {
20222 + println!("hello, world!");
20223 + }
20224 "#
20225 .unindent(),
20226 );
20227 cx.assert_index_text(None);
20228
20229 cx.update_editor(|editor, window, cx| {
20230 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20231 });
20232 executor.run_until_parked();
20233 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20234 cx.assert_state_with_diff(
20235 r#"
20236 + ˇfn main() {
20237 + println!("hello, world!");
20238 + }
20239 "#
20240 .unindent(),
20241 );
20242
20243 cx.update_editor(|editor, window, cx| {
20244 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20245 });
20246 executor.run_until_parked();
20247 cx.assert_index_text(None);
20248}
20249
20250async fn setup_indent_guides_editor(
20251 text: &str,
20252 cx: &mut TestAppContext,
20253) -> (BufferId, EditorTestContext) {
20254 init_test(cx, |_| {});
20255
20256 let mut cx = EditorTestContext::new(cx).await;
20257
20258 let buffer_id = cx.update_editor(|editor, window, cx| {
20259 editor.set_text(text, window, cx);
20260 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20261
20262 buffer_ids[0]
20263 });
20264
20265 (buffer_id, cx)
20266}
20267
20268fn assert_indent_guides(
20269 range: Range<u32>,
20270 expected: Vec<IndentGuide>,
20271 active_indices: Option<Vec<usize>>,
20272 cx: &mut EditorTestContext,
20273) {
20274 let indent_guides = cx.update_editor(|editor, window, cx| {
20275 let snapshot = editor.snapshot(window, cx).display_snapshot;
20276 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20277 editor,
20278 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20279 true,
20280 &snapshot,
20281 cx,
20282 );
20283
20284 indent_guides.sort_by(|a, b| {
20285 a.depth.cmp(&b.depth).then(
20286 a.start_row
20287 .cmp(&b.start_row)
20288 .then(a.end_row.cmp(&b.end_row)),
20289 )
20290 });
20291 indent_guides
20292 });
20293
20294 if let Some(expected) = active_indices {
20295 let active_indices = cx.update_editor(|editor, window, cx| {
20296 let snapshot = editor.snapshot(window, cx).display_snapshot;
20297 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20298 });
20299
20300 assert_eq!(
20301 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20302 expected,
20303 "Active indent guide indices do not match"
20304 );
20305 }
20306
20307 assert_eq!(indent_guides, expected, "Indent guides do not match");
20308}
20309
20310fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20311 IndentGuide {
20312 buffer_id,
20313 start_row: MultiBufferRow(start_row),
20314 end_row: MultiBufferRow(end_row),
20315 depth,
20316 tab_size: 4,
20317 settings: IndentGuideSettings {
20318 enabled: true,
20319 line_width: 1,
20320 active_line_width: 1,
20321 coloring: IndentGuideColoring::default(),
20322 background_coloring: IndentGuideBackgroundColoring::default(),
20323 },
20324 }
20325}
20326
20327#[gpui::test]
20328async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20329 let (buffer_id, mut cx) = setup_indent_guides_editor(
20330 &"
20331 fn main() {
20332 let a = 1;
20333 }"
20334 .unindent(),
20335 cx,
20336 )
20337 .await;
20338
20339 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20340}
20341
20342#[gpui::test]
20343async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20344 let (buffer_id, mut cx) = setup_indent_guides_editor(
20345 &"
20346 fn main() {
20347 let a = 1;
20348 let b = 2;
20349 }"
20350 .unindent(),
20351 cx,
20352 )
20353 .await;
20354
20355 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20356}
20357
20358#[gpui::test]
20359async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20360 let (buffer_id, mut cx) = setup_indent_guides_editor(
20361 &"
20362 fn main() {
20363 let a = 1;
20364 if a == 3 {
20365 let b = 2;
20366 } else {
20367 let c = 3;
20368 }
20369 }"
20370 .unindent(),
20371 cx,
20372 )
20373 .await;
20374
20375 assert_indent_guides(
20376 0..8,
20377 vec![
20378 indent_guide(buffer_id, 1, 6, 0),
20379 indent_guide(buffer_id, 3, 3, 1),
20380 indent_guide(buffer_id, 5, 5, 1),
20381 ],
20382 None,
20383 &mut cx,
20384 );
20385}
20386
20387#[gpui::test]
20388async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20389 let (buffer_id, mut cx) = setup_indent_guides_editor(
20390 &"
20391 fn main() {
20392 let a = 1;
20393 let b = 2;
20394 let c = 3;
20395 }"
20396 .unindent(),
20397 cx,
20398 )
20399 .await;
20400
20401 assert_indent_guides(
20402 0..5,
20403 vec![
20404 indent_guide(buffer_id, 1, 3, 0),
20405 indent_guide(buffer_id, 2, 2, 1),
20406 ],
20407 None,
20408 &mut cx,
20409 );
20410}
20411
20412#[gpui::test]
20413async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20414 let (buffer_id, mut cx) = setup_indent_guides_editor(
20415 &"
20416 fn main() {
20417 let a = 1;
20418
20419 let c = 3;
20420 }"
20421 .unindent(),
20422 cx,
20423 )
20424 .await;
20425
20426 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20427}
20428
20429#[gpui::test]
20430async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20431 let (buffer_id, mut cx) = setup_indent_guides_editor(
20432 &"
20433 fn main() {
20434 let a = 1;
20435
20436 let c = 3;
20437
20438 if a == 3 {
20439 let b = 2;
20440 } else {
20441 let c = 3;
20442 }
20443 }"
20444 .unindent(),
20445 cx,
20446 )
20447 .await;
20448
20449 assert_indent_guides(
20450 0..11,
20451 vec![
20452 indent_guide(buffer_id, 1, 9, 0),
20453 indent_guide(buffer_id, 6, 6, 1),
20454 indent_guide(buffer_id, 8, 8, 1),
20455 ],
20456 None,
20457 &mut cx,
20458 );
20459}
20460
20461#[gpui::test]
20462async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20463 let (buffer_id, mut cx) = setup_indent_guides_editor(
20464 &"
20465 fn main() {
20466 let a = 1;
20467
20468 let c = 3;
20469
20470 if a == 3 {
20471 let b = 2;
20472 } else {
20473 let c = 3;
20474 }
20475 }"
20476 .unindent(),
20477 cx,
20478 )
20479 .await;
20480
20481 assert_indent_guides(
20482 1..11,
20483 vec![
20484 indent_guide(buffer_id, 1, 9, 0),
20485 indent_guide(buffer_id, 6, 6, 1),
20486 indent_guide(buffer_id, 8, 8, 1),
20487 ],
20488 None,
20489 &mut cx,
20490 );
20491}
20492
20493#[gpui::test]
20494async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20495 let (buffer_id, mut cx) = setup_indent_guides_editor(
20496 &"
20497 fn main() {
20498 let a = 1;
20499
20500 let c = 3;
20501
20502 if a == 3 {
20503 let b = 2;
20504 } else {
20505 let c = 3;
20506 }
20507 }"
20508 .unindent(),
20509 cx,
20510 )
20511 .await;
20512
20513 assert_indent_guides(
20514 1..10,
20515 vec![
20516 indent_guide(buffer_id, 1, 9, 0),
20517 indent_guide(buffer_id, 6, 6, 1),
20518 indent_guide(buffer_id, 8, 8, 1),
20519 ],
20520 None,
20521 &mut cx,
20522 );
20523}
20524
20525#[gpui::test]
20526async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20527 let (buffer_id, mut cx) = setup_indent_guides_editor(
20528 &"
20529 fn main() {
20530 if a {
20531 b(
20532 c,
20533 d,
20534 )
20535 } else {
20536 e(
20537 f
20538 )
20539 }
20540 }"
20541 .unindent(),
20542 cx,
20543 )
20544 .await;
20545
20546 assert_indent_guides(
20547 0..11,
20548 vec![
20549 indent_guide(buffer_id, 1, 10, 0),
20550 indent_guide(buffer_id, 2, 5, 1),
20551 indent_guide(buffer_id, 7, 9, 1),
20552 indent_guide(buffer_id, 3, 4, 2),
20553 indent_guide(buffer_id, 8, 8, 2),
20554 ],
20555 None,
20556 &mut cx,
20557 );
20558
20559 cx.update_editor(|editor, window, cx| {
20560 editor.fold_at(MultiBufferRow(2), window, cx);
20561 assert_eq!(
20562 editor.display_text(cx),
20563 "
20564 fn main() {
20565 if a {
20566 b(⋯
20567 )
20568 } else {
20569 e(
20570 f
20571 )
20572 }
20573 }"
20574 .unindent()
20575 );
20576 });
20577
20578 assert_indent_guides(
20579 0..11,
20580 vec![
20581 indent_guide(buffer_id, 1, 10, 0),
20582 indent_guide(buffer_id, 2, 5, 1),
20583 indent_guide(buffer_id, 7, 9, 1),
20584 indent_guide(buffer_id, 8, 8, 2),
20585 ],
20586 None,
20587 &mut cx,
20588 );
20589}
20590
20591#[gpui::test]
20592async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20593 let (buffer_id, mut cx) = setup_indent_guides_editor(
20594 &"
20595 block1
20596 block2
20597 block3
20598 block4
20599 block2
20600 block1
20601 block1"
20602 .unindent(),
20603 cx,
20604 )
20605 .await;
20606
20607 assert_indent_guides(
20608 1..10,
20609 vec![
20610 indent_guide(buffer_id, 1, 4, 0),
20611 indent_guide(buffer_id, 2, 3, 1),
20612 indent_guide(buffer_id, 3, 3, 2),
20613 ],
20614 None,
20615 &mut cx,
20616 );
20617}
20618
20619#[gpui::test]
20620async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20621 let (buffer_id, mut cx) = setup_indent_guides_editor(
20622 &"
20623 block1
20624 block2
20625 block3
20626
20627 block1
20628 block1"
20629 .unindent(),
20630 cx,
20631 )
20632 .await;
20633
20634 assert_indent_guides(
20635 0..6,
20636 vec![
20637 indent_guide(buffer_id, 1, 2, 0),
20638 indent_guide(buffer_id, 2, 2, 1),
20639 ],
20640 None,
20641 &mut cx,
20642 );
20643}
20644
20645#[gpui::test]
20646async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20647 let (buffer_id, mut cx) = setup_indent_guides_editor(
20648 &"
20649 function component() {
20650 \treturn (
20651 \t\t\t
20652 \t\t<div>
20653 \t\t\t<abc></abc>
20654 \t\t</div>
20655 \t)
20656 }"
20657 .unindent(),
20658 cx,
20659 )
20660 .await;
20661
20662 assert_indent_guides(
20663 0..8,
20664 vec![
20665 indent_guide(buffer_id, 1, 6, 0),
20666 indent_guide(buffer_id, 2, 5, 1),
20667 indent_guide(buffer_id, 4, 4, 2),
20668 ],
20669 None,
20670 &mut cx,
20671 );
20672}
20673
20674#[gpui::test]
20675async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20676 let (buffer_id, mut cx) = setup_indent_guides_editor(
20677 &"
20678 function component() {
20679 \treturn (
20680 \t
20681 \t\t<div>
20682 \t\t\t<abc></abc>
20683 \t\t</div>
20684 \t)
20685 }"
20686 .unindent(),
20687 cx,
20688 )
20689 .await;
20690
20691 assert_indent_guides(
20692 0..8,
20693 vec![
20694 indent_guide(buffer_id, 1, 6, 0),
20695 indent_guide(buffer_id, 2, 5, 1),
20696 indent_guide(buffer_id, 4, 4, 2),
20697 ],
20698 None,
20699 &mut cx,
20700 );
20701}
20702
20703#[gpui::test]
20704async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20705 let (buffer_id, mut cx) = setup_indent_guides_editor(
20706 &"
20707 block1
20708
20709
20710
20711 block2
20712 "
20713 .unindent(),
20714 cx,
20715 )
20716 .await;
20717
20718 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20719}
20720
20721#[gpui::test]
20722async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20723 let (buffer_id, mut cx) = setup_indent_guides_editor(
20724 &"
20725 def a:
20726 \tb = 3
20727 \tif True:
20728 \t\tc = 4
20729 \t\td = 5
20730 \tprint(b)
20731 "
20732 .unindent(),
20733 cx,
20734 )
20735 .await;
20736
20737 assert_indent_guides(
20738 0..6,
20739 vec![
20740 indent_guide(buffer_id, 1, 5, 0),
20741 indent_guide(buffer_id, 3, 4, 1),
20742 ],
20743 None,
20744 &mut cx,
20745 );
20746}
20747
20748#[gpui::test]
20749async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20750 let (buffer_id, mut cx) = setup_indent_guides_editor(
20751 &"
20752 fn main() {
20753 let a = 1;
20754 }"
20755 .unindent(),
20756 cx,
20757 )
20758 .await;
20759
20760 cx.update_editor(|editor, window, cx| {
20761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20762 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20763 });
20764 });
20765
20766 assert_indent_guides(
20767 0..3,
20768 vec![indent_guide(buffer_id, 1, 1, 0)],
20769 Some(vec![0]),
20770 &mut cx,
20771 );
20772}
20773
20774#[gpui::test]
20775async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20776 let (buffer_id, mut cx) = setup_indent_guides_editor(
20777 &"
20778 fn main() {
20779 if 1 == 2 {
20780 let a = 1;
20781 }
20782 }"
20783 .unindent(),
20784 cx,
20785 )
20786 .await;
20787
20788 cx.update_editor(|editor, window, cx| {
20789 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20790 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20791 });
20792 });
20793
20794 assert_indent_guides(
20795 0..4,
20796 vec![
20797 indent_guide(buffer_id, 1, 3, 0),
20798 indent_guide(buffer_id, 2, 2, 1),
20799 ],
20800 Some(vec![1]),
20801 &mut cx,
20802 );
20803
20804 cx.update_editor(|editor, window, cx| {
20805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20806 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20807 });
20808 });
20809
20810 assert_indent_guides(
20811 0..4,
20812 vec![
20813 indent_guide(buffer_id, 1, 3, 0),
20814 indent_guide(buffer_id, 2, 2, 1),
20815 ],
20816 Some(vec![1]),
20817 &mut cx,
20818 );
20819
20820 cx.update_editor(|editor, window, cx| {
20821 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20822 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20823 });
20824 });
20825
20826 assert_indent_guides(
20827 0..4,
20828 vec![
20829 indent_guide(buffer_id, 1, 3, 0),
20830 indent_guide(buffer_id, 2, 2, 1),
20831 ],
20832 Some(vec![0]),
20833 &mut cx,
20834 );
20835}
20836
20837#[gpui::test]
20838async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20839 let (buffer_id, mut cx) = setup_indent_guides_editor(
20840 &"
20841 fn main() {
20842 let a = 1;
20843
20844 let b = 2;
20845 }"
20846 .unindent(),
20847 cx,
20848 )
20849 .await;
20850
20851 cx.update_editor(|editor, window, cx| {
20852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20853 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20854 });
20855 });
20856
20857 assert_indent_guides(
20858 0..5,
20859 vec![indent_guide(buffer_id, 1, 3, 0)],
20860 Some(vec![0]),
20861 &mut cx,
20862 );
20863}
20864
20865#[gpui::test]
20866async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20867 let (buffer_id, mut cx) = setup_indent_guides_editor(
20868 &"
20869 def m:
20870 a = 1
20871 pass"
20872 .unindent(),
20873 cx,
20874 )
20875 .await;
20876
20877 cx.update_editor(|editor, window, cx| {
20878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20879 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20880 });
20881 });
20882
20883 assert_indent_guides(
20884 0..3,
20885 vec![indent_guide(buffer_id, 1, 2, 0)],
20886 Some(vec![0]),
20887 &mut cx,
20888 );
20889}
20890
20891#[gpui::test]
20892async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20893 init_test(cx, |_| {});
20894 let mut cx = EditorTestContext::new(cx).await;
20895 let text = indoc! {
20896 "
20897 impl A {
20898 fn b() {
20899 0;
20900 3;
20901 5;
20902 6;
20903 7;
20904 }
20905 }
20906 "
20907 };
20908 let base_text = indoc! {
20909 "
20910 impl A {
20911 fn b() {
20912 0;
20913 1;
20914 2;
20915 3;
20916 4;
20917 }
20918 fn c() {
20919 5;
20920 6;
20921 7;
20922 }
20923 }
20924 "
20925 };
20926
20927 cx.update_editor(|editor, window, cx| {
20928 editor.set_text(text, window, cx);
20929
20930 editor.buffer().update(cx, |multibuffer, cx| {
20931 let buffer = multibuffer.as_singleton().unwrap();
20932 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20933
20934 multibuffer.set_all_diff_hunks_expanded(cx);
20935 multibuffer.add_diff(diff, cx);
20936
20937 buffer.read(cx).remote_id()
20938 })
20939 });
20940 cx.run_until_parked();
20941
20942 cx.assert_state_with_diff(
20943 indoc! { "
20944 impl A {
20945 fn b() {
20946 0;
20947 - 1;
20948 - 2;
20949 3;
20950 - 4;
20951 - }
20952 - fn c() {
20953 5;
20954 6;
20955 7;
20956 }
20957 }
20958 ˇ"
20959 }
20960 .to_string(),
20961 );
20962
20963 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20964 editor
20965 .snapshot(window, cx)
20966 .buffer_snapshot()
20967 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20968 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20969 .collect::<Vec<_>>()
20970 });
20971 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20972 assert_eq!(
20973 actual_guides,
20974 vec![
20975 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20976 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20977 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20978 ]
20979 );
20980}
20981
20982#[gpui::test]
20983async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20984 init_test(cx, |_| {});
20985 let mut cx = EditorTestContext::new(cx).await;
20986
20987 let diff_base = r#"
20988 a
20989 b
20990 c
20991 "#
20992 .unindent();
20993
20994 cx.set_state(
20995 &r#"
20996 ˇA
20997 b
20998 C
20999 "#
21000 .unindent(),
21001 );
21002 cx.set_head_text(&diff_base);
21003 cx.update_editor(|editor, window, cx| {
21004 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21005 });
21006 executor.run_until_parked();
21007
21008 let both_hunks_expanded = r#"
21009 - a
21010 + ˇA
21011 b
21012 - c
21013 + C
21014 "#
21015 .unindent();
21016
21017 cx.assert_state_with_diff(both_hunks_expanded.clone());
21018
21019 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21020 let snapshot = editor.snapshot(window, cx);
21021 let hunks = editor
21022 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21023 .collect::<Vec<_>>();
21024 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21025 let buffer_id = hunks[0].buffer_id;
21026 hunks
21027 .into_iter()
21028 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21029 .collect::<Vec<_>>()
21030 });
21031 assert_eq!(hunk_ranges.len(), 2);
21032
21033 cx.update_editor(|editor, _, cx| {
21034 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21035 });
21036 executor.run_until_parked();
21037
21038 let second_hunk_expanded = r#"
21039 ˇA
21040 b
21041 - c
21042 + C
21043 "#
21044 .unindent();
21045
21046 cx.assert_state_with_diff(second_hunk_expanded);
21047
21048 cx.update_editor(|editor, _, cx| {
21049 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21050 });
21051 executor.run_until_parked();
21052
21053 cx.assert_state_with_diff(both_hunks_expanded.clone());
21054
21055 cx.update_editor(|editor, _, cx| {
21056 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21057 });
21058 executor.run_until_parked();
21059
21060 let first_hunk_expanded = r#"
21061 - a
21062 + ˇA
21063 b
21064 C
21065 "#
21066 .unindent();
21067
21068 cx.assert_state_with_diff(first_hunk_expanded);
21069
21070 cx.update_editor(|editor, _, cx| {
21071 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21072 });
21073 executor.run_until_parked();
21074
21075 cx.assert_state_with_diff(both_hunks_expanded);
21076
21077 cx.set_state(
21078 &r#"
21079 ˇA
21080 b
21081 "#
21082 .unindent(),
21083 );
21084 cx.run_until_parked();
21085
21086 // TODO this cursor position seems bad
21087 cx.assert_state_with_diff(
21088 r#"
21089 - ˇa
21090 + A
21091 b
21092 "#
21093 .unindent(),
21094 );
21095
21096 cx.update_editor(|editor, window, cx| {
21097 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21098 });
21099
21100 cx.assert_state_with_diff(
21101 r#"
21102 - ˇa
21103 + A
21104 b
21105 - c
21106 "#
21107 .unindent(),
21108 );
21109
21110 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21111 let snapshot = editor.snapshot(window, cx);
21112 let hunks = editor
21113 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21114 .collect::<Vec<_>>();
21115 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21116 let buffer_id = hunks[0].buffer_id;
21117 hunks
21118 .into_iter()
21119 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21120 .collect::<Vec<_>>()
21121 });
21122 assert_eq!(hunk_ranges.len(), 2);
21123
21124 cx.update_editor(|editor, _, cx| {
21125 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21126 });
21127 executor.run_until_parked();
21128
21129 cx.assert_state_with_diff(
21130 r#"
21131 - ˇa
21132 + A
21133 b
21134 "#
21135 .unindent(),
21136 );
21137}
21138
21139#[gpui::test]
21140async fn test_toggle_deletion_hunk_at_start_of_file(
21141 executor: BackgroundExecutor,
21142 cx: &mut TestAppContext,
21143) {
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 ˇb
21157 c
21158 "#
21159 .unindent(),
21160 );
21161 cx.set_head_text(&diff_base);
21162 cx.update_editor(|editor, window, cx| {
21163 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21164 });
21165 executor.run_until_parked();
21166
21167 let hunk_expanded = r#"
21168 - a
21169 ˇb
21170 c
21171 "#
21172 .unindent();
21173
21174 cx.assert_state_with_diff(hunk_expanded.clone());
21175
21176 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21177 let snapshot = editor.snapshot(window, cx);
21178 let hunks = editor
21179 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21180 .collect::<Vec<_>>();
21181 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21182 let buffer_id = hunks[0].buffer_id;
21183 hunks
21184 .into_iter()
21185 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21186 .collect::<Vec<_>>()
21187 });
21188 assert_eq!(hunk_ranges.len(), 1);
21189
21190 cx.update_editor(|editor, _, cx| {
21191 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21192 });
21193 executor.run_until_parked();
21194
21195 let hunk_collapsed = r#"
21196 ˇb
21197 c
21198 "#
21199 .unindent();
21200
21201 cx.assert_state_with_diff(hunk_collapsed);
21202
21203 cx.update_editor(|editor, _, cx| {
21204 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21205 });
21206 executor.run_until_parked();
21207
21208 cx.assert_state_with_diff(hunk_expanded);
21209}
21210
21211#[gpui::test]
21212async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21213 init_test(cx, |_| {});
21214
21215 let fs = FakeFs::new(cx.executor());
21216 fs.insert_tree(
21217 path!("/test"),
21218 json!({
21219 ".git": {},
21220 "file-1": "ONE\n",
21221 "file-2": "TWO\n",
21222 "file-3": "THREE\n",
21223 }),
21224 )
21225 .await;
21226
21227 fs.set_head_for_repo(
21228 path!("/test/.git").as_ref(),
21229 &[
21230 ("file-1", "one\n".into()),
21231 ("file-2", "two\n".into()),
21232 ("file-3", "three\n".into()),
21233 ],
21234 "deadbeef",
21235 );
21236
21237 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21238 let mut buffers = vec![];
21239 for i in 1..=3 {
21240 let buffer = project
21241 .update(cx, |project, cx| {
21242 let path = format!(path!("/test/file-{}"), i);
21243 project.open_local_buffer(path, cx)
21244 })
21245 .await
21246 .unwrap();
21247 buffers.push(buffer);
21248 }
21249
21250 let multibuffer = cx.new(|cx| {
21251 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21252 multibuffer.set_all_diff_hunks_expanded(cx);
21253 for buffer in &buffers {
21254 let snapshot = buffer.read(cx).snapshot();
21255 multibuffer.set_excerpts_for_path(
21256 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21257 buffer.clone(),
21258 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21259 2,
21260 cx,
21261 );
21262 }
21263 multibuffer
21264 });
21265
21266 let editor = cx.add_window(|window, cx| {
21267 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21268 });
21269 cx.run_until_parked();
21270
21271 let snapshot = editor
21272 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21273 .unwrap();
21274 let hunks = snapshot
21275 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21276 .map(|hunk| match hunk {
21277 DisplayDiffHunk::Unfolded {
21278 display_row_range, ..
21279 } => display_row_range,
21280 DisplayDiffHunk::Folded { .. } => unreachable!(),
21281 })
21282 .collect::<Vec<_>>();
21283 assert_eq!(
21284 hunks,
21285 [
21286 DisplayRow(2)..DisplayRow(4),
21287 DisplayRow(7)..DisplayRow(9),
21288 DisplayRow(12)..DisplayRow(14),
21289 ]
21290 );
21291}
21292
21293#[gpui::test]
21294async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21295 init_test(cx, |_| {});
21296
21297 let mut cx = EditorTestContext::new(cx).await;
21298 cx.set_head_text(indoc! { "
21299 one
21300 two
21301 three
21302 four
21303 five
21304 "
21305 });
21306 cx.set_index_text(indoc! { "
21307 one
21308 two
21309 three
21310 four
21311 five
21312 "
21313 });
21314 cx.set_state(indoc! {"
21315 one
21316 TWO
21317 ˇTHREE
21318 FOUR
21319 five
21320 "});
21321 cx.run_until_parked();
21322 cx.update_editor(|editor, window, cx| {
21323 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21324 });
21325 cx.run_until_parked();
21326 cx.assert_index_text(Some(indoc! {"
21327 one
21328 TWO
21329 THREE
21330 FOUR
21331 five
21332 "}));
21333 cx.set_state(indoc! { "
21334 one
21335 TWO
21336 ˇTHREE-HUNDRED
21337 FOUR
21338 five
21339 "});
21340 cx.run_until_parked();
21341 cx.update_editor(|editor, window, cx| {
21342 let snapshot = editor.snapshot(window, cx);
21343 let hunks = editor
21344 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21345 .collect::<Vec<_>>();
21346 assert_eq!(hunks.len(), 1);
21347 assert_eq!(
21348 hunks[0].status(),
21349 DiffHunkStatus {
21350 kind: DiffHunkStatusKind::Modified,
21351 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21352 }
21353 );
21354
21355 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21356 });
21357 cx.run_until_parked();
21358 cx.assert_index_text(Some(indoc! {"
21359 one
21360 TWO
21361 THREE-HUNDRED
21362 FOUR
21363 five
21364 "}));
21365}
21366
21367#[gpui::test]
21368fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21369 init_test(cx, |_| {});
21370
21371 let editor = cx.add_window(|window, cx| {
21372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21373 build_editor(buffer, window, cx)
21374 });
21375
21376 let render_args = Arc::new(Mutex::new(None));
21377 let snapshot = editor
21378 .update(cx, |editor, window, cx| {
21379 let snapshot = editor.buffer().read(cx).snapshot(cx);
21380 let range =
21381 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21382
21383 struct RenderArgs {
21384 row: MultiBufferRow,
21385 folded: bool,
21386 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21387 }
21388
21389 let crease = Crease::inline(
21390 range,
21391 FoldPlaceholder::test(),
21392 {
21393 let toggle_callback = render_args.clone();
21394 move |row, folded, callback, _window, _cx| {
21395 *toggle_callback.lock() = Some(RenderArgs {
21396 row,
21397 folded,
21398 callback,
21399 });
21400 div()
21401 }
21402 },
21403 |_row, _folded, _window, _cx| div(),
21404 );
21405
21406 editor.insert_creases(Some(crease), cx);
21407 let snapshot = editor.snapshot(window, cx);
21408 let _div =
21409 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21410 snapshot
21411 })
21412 .unwrap();
21413
21414 let render_args = render_args.lock().take().unwrap();
21415 assert_eq!(render_args.row, MultiBufferRow(1));
21416 assert!(!render_args.folded);
21417 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21418
21419 cx.update_window(*editor, |_, window, cx| {
21420 (render_args.callback)(true, window, cx)
21421 })
21422 .unwrap();
21423 let snapshot = editor
21424 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21425 .unwrap();
21426 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21427
21428 cx.update_window(*editor, |_, window, cx| {
21429 (render_args.callback)(false, window, cx)
21430 })
21431 .unwrap();
21432 let snapshot = editor
21433 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21434 .unwrap();
21435 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21436}
21437
21438#[gpui::test]
21439async fn test_input_text(cx: &mut TestAppContext) {
21440 init_test(cx, |_| {});
21441 let mut cx = EditorTestContext::new(cx).await;
21442
21443 cx.set_state(
21444 &r#"ˇone
21445 two
21446
21447 three
21448 fourˇ
21449 five
21450
21451 siˇx"#
21452 .unindent(),
21453 );
21454
21455 cx.dispatch_action(HandleInput(String::new()));
21456 cx.assert_editor_state(
21457 &r#"ˇone
21458 two
21459
21460 three
21461 fourˇ
21462 five
21463
21464 siˇx"#
21465 .unindent(),
21466 );
21467
21468 cx.dispatch_action(HandleInput("AAAA".to_string()));
21469 cx.assert_editor_state(
21470 &r#"AAAAˇone
21471 two
21472
21473 three
21474 fourAAAAˇ
21475 five
21476
21477 siAAAAˇx"#
21478 .unindent(),
21479 );
21480}
21481
21482#[gpui::test]
21483async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21484 init_test(cx, |_| {});
21485
21486 let mut cx = EditorTestContext::new(cx).await;
21487 cx.set_state(
21488 r#"let foo = 1;
21489let foo = 2;
21490let foo = 3;
21491let fooˇ = 4;
21492let foo = 5;
21493let foo = 6;
21494let foo = 7;
21495let foo = 8;
21496let foo = 9;
21497let foo = 10;
21498let foo = 11;
21499let foo = 12;
21500let foo = 13;
21501let foo = 14;
21502let foo = 15;"#,
21503 );
21504
21505 cx.update_editor(|e, window, cx| {
21506 assert_eq!(
21507 e.next_scroll_position,
21508 NextScrollCursorCenterTopBottom::Center,
21509 "Default next scroll direction is center",
21510 );
21511
21512 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21513 assert_eq!(
21514 e.next_scroll_position,
21515 NextScrollCursorCenterTopBottom::Top,
21516 "After center, next scroll direction should be top",
21517 );
21518
21519 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21520 assert_eq!(
21521 e.next_scroll_position,
21522 NextScrollCursorCenterTopBottom::Bottom,
21523 "After top, next scroll direction should be bottom",
21524 );
21525
21526 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21527 assert_eq!(
21528 e.next_scroll_position,
21529 NextScrollCursorCenterTopBottom::Center,
21530 "After bottom, scrolling should start over",
21531 );
21532
21533 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21534 assert_eq!(
21535 e.next_scroll_position,
21536 NextScrollCursorCenterTopBottom::Top,
21537 "Scrolling continues if retriggered fast enough"
21538 );
21539 });
21540
21541 cx.executor()
21542 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21543 cx.executor().run_until_parked();
21544 cx.update_editor(|e, _, _| {
21545 assert_eq!(
21546 e.next_scroll_position,
21547 NextScrollCursorCenterTopBottom::Center,
21548 "If scrolling is not triggered fast enough, it should reset"
21549 );
21550 });
21551}
21552
21553#[gpui::test]
21554async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21555 init_test(cx, |_| {});
21556 let mut cx = EditorLspTestContext::new_rust(
21557 lsp::ServerCapabilities {
21558 definition_provider: Some(lsp::OneOf::Left(true)),
21559 references_provider: Some(lsp::OneOf::Left(true)),
21560 ..lsp::ServerCapabilities::default()
21561 },
21562 cx,
21563 )
21564 .await;
21565
21566 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21567 let go_to_definition = cx
21568 .lsp
21569 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21570 move |params, _| async move {
21571 if empty_go_to_definition {
21572 Ok(None)
21573 } else {
21574 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21575 uri: params.text_document_position_params.text_document.uri,
21576 range: lsp::Range::new(
21577 lsp::Position::new(4, 3),
21578 lsp::Position::new(4, 6),
21579 ),
21580 })))
21581 }
21582 },
21583 );
21584 let references = cx
21585 .lsp
21586 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21587 Ok(Some(vec![lsp::Location {
21588 uri: params.text_document_position.text_document.uri,
21589 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21590 }]))
21591 });
21592 (go_to_definition, references)
21593 };
21594
21595 cx.set_state(
21596 &r#"fn one() {
21597 let mut a = ˇtwo();
21598 }
21599
21600 fn two() {}"#
21601 .unindent(),
21602 );
21603 set_up_lsp_handlers(false, &mut cx);
21604 let navigated = cx
21605 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21606 .await
21607 .expect("Failed to navigate to definition");
21608 assert_eq!(
21609 navigated,
21610 Navigated::Yes,
21611 "Should have navigated to definition from the GetDefinition response"
21612 );
21613 cx.assert_editor_state(
21614 &r#"fn one() {
21615 let mut a = two();
21616 }
21617
21618 fn «twoˇ»() {}"#
21619 .unindent(),
21620 );
21621
21622 let editors = cx.update_workspace(|workspace, _, cx| {
21623 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21624 });
21625 cx.update_editor(|_, _, test_editor_cx| {
21626 assert_eq!(
21627 editors.len(),
21628 1,
21629 "Initially, only one, test, editor should be open in the workspace"
21630 );
21631 assert_eq!(
21632 test_editor_cx.entity(),
21633 editors.last().expect("Asserted len is 1").clone()
21634 );
21635 });
21636
21637 set_up_lsp_handlers(true, &mut cx);
21638 let navigated = cx
21639 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21640 .await
21641 .expect("Failed to navigate to lookup references");
21642 assert_eq!(
21643 navigated,
21644 Navigated::Yes,
21645 "Should have navigated to references as a fallback after empty GoToDefinition response"
21646 );
21647 // We should not change the selections in the existing file,
21648 // if opening another milti buffer with the references
21649 cx.assert_editor_state(
21650 &r#"fn one() {
21651 let mut a = two();
21652 }
21653
21654 fn «twoˇ»() {}"#
21655 .unindent(),
21656 );
21657 let editors = cx.update_workspace(|workspace, _, cx| {
21658 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21659 });
21660 cx.update_editor(|_, _, test_editor_cx| {
21661 assert_eq!(
21662 editors.len(),
21663 2,
21664 "After falling back to references search, we open a new editor with the results"
21665 );
21666 let references_fallback_text = editors
21667 .into_iter()
21668 .find(|new_editor| *new_editor != test_editor_cx.entity())
21669 .expect("Should have one non-test editor now")
21670 .read(test_editor_cx)
21671 .text(test_editor_cx);
21672 assert_eq!(
21673 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21674 "Should use the range from the references response and not the GoToDefinition one"
21675 );
21676 });
21677}
21678
21679#[gpui::test]
21680async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21681 init_test(cx, |_| {});
21682 cx.update(|cx| {
21683 let mut editor_settings = EditorSettings::get_global(cx).clone();
21684 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21685 EditorSettings::override_global(editor_settings, cx);
21686 });
21687 let mut cx = EditorLspTestContext::new_rust(
21688 lsp::ServerCapabilities {
21689 definition_provider: Some(lsp::OneOf::Left(true)),
21690 references_provider: Some(lsp::OneOf::Left(true)),
21691 ..lsp::ServerCapabilities::default()
21692 },
21693 cx,
21694 )
21695 .await;
21696 let original_state = r#"fn one() {
21697 let mut a = ˇtwo();
21698 }
21699
21700 fn two() {}"#
21701 .unindent();
21702 cx.set_state(&original_state);
21703
21704 let mut go_to_definition = cx
21705 .lsp
21706 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21707 move |_, _| async move { Ok(None) },
21708 );
21709 let _references = cx
21710 .lsp
21711 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21712 panic!("Should not call for references with no go to definition fallback")
21713 });
21714
21715 let navigated = cx
21716 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21717 .await
21718 .expect("Failed to navigate to lookup references");
21719 go_to_definition
21720 .next()
21721 .await
21722 .expect("Should have called the go_to_definition handler");
21723
21724 assert_eq!(
21725 navigated,
21726 Navigated::No,
21727 "Should have navigated to references as a fallback after empty GoToDefinition response"
21728 );
21729 cx.assert_editor_state(&original_state);
21730 let editors = cx.update_workspace(|workspace, _, cx| {
21731 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21732 });
21733 cx.update_editor(|_, _, _| {
21734 assert_eq!(
21735 editors.len(),
21736 1,
21737 "After unsuccessful fallback, no other editor should have been opened"
21738 );
21739 });
21740}
21741
21742#[gpui::test]
21743async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21744 init_test(cx, |_| {});
21745 let mut cx = EditorLspTestContext::new_rust(
21746 lsp::ServerCapabilities {
21747 references_provider: Some(lsp::OneOf::Left(true)),
21748 ..lsp::ServerCapabilities::default()
21749 },
21750 cx,
21751 )
21752 .await;
21753
21754 cx.set_state(
21755 &r#"
21756 fn one() {
21757 let mut a = two();
21758 }
21759
21760 fn ˇtwo() {}"#
21761 .unindent(),
21762 );
21763 cx.lsp
21764 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21765 Ok(Some(vec![
21766 lsp::Location {
21767 uri: params.text_document_position.text_document.uri.clone(),
21768 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21769 },
21770 lsp::Location {
21771 uri: params.text_document_position.text_document.uri,
21772 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21773 },
21774 ]))
21775 });
21776 let navigated = cx
21777 .update_editor(|editor, window, cx| {
21778 editor.find_all_references(&FindAllReferences, window, cx)
21779 })
21780 .unwrap()
21781 .await
21782 .expect("Failed to navigate to references");
21783 assert_eq!(
21784 navigated,
21785 Navigated::Yes,
21786 "Should have navigated to references from the FindAllReferences response"
21787 );
21788 cx.assert_editor_state(
21789 &r#"fn one() {
21790 let mut a = two();
21791 }
21792
21793 fn ˇtwo() {}"#
21794 .unindent(),
21795 );
21796
21797 let editors = cx.update_workspace(|workspace, _, cx| {
21798 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21799 });
21800 cx.update_editor(|_, _, _| {
21801 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21802 });
21803
21804 cx.set_state(
21805 &r#"fn one() {
21806 let mut a = ˇtwo();
21807 }
21808
21809 fn two() {}"#
21810 .unindent(),
21811 );
21812 let navigated = cx
21813 .update_editor(|editor, window, cx| {
21814 editor.find_all_references(&FindAllReferences, window, cx)
21815 })
21816 .unwrap()
21817 .await
21818 .expect("Failed to navigate to references");
21819 assert_eq!(
21820 navigated,
21821 Navigated::Yes,
21822 "Should have navigated to references from the FindAllReferences response"
21823 );
21824 cx.assert_editor_state(
21825 &r#"fn one() {
21826 let mut a = ˇtwo();
21827 }
21828
21829 fn two() {}"#
21830 .unindent(),
21831 );
21832 let editors = cx.update_workspace(|workspace, _, cx| {
21833 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21834 });
21835 cx.update_editor(|_, _, _| {
21836 assert_eq!(
21837 editors.len(),
21838 2,
21839 "should have re-used the previous multibuffer"
21840 );
21841 });
21842
21843 cx.set_state(
21844 &r#"fn one() {
21845 let mut a = ˇtwo();
21846 }
21847 fn three() {}
21848 fn two() {}"#
21849 .unindent(),
21850 );
21851 cx.lsp
21852 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21853 Ok(Some(vec![
21854 lsp::Location {
21855 uri: params.text_document_position.text_document.uri.clone(),
21856 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21857 },
21858 lsp::Location {
21859 uri: params.text_document_position.text_document.uri,
21860 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21861 },
21862 ]))
21863 });
21864 let navigated = cx
21865 .update_editor(|editor, window, cx| {
21866 editor.find_all_references(&FindAllReferences, window, cx)
21867 })
21868 .unwrap()
21869 .await
21870 .expect("Failed to navigate to references");
21871 assert_eq!(
21872 navigated,
21873 Navigated::Yes,
21874 "Should have navigated to references from the FindAllReferences response"
21875 );
21876 cx.assert_editor_state(
21877 &r#"fn one() {
21878 let mut a = ˇtwo();
21879 }
21880 fn three() {}
21881 fn two() {}"#
21882 .unindent(),
21883 );
21884 let editors = cx.update_workspace(|workspace, _, cx| {
21885 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21886 });
21887 cx.update_editor(|_, _, _| {
21888 assert_eq!(
21889 editors.len(),
21890 3,
21891 "should have used a new multibuffer as offsets changed"
21892 );
21893 });
21894}
21895#[gpui::test]
21896async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21897 init_test(cx, |_| {});
21898
21899 let language = Arc::new(Language::new(
21900 LanguageConfig::default(),
21901 Some(tree_sitter_rust::LANGUAGE.into()),
21902 ));
21903
21904 let text = r#"
21905 #[cfg(test)]
21906 mod tests() {
21907 #[test]
21908 fn runnable_1() {
21909 let a = 1;
21910 }
21911
21912 #[test]
21913 fn runnable_2() {
21914 let a = 1;
21915 let b = 2;
21916 }
21917 }
21918 "#
21919 .unindent();
21920
21921 let fs = FakeFs::new(cx.executor());
21922 fs.insert_file("/file.rs", Default::default()).await;
21923
21924 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21925 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21926 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21927 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21928 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21929
21930 let editor = cx.new_window_entity(|window, cx| {
21931 Editor::new(
21932 EditorMode::full(),
21933 multi_buffer,
21934 Some(project.clone()),
21935 window,
21936 cx,
21937 )
21938 });
21939
21940 editor.update_in(cx, |editor, window, cx| {
21941 let snapshot = editor.buffer().read(cx).snapshot(cx);
21942 editor.tasks.insert(
21943 (buffer.read(cx).remote_id(), 3),
21944 RunnableTasks {
21945 templates: vec![],
21946 offset: snapshot.anchor_before(43),
21947 column: 0,
21948 extra_variables: HashMap::default(),
21949 context_range: BufferOffset(43)..BufferOffset(85),
21950 },
21951 );
21952 editor.tasks.insert(
21953 (buffer.read(cx).remote_id(), 8),
21954 RunnableTasks {
21955 templates: vec![],
21956 offset: snapshot.anchor_before(86),
21957 column: 0,
21958 extra_variables: HashMap::default(),
21959 context_range: BufferOffset(86)..BufferOffset(191),
21960 },
21961 );
21962
21963 // Test finding task when cursor is inside function body
21964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21965 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21966 });
21967 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21968 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21969
21970 // Test finding task when cursor is on function name
21971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21972 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21973 });
21974 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21975 assert_eq!(row, 8, "Should find task when cursor is on function name");
21976 });
21977}
21978
21979#[gpui::test]
21980async fn test_folding_buffers(cx: &mut TestAppContext) {
21981 init_test(cx, |_| {});
21982
21983 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21984 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21985 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21986
21987 let fs = FakeFs::new(cx.executor());
21988 fs.insert_tree(
21989 path!("/a"),
21990 json!({
21991 "first.rs": sample_text_1,
21992 "second.rs": sample_text_2,
21993 "third.rs": sample_text_3,
21994 }),
21995 )
21996 .await;
21997 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21998 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21999 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22000 let worktree = project.update(cx, |project, cx| {
22001 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22002 assert_eq!(worktrees.len(), 1);
22003 worktrees.pop().unwrap()
22004 });
22005 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22006
22007 let buffer_1 = project
22008 .update(cx, |project, cx| {
22009 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22010 })
22011 .await
22012 .unwrap();
22013 let buffer_2 = project
22014 .update(cx, |project, cx| {
22015 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22016 })
22017 .await
22018 .unwrap();
22019 let buffer_3 = project
22020 .update(cx, |project, cx| {
22021 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22022 })
22023 .await
22024 .unwrap();
22025
22026 let multi_buffer = cx.new(|cx| {
22027 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22028 multi_buffer.push_excerpts(
22029 buffer_1.clone(),
22030 [
22031 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22032 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22033 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22034 ],
22035 cx,
22036 );
22037 multi_buffer.push_excerpts(
22038 buffer_2.clone(),
22039 [
22040 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22041 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22042 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22043 ],
22044 cx,
22045 );
22046 multi_buffer.push_excerpts(
22047 buffer_3.clone(),
22048 [
22049 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22050 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22051 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22052 ],
22053 cx,
22054 );
22055 multi_buffer
22056 });
22057 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22058 Editor::new(
22059 EditorMode::full(),
22060 multi_buffer.clone(),
22061 Some(project.clone()),
22062 window,
22063 cx,
22064 )
22065 });
22066
22067 assert_eq!(
22068 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22069 "\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",
22070 );
22071
22072 multi_buffer_editor.update(cx, |editor, cx| {
22073 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22074 });
22075 assert_eq!(
22076 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22077 "\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",
22078 "After folding the first buffer, its text should not be displayed"
22079 );
22080
22081 multi_buffer_editor.update(cx, |editor, cx| {
22082 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22083 });
22084 assert_eq!(
22085 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22086 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22087 "After folding the second buffer, its text should not be displayed"
22088 );
22089
22090 multi_buffer_editor.update(cx, |editor, cx| {
22091 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22092 });
22093 assert_eq!(
22094 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22095 "\n\n\n\n\n",
22096 "After folding the third buffer, its text should not be displayed"
22097 );
22098
22099 // Emulate selection inside the fold logic, that should work
22100 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22101 editor
22102 .snapshot(window, cx)
22103 .next_line_boundary(Point::new(0, 4));
22104 });
22105
22106 multi_buffer_editor.update(cx, |editor, cx| {
22107 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22108 });
22109 assert_eq!(
22110 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22111 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22112 "After unfolding the second buffer, its text should be displayed"
22113 );
22114
22115 // Typing inside of buffer 1 causes that buffer to be unfolded.
22116 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22117 assert_eq!(
22118 multi_buffer
22119 .read(cx)
22120 .snapshot(cx)
22121 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22122 .collect::<String>(),
22123 "bbbb"
22124 );
22125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22126 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22127 });
22128 editor.handle_input("B", window, cx);
22129 });
22130
22131 assert_eq!(
22132 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22133 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22134 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22135 );
22136
22137 multi_buffer_editor.update(cx, |editor, cx| {
22138 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22139 });
22140 assert_eq!(
22141 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22142 "\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",
22143 "After unfolding the all buffers, all original text should be displayed"
22144 );
22145}
22146
22147#[gpui::test]
22148async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22149 init_test(cx, |_| {});
22150
22151 let sample_text_1 = "1111\n2222\n3333".to_string();
22152 let sample_text_2 = "4444\n5555\n6666".to_string();
22153 let sample_text_3 = "7777\n8888\n9999".to_string();
22154
22155 let fs = FakeFs::new(cx.executor());
22156 fs.insert_tree(
22157 path!("/a"),
22158 json!({
22159 "first.rs": sample_text_1,
22160 "second.rs": sample_text_2,
22161 "third.rs": sample_text_3,
22162 }),
22163 )
22164 .await;
22165 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22166 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22167 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22168 let worktree = project.update(cx, |project, cx| {
22169 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22170 assert_eq!(worktrees.len(), 1);
22171 worktrees.pop().unwrap()
22172 });
22173 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22174
22175 let buffer_1 = project
22176 .update(cx, |project, cx| {
22177 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22178 })
22179 .await
22180 .unwrap();
22181 let buffer_2 = project
22182 .update(cx, |project, cx| {
22183 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22184 })
22185 .await
22186 .unwrap();
22187 let buffer_3 = project
22188 .update(cx, |project, cx| {
22189 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22190 })
22191 .await
22192 .unwrap();
22193
22194 let multi_buffer = cx.new(|cx| {
22195 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22196 multi_buffer.push_excerpts(
22197 buffer_1.clone(),
22198 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22199 cx,
22200 );
22201 multi_buffer.push_excerpts(
22202 buffer_2.clone(),
22203 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22204 cx,
22205 );
22206 multi_buffer.push_excerpts(
22207 buffer_3.clone(),
22208 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22209 cx,
22210 );
22211 multi_buffer
22212 });
22213
22214 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22215 Editor::new(
22216 EditorMode::full(),
22217 multi_buffer,
22218 Some(project.clone()),
22219 window,
22220 cx,
22221 )
22222 });
22223
22224 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22225 assert_eq!(
22226 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22227 full_text,
22228 );
22229
22230 multi_buffer_editor.update(cx, |editor, cx| {
22231 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22232 });
22233 assert_eq!(
22234 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22235 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22236 "After folding the first buffer, its text should not be displayed"
22237 );
22238
22239 multi_buffer_editor.update(cx, |editor, cx| {
22240 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22241 });
22242
22243 assert_eq!(
22244 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22245 "\n\n\n\n\n\n7777\n8888\n9999",
22246 "After folding the second buffer, its text should not be displayed"
22247 );
22248
22249 multi_buffer_editor.update(cx, |editor, cx| {
22250 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22251 });
22252 assert_eq!(
22253 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22254 "\n\n\n\n\n",
22255 "After folding the third buffer, its text should not be displayed"
22256 );
22257
22258 multi_buffer_editor.update(cx, |editor, cx| {
22259 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22260 });
22261 assert_eq!(
22262 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22263 "\n\n\n\n4444\n5555\n6666\n\n",
22264 "After unfolding the second buffer, its text should be displayed"
22265 );
22266
22267 multi_buffer_editor.update(cx, |editor, cx| {
22268 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22269 });
22270 assert_eq!(
22271 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22272 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22273 "After unfolding the first buffer, its text should be displayed"
22274 );
22275
22276 multi_buffer_editor.update(cx, |editor, cx| {
22277 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22278 });
22279 assert_eq!(
22280 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22281 full_text,
22282 "After unfolding all buffers, all original text should be displayed"
22283 );
22284}
22285
22286#[gpui::test]
22287async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22288 init_test(cx, |_| {});
22289
22290 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22291
22292 let fs = FakeFs::new(cx.executor());
22293 fs.insert_tree(
22294 path!("/a"),
22295 json!({
22296 "main.rs": sample_text,
22297 }),
22298 )
22299 .await;
22300 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22301 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22302 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22303 let worktree = project.update(cx, |project, cx| {
22304 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22305 assert_eq!(worktrees.len(), 1);
22306 worktrees.pop().unwrap()
22307 });
22308 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22309
22310 let buffer_1 = project
22311 .update(cx, |project, cx| {
22312 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22313 })
22314 .await
22315 .unwrap();
22316
22317 let multi_buffer = cx.new(|cx| {
22318 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22319 multi_buffer.push_excerpts(
22320 buffer_1.clone(),
22321 [ExcerptRange::new(
22322 Point::new(0, 0)
22323 ..Point::new(
22324 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22325 0,
22326 ),
22327 )],
22328 cx,
22329 );
22330 multi_buffer
22331 });
22332 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22333 Editor::new(
22334 EditorMode::full(),
22335 multi_buffer,
22336 Some(project.clone()),
22337 window,
22338 cx,
22339 )
22340 });
22341
22342 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22343 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22344 enum TestHighlight {}
22345 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22346 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22347 editor.highlight_text::<TestHighlight>(
22348 vec![highlight_range.clone()],
22349 HighlightStyle::color(Hsla::green()),
22350 cx,
22351 );
22352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22353 s.select_ranges(Some(highlight_range))
22354 });
22355 });
22356
22357 let full_text = format!("\n\n{sample_text}");
22358 assert_eq!(
22359 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22360 full_text,
22361 );
22362}
22363
22364#[gpui::test]
22365async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22366 init_test(cx, |_| {});
22367 cx.update(|cx| {
22368 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22369 "keymaps/default-linux.json",
22370 cx,
22371 )
22372 .unwrap();
22373 cx.bind_keys(default_key_bindings);
22374 });
22375
22376 let (editor, cx) = cx.add_window_view(|window, cx| {
22377 let multi_buffer = MultiBuffer::build_multi(
22378 [
22379 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22380 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22381 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22382 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22383 ],
22384 cx,
22385 );
22386 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22387
22388 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22389 // fold all but the second buffer, so that we test navigating between two
22390 // adjacent folded buffers, as well as folded buffers at the start and
22391 // end the multibuffer
22392 editor.fold_buffer(buffer_ids[0], cx);
22393 editor.fold_buffer(buffer_ids[2], cx);
22394 editor.fold_buffer(buffer_ids[3], cx);
22395
22396 editor
22397 });
22398 cx.simulate_resize(size(px(1000.), px(1000.)));
22399
22400 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22401 cx.assert_excerpts_with_selections(indoc! {"
22402 [EXCERPT]
22403 ˇ[FOLDED]
22404 [EXCERPT]
22405 a1
22406 b1
22407 [EXCERPT]
22408 [FOLDED]
22409 [EXCERPT]
22410 [FOLDED]
22411 "
22412 });
22413 cx.simulate_keystroke("down");
22414 cx.assert_excerpts_with_selections(indoc! {"
22415 [EXCERPT]
22416 [FOLDED]
22417 [EXCERPT]
22418 ˇa1
22419 b1
22420 [EXCERPT]
22421 [FOLDED]
22422 [EXCERPT]
22423 [FOLDED]
22424 "
22425 });
22426 cx.simulate_keystroke("down");
22427 cx.assert_excerpts_with_selections(indoc! {"
22428 [EXCERPT]
22429 [FOLDED]
22430 [EXCERPT]
22431 a1
22432 ˇb1
22433 [EXCERPT]
22434 [FOLDED]
22435 [EXCERPT]
22436 [FOLDED]
22437 "
22438 });
22439 cx.simulate_keystroke("down");
22440 cx.assert_excerpts_with_selections(indoc! {"
22441 [EXCERPT]
22442 [FOLDED]
22443 [EXCERPT]
22444 a1
22445 b1
22446 ˇ[EXCERPT]
22447 [FOLDED]
22448 [EXCERPT]
22449 [FOLDED]
22450 "
22451 });
22452 cx.simulate_keystroke("down");
22453 cx.assert_excerpts_with_selections(indoc! {"
22454 [EXCERPT]
22455 [FOLDED]
22456 [EXCERPT]
22457 a1
22458 b1
22459 [EXCERPT]
22460 ˇ[FOLDED]
22461 [EXCERPT]
22462 [FOLDED]
22463 "
22464 });
22465 for _ in 0..5 {
22466 cx.simulate_keystroke("down");
22467 cx.assert_excerpts_with_selections(indoc! {"
22468 [EXCERPT]
22469 [FOLDED]
22470 [EXCERPT]
22471 a1
22472 b1
22473 [EXCERPT]
22474 [FOLDED]
22475 [EXCERPT]
22476 ˇ[FOLDED]
22477 "
22478 });
22479 }
22480
22481 cx.simulate_keystroke("up");
22482 cx.assert_excerpts_with_selections(indoc! {"
22483 [EXCERPT]
22484 [FOLDED]
22485 [EXCERPT]
22486 a1
22487 b1
22488 [EXCERPT]
22489 ˇ[FOLDED]
22490 [EXCERPT]
22491 [FOLDED]
22492 "
22493 });
22494 cx.simulate_keystroke("up");
22495 cx.assert_excerpts_with_selections(indoc! {"
22496 [EXCERPT]
22497 [FOLDED]
22498 [EXCERPT]
22499 a1
22500 b1
22501 ˇ[EXCERPT]
22502 [FOLDED]
22503 [EXCERPT]
22504 [FOLDED]
22505 "
22506 });
22507 cx.simulate_keystroke("up");
22508 cx.assert_excerpts_with_selections(indoc! {"
22509 [EXCERPT]
22510 [FOLDED]
22511 [EXCERPT]
22512 a1
22513 ˇb1
22514 [EXCERPT]
22515 [FOLDED]
22516 [EXCERPT]
22517 [FOLDED]
22518 "
22519 });
22520 cx.simulate_keystroke("up");
22521 cx.assert_excerpts_with_selections(indoc! {"
22522 [EXCERPT]
22523 [FOLDED]
22524 [EXCERPT]
22525 ˇa1
22526 b1
22527 [EXCERPT]
22528 [FOLDED]
22529 [EXCERPT]
22530 [FOLDED]
22531 "
22532 });
22533 for _ in 0..5 {
22534 cx.simulate_keystroke("up");
22535 cx.assert_excerpts_with_selections(indoc! {"
22536 [EXCERPT]
22537 ˇ[FOLDED]
22538 [EXCERPT]
22539 a1
22540 b1
22541 [EXCERPT]
22542 [FOLDED]
22543 [EXCERPT]
22544 [FOLDED]
22545 "
22546 });
22547 }
22548}
22549
22550#[gpui::test]
22551async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22552 init_test(cx, |_| {});
22553
22554 // Simple insertion
22555 assert_highlighted_edits(
22556 "Hello, world!",
22557 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22558 true,
22559 cx,
22560 |highlighted_edits, cx| {
22561 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22562 assert_eq!(highlighted_edits.highlights.len(), 1);
22563 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22564 assert_eq!(
22565 highlighted_edits.highlights[0].1.background_color,
22566 Some(cx.theme().status().created_background)
22567 );
22568 },
22569 )
22570 .await;
22571
22572 // Replacement
22573 assert_highlighted_edits(
22574 "This is a test.",
22575 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22576 false,
22577 cx,
22578 |highlighted_edits, cx| {
22579 assert_eq!(highlighted_edits.text, "That is a test.");
22580 assert_eq!(highlighted_edits.highlights.len(), 1);
22581 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22582 assert_eq!(
22583 highlighted_edits.highlights[0].1.background_color,
22584 Some(cx.theme().status().created_background)
22585 );
22586 },
22587 )
22588 .await;
22589
22590 // Multiple edits
22591 assert_highlighted_edits(
22592 "Hello, world!",
22593 vec![
22594 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22595 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22596 ],
22597 false,
22598 cx,
22599 |highlighted_edits, cx| {
22600 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22601 assert_eq!(highlighted_edits.highlights.len(), 2);
22602 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22603 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22604 assert_eq!(
22605 highlighted_edits.highlights[0].1.background_color,
22606 Some(cx.theme().status().created_background)
22607 );
22608 assert_eq!(
22609 highlighted_edits.highlights[1].1.background_color,
22610 Some(cx.theme().status().created_background)
22611 );
22612 },
22613 )
22614 .await;
22615
22616 // Multiple lines with edits
22617 assert_highlighted_edits(
22618 "First line\nSecond line\nThird line\nFourth line",
22619 vec![
22620 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22621 (
22622 Point::new(2, 0)..Point::new(2, 10),
22623 "New third line".to_string(),
22624 ),
22625 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22626 ],
22627 false,
22628 cx,
22629 |highlighted_edits, cx| {
22630 assert_eq!(
22631 highlighted_edits.text,
22632 "Second modified\nNew third line\nFourth updated line"
22633 );
22634 assert_eq!(highlighted_edits.highlights.len(), 3);
22635 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22636 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22637 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22638 for highlight in &highlighted_edits.highlights {
22639 assert_eq!(
22640 highlight.1.background_color,
22641 Some(cx.theme().status().created_background)
22642 );
22643 }
22644 },
22645 )
22646 .await;
22647}
22648
22649#[gpui::test]
22650async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22651 init_test(cx, |_| {});
22652
22653 // Deletion
22654 assert_highlighted_edits(
22655 "Hello, world!",
22656 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22657 true,
22658 cx,
22659 |highlighted_edits, cx| {
22660 assert_eq!(highlighted_edits.text, "Hello, world!");
22661 assert_eq!(highlighted_edits.highlights.len(), 1);
22662 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22663 assert_eq!(
22664 highlighted_edits.highlights[0].1.background_color,
22665 Some(cx.theme().status().deleted_background)
22666 );
22667 },
22668 )
22669 .await;
22670
22671 // Insertion
22672 assert_highlighted_edits(
22673 "Hello, world!",
22674 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22675 true,
22676 cx,
22677 |highlighted_edits, cx| {
22678 assert_eq!(highlighted_edits.highlights.len(), 1);
22679 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22680 assert_eq!(
22681 highlighted_edits.highlights[0].1.background_color,
22682 Some(cx.theme().status().created_background)
22683 );
22684 },
22685 )
22686 .await;
22687}
22688
22689async fn assert_highlighted_edits(
22690 text: &str,
22691 edits: Vec<(Range<Point>, String)>,
22692 include_deletions: bool,
22693 cx: &mut TestAppContext,
22694 assertion_fn: impl Fn(HighlightedText, &App),
22695) {
22696 let window = cx.add_window(|window, cx| {
22697 let buffer = MultiBuffer::build_simple(text, cx);
22698 Editor::new(EditorMode::full(), buffer, None, window, cx)
22699 });
22700 let cx = &mut VisualTestContext::from_window(*window, cx);
22701
22702 let (buffer, snapshot) = window
22703 .update(cx, |editor, _window, cx| {
22704 (
22705 editor.buffer().clone(),
22706 editor.buffer().read(cx).snapshot(cx),
22707 )
22708 })
22709 .unwrap();
22710
22711 let edits = edits
22712 .into_iter()
22713 .map(|(range, edit)| {
22714 (
22715 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22716 edit,
22717 )
22718 })
22719 .collect::<Vec<_>>();
22720
22721 let text_anchor_edits = edits
22722 .clone()
22723 .into_iter()
22724 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22725 .collect::<Vec<_>>();
22726
22727 let edit_preview = window
22728 .update(cx, |_, _window, cx| {
22729 buffer
22730 .read(cx)
22731 .as_singleton()
22732 .unwrap()
22733 .read(cx)
22734 .preview_edits(text_anchor_edits.into(), cx)
22735 })
22736 .unwrap()
22737 .await;
22738
22739 cx.update(|_window, cx| {
22740 let highlighted_edits = edit_prediction_edit_text(
22741 snapshot.as_singleton().unwrap().2,
22742 &edits,
22743 &edit_preview,
22744 include_deletions,
22745 cx,
22746 );
22747 assertion_fn(highlighted_edits, cx)
22748 });
22749}
22750
22751#[track_caller]
22752fn assert_breakpoint(
22753 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22754 path: &Arc<Path>,
22755 expected: Vec<(u32, Breakpoint)>,
22756) {
22757 if expected.is_empty() {
22758 assert!(!breakpoints.contains_key(path), "{}", path.display());
22759 } else {
22760 let mut breakpoint = breakpoints
22761 .get(path)
22762 .unwrap()
22763 .iter()
22764 .map(|breakpoint| {
22765 (
22766 breakpoint.row,
22767 Breakpoint {
22768 message: breakpoint.message.clone(),
22769 state: breakpoint.state,
22770 condition: breakpoint.condition.clone(),
22771 hit_condition: breakpoint.hit_condition.clone(),
22772 },
22773 )
22774 })
22775 .collect::<Vec<_>>();
22776
22777 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22778
22779 assert_eq!(expected, breakpoint);
22780 }
22781}
22782
22783fn add_log_breakpoint_at_cursor(
22784 editor: &mut Editor,
22785 log_message: &str,
22786 window: &mut Window,
22787 cx: &mut Context<Editor>,
22788) {
22789 let (anchor, bp) = editor
22790 .breakpoints_at_cursors(window, cx)
22791 .first()
22792 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22793 .unwrap_or_else(|| {
22794 let snapshot = editor.snapshot(window, cx);
22795 let cursor_position: Point =
22796 editor.selections.newest(&snapshot.display_snapshot).head();
22797
22798 let breakpoint_position = snapshot
22799 .buffer_snapshot()
22800 .anchor_before(Point::new(cursor_position.row, 0));
22801
22802 (breakpoint_position, Breakpoint::new_log(log_message))
22803 });
22804
22805 editor.edit_breakpoint_at_anchor(
22806 anchor,
22807 bp,
22808 BreakpointEditAction::EditLogMessage(log_message.into()),
22809 cx,
22810 );
22811}
22812
22813#[gpui::test]
22814async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22815 init_test(cx, |_| {});
22816
22817 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22818 let fs = FakeFs::new(cx.executor());
22819 fs.insert_tree(
22820 path!("/a"),
22821 json!({
22822 "main.rs": sample_text,
22823 }),
22824 )
22825 .await;
22826 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22827 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22828 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22829
22830 let fs = FakeFs::new(cx.executor());
22831 fs.insert_tree(
22832 path!("/a"),
22833 json!({
22834 "main.rs": sample_text,
22835 }),
22836 )
22837 .await;
22838 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22839 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22840 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22841 let worktree_id = workspace
22842 .update(cx, |workspace, _window, cx| {
22843 workspace.project().update(cx, |project, cx| {
22844 project.worktrees(cx).next().unwrap().read(cx).id()
22845 })
22846 })
22847 .unwrap();
22848
22849 let buffer = project
22850 .update(cx, |project, cx| {
22851 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22852 })
22853 .await
22854 .unwrap();
22855
22856 let (editor, cx) = cx.add_window_view(|window, cx| {
22857 Editor::new(
22858 EditorMode::full(),
22859 MultiBuffer::build_from_buffer(buffer, cx),
22860 Some(project.clone()),
22861 window,
22862 cx,
22863 )
22864 });
22865
22866 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22867 let abs_path = project.read_with(cx, |project, cx| {
22868 project
22869 .absolute_path(&project_path, cx)
22870 .map(Arc::from)
22871 .unwrap()
22872 });
22873
22874 // assert we can add breakpoint on the first line
22875 editor.update_in(cx, |editor, window, cx| {
22876 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22877 editor.move_to_end(&MoveToEnd, window, cx);
22878 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22879 });
22880
22881 let breakpoints = editor.update(cx, |editor, cx| {
22882 editor
22883 .breakpoint_store()
22884 .as_ref()
22885 .unwrap()
22886 .read(cx)
22887 .all_source_breakpoints(cx)
22888 });
22889
22890 assert_eq!(1, breakpoints.len());
22891 assert_breakpoint(
22892 &breakpoints,
22893 &abs_path,
22894 vec![
22895 (0, Breakpoint::new_standard()),
22896 (3, Breakpoint::new_standard()),
22897 ],
22898 );
22899
22900 editor.update_in(cx, |editor, window, cx| {
22901 editor.move_to_beginning(&MoveToBeginning, window, cx);
22902 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22903 });
22904
22905 let breakpoints = editor.update(cx, |editor, cx| {
22906 editor
22907 .breakpoint_store()
22908 .as_ref()
22909 .unwrap()
22910 .read(cx)
22911 .all_source_breakpoints(cx)
22912 });
22913
22914 assert_eq!(1, breakpoints.len());
22915 assert_breakpoint(
22916 &breakpoints,
22917 &abs_path,
22918 vec![(3, Breakpoint::new_standard())],
22919 );
22920
22921 editor.update_in(cx, |editor, window, cx| {
22922 editor.move_to_end(&MoveToEnd, window, cx);
22923 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22924 });
22925
22926 let breakpoints = editor.update(cx, |editor, cx| {
22927 editor
22928 .breakpoint_store()
22929 .as_ref()
22930 .unwrap()
22931 .read(cx)
22932 .all_source_breakpoints(cx)
22933 });
22934
22935 assert_eq!(0, breakpoints.len());
22936 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22937}
22938
22939#[gpui::test]
22940async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22941 init_test(cx, |_| {});
22942
22943 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22944
22945 let fs = FakeFs::new(cx.executor());
22946 fs.insert_tree(
22947 path!("/a"),
22948 json!({
22949 "main.rs": sample_text,
22950 }),
22951 )
22952 .await;
22953 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22954 let (workspace, cx) =
22955 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22956
22957 let worktree_id = workspace.update(cx, |workspace, cx| {
22958 workspace.project().update(cx, |project, cx| {
22959 project.worktrees(cx).next().unwrap().read(cx).id()
22960 })
22961 });
22962
22963 let buffer = project
22964 .update(cx, |project, cx| {
22965 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22966 })
22967 .await
22968 .unwrap();
22969
22970 let (editor, cx) = cx.add_window_view(|window, cx| {
22971 Editor::new(
22972 EditorMode::full(),
22973 MultiBuffer::build_from_buffer(buffer, cx),
22974 Some(project.clone()),
22975 window,
22976 cx,
22977 )
22978 });
22979
22980 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22981 let abs_path = project.read_with(cx, |project, cx| {
22982 project
22983 .absolute_path(&project_path, cx)
22984 .map(Arc::from)
22985 .unwrap()
22986 });
22987
22988 editor.update_in(cx, |editor, window, cx| {
22989 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22990 });
22991
22992 let breakpoints = editor.update(cx, |editor, cx| {
22993 editor
22994 .breakpoint_store()
22995 .as_ref()
22996 .unwrap()
22997 .read(cx)
22998 .all_source_breakpoints(cx)
22999 });
23000
23001 assert_breakpoint(
23002 &breakpoints,
23003 &abs_path,
23004 vec![(0, Breakpoint::new_log("hello world"))],
23005 );
23006
23007 // Removing a log message from a log breakpoint should remove it
23008 editor.update_in(cx, |editor, window, cx| {
23009 add_log_breakpoint_at_cursor(editor, "", window, cx);
23010 });
23011
23012 let breakpoints = editor.update(cx, |editor, cx| {
23013 editor
23014 .breakpoint_store()
23015 .as_ref()
23016 .unwrap()
23017 .read(cx)
23018 .all_source_breakpoints(cx)
23019 });
23020
23021 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23022
23023 editor.update_in(cx, |editor, window, cx| {
23024 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23025 editor.move_to_end(&MoveToEnd, window, cx);
23026 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23027 // Not adding a log message to a standard breakpoint shouldn't remove it
23028 add_log_breakpoint_at_cursor(editor, "", window, cx);
23029 });
23030
23031 let breakpoints = editor.update(cx, |editor, cx| {
23032 editor
23033 .breakpoint_store()
23034 .as_ref()
23035 .unwrap()
23036 .read(cx)
23037 .all_source_breakpoints(cx)
23038 });
23039
23040 assert_breakpoint(
23041 &breakpoints,
23042 &abs_path,
23043 vec![
23044 (0, Breakpoint::new_standard()),
23045 (3, Breakpoint::new_standard()),
23046 ],
23047 );
23048
23049 editor.update_in(cx, |editor, window, cx| {
23050 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23051 });
23052
23053 let breakpoints = editor.update(cx, |editor, cx| {
23054 editor
23055 .breakpoint_store()
23056 .as_ref()
23057 .unwrap()
23058 .read(cx)
23059 .all_source_breakpoints(cx)
23060 });
23061
23062 assert_breakpoint(
23063 &breakpoints,
23064 &abs_path,
23065 vec![
23066 (0, Breakpoint::new_standard()),
23067 (3, Breakpoint::new_log("hello world")),
23068 ],
23069 );
23070
23071 editor.update_in(cx, |editor, window, cx| {
23072 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23073 });
23074
23075 let breakpoints = editor.update(cx, |editor, cx| {
23076 editor
23077 .breakpoint_store()
23078 .as_ref()
23079 .unwrap()
23080 .read(cx)
23081 .all_source_breakpoints(cx)
23082 });
23083
23084 assert_breakpoint(
23085 &breakpoints,
23086 &abs_path,
23087 vec![
23088 (0, Breakpoint::new_standard()),
23089 (3, Breakpoint::new_log("hello Earth!!")),
23090 ],
23091 );
23092}
23093
23094/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23095/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23096/// or when breakpoints were placed out of order. This tests for a regression too
23097#[gpui::test]
23098async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23099 init_test(cx, |_| {});
23100
23101 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23102 let fs = FakeFs::new(cx.executor());
23103 fs.insert_tree(
23104 path!("/a"),
23105 json!({
23106 "main.rs": sample_text,
23107 }),
23108 )
23109 .await;
23110 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23111 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23112 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23113
23114 let fs = FakeFs::new(cx.executor());
23115 fs.insert_tree(
23116 path!("/a"),
23117 json!({
23118 "main.rs": sample_text,
23119 }),
23120 )
23121 .await;
23122 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23123 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23124 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23125 let worktree_id = workspace
23126 .update(cx, |workspace, _window, cx| {
23127 workspace.project().update(cx, |project, cx| {
23128 project.worktrees(cx).next().unwrap().read(cx).id()
23129 })
23130 })
23131 .unwrap();
23132
23133 let buffer = project
23134 .update(cx, |project, cx| {
23135 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23136 })
23137 .await
23138 .unwrap();
23139
23140 let (editor, cx) = cx.add_window_view(|window, cx| {
23141 Editor::new(
23142 EditorMode::full(),
23143 MultiBuffer::build_from_buffer(buffer, cx),
23144 Some(project.clone()),
23145 window,
23146 cx,
23147 )
23148 });
23149
23150 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23151 let abs_path = project.read_with(cx, |project, cx| {
23152 project
23153 .absolute_path(&project_path, cx)
23154 .map(Arc::from)
23155 .unwrap()
23156 });
23157
23158 // assert we can add breakpoint on the first line
23159 editor.update_in(cx, |editor, window, cx| {
23160 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23161 editor.move_to_end(&MoveToEnd, window, cx);
23162 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23163 editor.move_up(&MoveUp, window, cx);
23164 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23165 });
23166
23167 let breakpoints = editor.update(cx, |editor, cx| {
23168 editor
23169 .breakpoint_store()
23170 .as_ref()
23171 .unwrap()
23172 .read(cx)
23173 .all_source_breakpoints(cx)
23174 });
23175
23176 assert_eq!(1, breakpoints.len());
23177 assert_breakpoint(
23178 &breakpoints,
23179 &abs_path,
23180 vec![
23181 (0, Breakpoint::new_standard()),
23182 (2, Breakpoint::new_standard()),
23183 (3, Breakpoint::new_standard()),
23184 ],
23185 );
23186
23187 editor.update_in(cx, |editor, window, cx| {
23188 editor.move_to_beginning(&MoveToBeginning, window, cx);
23189 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23190 editor.move_to_end(&MoveToEnd, window, cx);
23191 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23192 // Disabling a breakpoint that doesn't exist should do nothing
23193 editor.move_up(&MoveUp, window, cx);
23194 editor.move_up(&MoveUp, window, cx);
23195 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23196 });
23197
23198 let breakpoints = editor.update(cx, |editor, cx| {
23199 editor
23200 .breakpoint_store()
23201 .as_ref()
23202 .unwrap()
23203 .read(cx)
23204 .all_source_breakpoints(cx)
23205 });
23206
23207 let disable_breakpoint = {
23208 let mut bp = Breakpoint::new_standard();
23209 bp.state = BreakpointState::Disabled;
23210 bp
23211 };
23212
23213 assert_eq!(1, breakpoints.len());
23214 assert_breakpoint(
23215 &breakpoints,
23216 &abs_path,
23217 vec![
23218 (0, disable_breakpoint.clone()),
23219 (2, Breakpoint::new_standard()),
23220 (3, disable_breakpoint.clone()),
23221 ],
23222 );
23223
23224 editor.update_in(cx, |editor, window, cx| {
23225 editor.move_to_beginning(&MoveToBeginning, window, cx);
23226 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23227 editor.move_to_end(&MoveToEnd, window, cx);
23228 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23229 editor.move_up(&MoveUp, window, cx);
23230 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23231 });
23232
23233 let breakpoints = editor.update(cx, |editor, cx| {
23234 editor
23235 .breakpoint_store()
23236 .as_ref()
23237 .unwrap()
23238 .read(cx)
23239 .all_source_breakpoints(cx)
23240 });
23241
23242 assert_eq!(1, breakpoints.len());
23243 assert_breakpoint(
23244 &breakpoints,
23245 &abs_path,
23246 vec![
23247 (0, Breakpoint::new_standard()),
23248 (2, disable_breakpoint),
23249 (3, Breakpoint::new_standard()),
23250 ],
23251 );
23252}
23253
23254#[gpui::test]
23255async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23256 init_test(cx, |_| {});
23257 let capabilities = lsp::ServerCapabilities {
23258 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23259 prepare_provider: Some(true),
23260 work_done_progress_options: Default::default(),
23261 })),
23262 ..Default::default()
23263 };
23264 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23265
23266 cx.set_state(indoc! {"
23267 struct Fˇoo {}
23268 "});
23269
23270 cx.update_editor(|editor, _, cx| {
23271 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23272 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23273 editor.highlight_background::<DocumentHighlightRead>(
23274 &[highlight_range],
23275 |theme| theme.colors().editor_document_highlight_read_background,
23276 cx,
23277 );
23278 });
23279
23280 let mut prepare_rename_handler = cx
23281 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23282 move |_, _, _| async move {
23283 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23284 start: lsp::Position {
23285 line: 0,
23286 character: 7,
23287 },
23288 end: lsp::Position {
23289 line: 0,
23290 character: 10,
23291 },
23292 })))
23293 },
23294 );
23295 let prepare_rename_task = cx
23296 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23297 .expect("Prepare rename was not started");
23298 prepare_rename_handler.next().await.unwrap();
23299 prepare_rename_task.await.expect("Prepare rename failed");
23300
23301 let mut rename_handler =
23302 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23303 let edit = lsp::TextEdit {
23304 range: lsp::Range {
23305 start: lsp::Position {
23306 line: 0,
23307 character: 7,
23308 },
23309 end: lsp::Position {
23310 line: 0,
23311 character: 10,
23312 },
23313 },
23314 new_text: "FooRenamed".to_string(),
23315 };
23316 Ok(Some(lsp::WorkspaceEdit::new(
23317 // Specify the same edit twice
23318 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23319 )))
23320 });
23321 let rename_task = cx
23322 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23323 .expect("Confirm rename was not started");
23324 rename_handler.next().await.unwrap();
23325 rename_task.await.expect("Confirm rename failed");
23326 cx.run_until_parked();
23327
23328 // Despite two edits, only one is actually applied as those are identical
23329 cx.assert_editor_state(indoc! {"
23330 struct FooRenamedˇ {}
23331 "});
23332}
23333
23334#[gpui::test]
23335async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23336 init_test(cx, |_| {});
23337 // These capabilities indicate that the server does not support prepare rename.
23338 let capabilities = lsp::ServerCapabilities {
23339 rename_provider: Some(lsp::OneOf::Left(true)),
23340 ..Default::default()
23341 };
23342 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23343
23344 cx.set_state(indoc! {"
23345 struct Fˇoo {}
23346 "});
23347
23348 cx.update_editor(|editor, _window, cx| {
23349 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23350 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23351 editor.highlight_background::<DocumentHighlightRead>(
23352 &[highlight_range],
23353 |theme| theme.colors().editor_document_highlight_read_background,
23354 cx,
23355 );
23356 });
23357
23358 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23359 .expect("Prepare rename was not started")
23360 .await
23361 .expect("Prepare rename failed");
23362
23363 let mut rename_handler =
23364 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23365 let edit = lsp::TextEdit {
23366 range: lsp::Range {
23367 start: lsp::Position {
23368 line: 0,
23369 character: 7,
23370 },
23371 end: lsp::Position {
23372 line: 0,
23373 character: 10,
23374 },
23375 },
23376 new_text: "FooRenamed".to_string(),
23377 };
23378 Ok(Some(lsp::WorkspaceEdit::new(
23379 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23380 )))
23381 });
23382 let rename_task = cx
23383 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23384 .expect("Confirm rename was not started");
23385 rename_handler.next().await.unwrap();
23386 rename_task.await.expect("Confirm rename failed");
23387 cx.run_until_parked();
23388
23389 // Correct range is renamed, as `surrounding_word` is used to find it.
23390 cx.assert_editor_state(indoc! {"
23391 struct FooRenamedˇ {}
23392 "});
23393}
23394
23395#[gpui::test]
23396async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23397 init_test(cx, |_| {});
23398 let mut cx = EditorTestContext::new(cx).await;
23399
23400 let language = Arc::new(
23401 Language::new(
23402 LanguageConfig::default(),
23403 Some(tree_sitter_html::LANGUAGE.into()),
23404 )
23405 .with_brackets_query(
23406 r#"
23407 ("<" @open "/>" @close)
23408 ("</" @open ">" @close)
23409 ("<" @open ">" @close)
23410 ("\"" @open "\"" @close)
23411 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23412 "#,
23413 )
23414 .unwrap(),
23415 );
23416 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23417
23418 cx.set_state(indoc! {"
23419 <span>ˇ</span>
23420 "});
23421 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23422 cx.assert_editor_state(indoc! {"
23423 <span>
23424 ˇ
23425 </span>
23426 "});
23427
23428 cx.set_state(indoc! {"
23429 <span><span></span>ˇ</span>
23430 "});
23431 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23432 cx.assert_editor_state(indoc! {"
23433 <span><span></span>
23434 ˇ</span>
23435 "});
23436
23437 cx.set_state(indoc! {"
23438 <span>ˇ
23439 </span>
23440 "});
23441 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23442 cx.assert_editor_state(indoc! {"
23443 <span>
23444 ˇ
23445 </span>
23446 "});
23447}
23448
23449#[gpui::test(iterations = 10)]
23450async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23451 init_test(cx, |_| {});
23452
23453 let fs = FakeFs::new(cx.executor());
23454 fs.insert_tree(
23455 path!("/dir"),
23456 json!({
23457 "a.ts": "a",
23458 }),
23459 )
23460 .await;
23461
23462 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23463 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23464 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23465
23466 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23467 language_registry.add(Arc::new(Language::new(
23468 LanguageConfig {
23469 name: "TypeScript".into(),
23470 matcher: LanguageMatcher {
23471 path_suffixes: vec!["ts".to_string()],
23472 ..Default::default()
23473 },
23474 ..Default::default()
23475 },
23476 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23477 )));
23478 let mut fake_language_servers = language_registry.register_fake_lsp(
23479 "TypeScript",
23480 FakeLspAdapter {
23481 capabilities: lsp::ServerCapabilities {
23482 code_lens_provider: Some(lsp::CodeLensOptions {
23483 resolve_provider: Some(true),
23484 }),
23485 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23486 commands: vec!["_the/command".to_string()],
23487 ..lsp::ExecuteCommandOptions::default()
23488 }),
23489 ..lsp::ServerCapabilities::default()
23490 },
23491 ..FakeLspAdapter::default()
23492 },
23493 );
23494
23495 let editor = workspace
23496 .update(cx, |workspace, window, cx| {
23497 workspace.open_abs_path(
23498 PathBuf::from(path!("/dir/a.ts")),
23499 OpenOptions::default(),
23500 window,
23501 cx,
23502 )
23503 })
23504 .unwrap()
23505 .await
23506 .unwrap()
23507 .downcast::<Editor>()
23508 .unwrap();
23509 cx.executor().run_until_parked();
23510
23511 let fake_server = fake_language_servers.next().await.unwrap();
23512
23513 let buffer = editor.update(cx, |editor, cx| {
23514 editor
23515 .buffer()
23516 .read(cx)
23517 .as_singleton()
23518 .expect("have opened a single file by path")
23519 });
23520
23521 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23522 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23523 drop(buffer_snapshot);
23524 let actions = cx
23525 .update_window(*workspace, |_, window, cx| {
23526 project.code_actions(&buffer, anchor..anchor, window, cx)
23527 })
23528 .unwrap();
23529
23530 fake_server
23531 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23532 Ok(Some(vec![
23533 lsp::CodeLens {
23534 range: lsp::Range::default(),
23535 command: Some(lsp::Command {
23536 title: "Code lens command".to_owned(),
23537 command: "_the/command".to_owned(),
23538 arguments: None,
23539 }),
23540 data: None,
23541 },
23542 lsp::CodeLens {
23543 range: lsp::Range::default(),
23544 command: Some(lsp::Command {
23545 title: "Command not in capabilities".to_owned(),
23546 command: "not in capabilities".to_owned(),
23547 arguments: None,
23548 }),
23549 data: None,
23550 },
23551 lsp::CodeLens {
23552 range: lsp::Range {
23553 start: lsp::Position {
23554 line: 1,
23555 character: 1,
23556 },
23557 end: lsp::Position {
23558 line: 1,
23559 character: 1,
23560 },
23561 },
23562 command: Some(lsp::Command {
23563 title: "Command not in range".to_owned(),
23564 command: "_the/command".to_owned(),
23565 arguments: None,
23566 }),
23567 data: None,
23568 },
23569 ]))
23570 })
23571 .next()
23572 .await;
23573
23574 let actions = actions.await.unwrap();
23575 assert_eq!(
23576 actions.len(),
23577 1,
23578 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23579 );
23580 let action = actions[0].clone();
23581 let apply = project.update(cx, |project, cx| {
23582 project.apply_code_action(buffer.clone(), action, true, cx)
23583 });
23584
23585 // Resolving the code action does not populate its edits. In absence of
23586 // edits, we must execute the given command.
23587 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23588 |mut lens, _| async move {
23589 let lens_command = lens.command.as_mut().expect("should have a command");
23590 assert_eq!(lens_command.title, "Code lens command");
23591 lens_command.arguments = Some(vec![json!("the-argument")]);
23592 Ok(lens)
23593 },
23594 );
23595
23596 // While executing the command, the language server sends the editor
23597 // a `workspaceEdit` request.
23598 fake_server
23599 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23600 let fake = fake_server.clone();
23601 move |params, _| {
23602 assert_eq!(params.command, "_the/command");
23603 let fake = fake.clone();
23604 async move {
23605 fake.server
23606 .request::<lsp::request::ApplyWorkspaceEdit>(
23607 lsp::ApplyWorkspaceEditParams {
23608 label: None,
23609 edit: lsp::WorkspaceEdit {
23610 changes: Some(
23611 [(
23612 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23613 vec![lsp::TextEdit {
23614 range: lsp::Range::new(
23615 lsp::Position::new(0, 0),
23616 lsp::Position::new(0, 0),
23617 ),
23618 new_text: "X".into(),
23619 }],
23620 )]
23621 .into_iter()
23622 .collect(),
23623 ),
23624 ..lsp::WorkspaceEdit::default()
23625 },
23626 },
23627 )
23628 .await
23629 .into_response()
23630 .unwrap();
23631 Ok(Some(json!(null)))
23632 }
23633 }
23634 })
23635 .next()
23636 .await;
23637
23638 // Applying the code lens command returns a project transaction containing the edits
23639 // sent by the language server in its `workspaceEdit` request.
23640 let transaction = apply.await.unwrap();
23641 assert!(transaction.0.contains_key(&buffer));
23642 buffer.update(cx, |buffer, cx| {
23643 assert_eq!(buffer.text(), "Xa");
23644 buffer.undo(cx);
23645 assert_eq!(buffer.text(), "a");
23646 });
23647
23648 let actions_after_edits = cx
23649 .update_window(*workspace, |_, window, cx| {
23650 project.code_actions(&buffer, anchor..anchor, window, cx)
23651 })
23652 .unwrap()
23653 .await
23654 .unwrap();
23655 assert_eq!(
23656 actions, actions_after_edits,
23657 "For the same selection, same code lens actions should be returned"
23658 );
23659
23660 let _responses =
23661 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23662 panic!("No more code lens requests are expected");
23663 });
23664 editor.update_in(cx, |editor, window, cx| {
23665 editor.select_all(&SelectAll, window, cx);
23666 });
23667 cx.executor().run_until_parked();
23668 let new_actions = cx
23669 .update_window(*workspace, |_, window, cx| {
23670 project.code_actions(&buffer, anchor..anchor, window, cx)
23671 })
23672 .unwrap()
23673 .await
23674 .unwrap();
23675 assert_eq!(
23676 actions, new_actions,
23677 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23678 );
23679}
23680
23681#[gpui::test]
23682async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23683 init_test(cx, |_| {});
23684
23685 let fs = FakeFs::new(cx.executor());
23686 let main_text = r#"fn main() {
23687println!("1");
23688println!("2");
23689println!("3");
23690println!("4");
23691println!("5");
23692}"#;
23693 let lib_text = "mod foo {}";
23694 fs.insert_tree(
23695 path!("/a"),
23696 json!({
23697 "lib.rs": lib_text,
23698 "main.rs": main_text,
23699 }),
23700 )
23701 .await;
23702
23703 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23704 let (workspace, cx) =
23705 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23706 let worktree_id = workspace.update(cx, |workspace, cx| {
23707 workspace.project().update(cx, |project, cx| {
23708 project.worktrees(cx).next().unwrap().read(cx).id()
23709 })
23710 });
23711
23712 let expected_ranges = vec![
23713 Point::new(0, 0)..Point::new(0, 0),
23714 Point::new(1, 0)..Point::new(1, 1),
23715 Point::new(2, 0)..Point::new(2, 2),
23716 Point::new(3, 0)..Point::new(3, 3),
23717 ];
23718
23719 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23720 let editor_1 = workspace
23721 .update_in(cx, |workspace, window, cx| {
23722 workspace.open_path(
23723 (worktree_id, rel_path("main.rs")),
23724 Some(pane_1.downgrade()),
23725 true,
23726 window,
23727 cx,
23728 )
23729 })
23730 .unwrap()
23731 .await
23732 .downcast::<Editor>()
23733 .unwrap();
23734 pane_1.update(cx, |pane, cx| {
23735 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23736 open_editor.update(cx, |editor, cx| {
23737 assert_eq!(
23738 editor.display_text(cx),
23739 main_text,
23740 "Original main.rs text on initial open",
23741 );
23742 assert_eq!(
23743 editor
23744 .selections
23745 .all::<Point>(&editor.display_snapshot(cx))
23746 .into_iter()
23747 .map(|s| s.range())
23748 .collect::<Vec<_>>(),
23749 vec![Point::zero()..Point::zero()],
23750 "Default selections on initial open",
23751 );
23752 })
23753 });
23754 editor_1.update_in(cx, |editor, window, cx| {
23755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23756 s.select_ranges(expected_ranges.clone());
23757 });
23758 });
23759
23760 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23761 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23762 });
23763 let editor_2 = workspace
23764 .update_in(cx, |workspace, window, cx| {
23765 workspace.open_path(
23766 (worktree_id, rel_path("main.rs")),
23767 Some(pane_2.downgrade()),
23768 true,
23769 window,
23770 cx,
23771 )
23772 })
23773 .unwrap()
23774 .await
23775 .downcast::<Editor>()
23776 .unwrap();
23777 pane_2.update(cx, |pane, cx| {
23778 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23779 open_editor.update(cx, |editor, cx| {
23780 assert_eq!(
23781 editor.display_text(cx),
23782 main_text,
23783 "Original main.rs text on initial open in another panel",
23784 );
23785 assert_eq!(
23786 editor
23787 .selections
23788 .all::<Point>(&editor.display_snapshot(cx))
23789 .into_iter()
23790 .map(|s| s.range())
23791 .collect::<Vec<_>>(),
23792 vec![Point::zero()..Point::zero()],
23793 "Default selections on initial open in another panel",
23794 );
23795 })
23796 });
23797
23798 editor_2.update_in(cx, |editor, window, cx| {
23799 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23800 });
23801
23802 let _other_editor_1 = workspace
23803 .update_in(cx, |workspace, window, cx| {
23804 workspace.open_path(
23805 (worktree_id, rel_path("lib.rs")),
23806 Some(pane_1.downgrade()),
23807 true,
23808 window,
23809 cx,
23810 )
23811 })
23812 .unwrap()
23813 .await
23814 .downcast::<Editor>()
23815 .unwrap();
23816 pane_1
23817 .update_in(cx, |pane, window, cx| {
23818 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23819 })
23820 .await
23821 .unwrap();
23822 drop(editor_1);
23823 pane_1.update(cx, |pane, cx| {
23824 pane.active_item()
23825 .unwrap()
23826 .downcast::<Editor>()
23827 .unwrap()
23828 .update(cx, |editor, cx| {
23829 assert_eq!(
23830 editor.display_text(cx),
23831 lib_text,
23832 "Other file should be open and active",
23833 );
23834 });
23835 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23836 });
23837
23838 let _other_editor_2 = workspace
23839 .update_in(cx, |workspace, window, cx| {
23840 workspace.open_path(
23841 (worktree_id, rel_path("lib.rs")),
23842 Some(pane_2.downgrade()),
23843 true,
23844 window,
23845 cx,
23846 )
23847 })
23848 .unwrap()
23849 .await
23850 .downcast::<Editor>()
23851 .unwrap();
23852 pane_2
23853 .update_in(cx, |pane, window, cx| {
23854 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23855 })
23856 .await
23857 .unwrap();
23858 drop(editor_2);
23859 pane_2.update(cx, |pane, cx| {
23860 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23861 open_editor.update(cx, |editor, cx| {
23862 assert_eq!(
23863 editor.display_text(cx),
23864 lib_text,
23865 "Other file should be open and active in another panel too",
23866 );
23867 });
23868 assert_eq!(
23869 pane.items().count(),
23870 1,
23871 "No other editors should be open in another pane",
23872 );
23873 });
23874
23875 let _editor_1_reopened = workspace
23876 .update_in(cx, |workspace, window, cx| {
23877 workspace.open_path(
23878 (worktree_id, rel_path("main.rs")),
23879 Some(pane_1.downgrade()),
23880 true,
23881 window,
23882 cx,
23883 )
23884 })
23885 .unwrap()
23886 .await
23887 .downcast::<Editor>()
23888 .unwrap();
23889 let _editor_2_reopened = workspace
23890 .update_in(cx, |workspace, window, cx| {
23891 workspace.open_path(
23892 (worktree_id, rel_path("main.rs")),
23893 Some(pane_2.downgrade()),
23894 true,
23895 window,
23896 cx,
23897 )
23898 })
23899 .unwrap()
23900 .await
23901 .downcast::<Editor>()
23902 .unwrap();
23903 pane_1.update(cx, |pane, cx| {
23904 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23905 open_editor.update(cx, |editor, cx| {
23906 assert_eq!(
23907 editor.display_text(cx),
23908 main_text,
23909 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23910 );
23911 assert_eq!(
23912 editor
23913 .selections
23914 .all::<Point>(&editor.display_snapshot(cx))
23915 .into_iter()
23916 .map(|s| s.range())
23917 .collect::<Vec<_>>(),
23918 expected_ranges,
23919 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23920 );
23921 })
23922 });
23923 pane_2.update(cx, |pane, cx| {
23924 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23925 open_editor.update(cx, |editor, cx| {
23926 assert_eq!(
23927 editor.display_text(cx),
23928 r#"fn main() {
23929⋯rintln!("1");
23930⋯intln!("2");
23931⋯ntln!("3");
23932println!("4");
23933println!("5");
23934}"#,
23935 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23936 );
23937 assert_eq!(
23938 editor
23939 .selections
23940 .all::<Point>(&editor.display_snapshot(cx))
23941 .into_iter()
23942 .map(|s| s.range())
23943 .collect::<Vec<_>>(),
23944 vec![Point::zero()..Point::zero()],
23945 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23946 );
23947 })
23948 });
23949}
23950
23951#[gpui::test]
23952async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23953 init_test(cx, |_| {});
23954
23955 let fs = FakeFs::new(cx.executor());
23956 let main_text = r#"fn main() {
23957println!("1");
23958println!("2");
23959println!("3");
23960println!("4");
23961println!("5");
23962}"#;
23963 let lib_text = "mod foo {}";
23964 fs.insert_tree(
23965 path!("/a"),
23966 json!({
23967 "lib.rs": lib_text,
23968 "main.rs": main_text,
23969 }),
23970 )
23971 .await;
23972
23973 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23974 let (workspace, cx) =
23975 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23976 let worktree_id = workspace.update(cx, |workspace, cx| {
23977 workspace.project().update(cx, |project, cx| {
23978 project.worktrees(cx).next().unwrap().read(cx).id()
23979 })
23980 });
23981
23982 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23983 let editor = workspace
23984 .update_in(cx, |workspace, window, cx| {
23985 workspace.open_path(
23986 (worktree_id, rel_path("main.rs")),
23987 Some(pane.downgrade()),
23988 true,
23989 window,
23990 cx,
23991 )
23992 })
23993 .unwrap()
23994 .await
23995 .downcast::<Editor>()
23996 .unwrap();
23997 pane.update(cx, |pane, cx| {
23998 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23999 open_editor.update(cx, |editor, cx| {
24000 assert_eq!(
24001 editor.display_text(cx),
24002 main_text,
24003 "Original main.rs text on initial open",
24004 );
24005 })
24006 });
24007 editor.update_in(cx, |editor, window, cx| {
24008 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24009 });
24010
24011 cx.update_global(|store: &mut SettingsStore, cx| {
24012 store.update_user_settings(cx, |s| {
24013 s.workspace.restore_on_file_reopen = Some(false);
24014 });
24015 });
24016 editor.update_in(cx, |editor, window, cx| {
24017 editor.fold_ranges(
24018 vec![
24019 Point::new(1, 0)..Point::new(1, 1),
24020 Point::new(2, 0)..Point::new(2, 2),
24021 Point::new(3, 0)..Point::new(3, 3),
24022 ],
24023 false,
24024 window,
24025 cx,
24026 );
24027 });
24028 pane.update_in(cx, |pane, window, cx| {
24029 pane.close_all_items(&CloseAllItems::default(), window, cx)
24030 })
24031 .await
24032 .unwrap();
24033 pane.update(cx, |pane, _| {
24034 assert!(pane.active_item().is_none());
24035 });
24036 cx.update_global(|store: &mut SettingsStore, cx| {
24037 store.update_user_settings(cx, |s| {
24038 s.workspace.restore_on_file_reopen = Some(true);
24039 });
24040 });
24041
24042 let _editor_reopened = workspace
24043 .update_in(cx, |workspace, window, cx| {
24044 workspace.open_path(
24045 (worktree_id, rel_path("main.rs")),
24046 Some(pane.downgrade()),
24047 true,
24048 window,
24049 cx,
24050 )
24051 })
24052 .unwrap()
24053 .await
24054 .downcast::<Editor>()
24055 .unwrap();
24056 pane.update(cx, |pane, cx| {
24057 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24058 open_editor.update(cx, |editor, cx| {
24059 assert_eq!(
24060 editor.display_text(cx),
24061 main_text,
24062 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24063 );
24064 })
24065 });
24066}
24067
24068#[gpui::test]
24069async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24070 struct EmptyModalView {
24071 focus_handle: gpui::FocusHandle,
24072 }
24073 impl EventEmitter<DismissEvent> for EmptyModalView {}
24074 impl Render for EmptyModalView {
24075 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24076 div()
24077 }
24078 }
24079 impl Focusable for EmptyModalView {
24080 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24081 self.focus_handle.clone()
24082 }
24083 }
24084 impl workspace::ModalView for EmptyModalView {}
24085 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24086 EmptyModalView {
24087 focus_handle: cx.focus_handle(),
24088 }
24089 }
24090
24091 init_test(cx, |_| {});
24092
24093 let fs = FakeFs::new(cx.executor());
24094 let project = Project::test(fs, [], cx).await;
24095 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24096 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24097 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24098 let editor = cx.new_window_entity(|window, cx| {
24099 Editor::new(
24100 EditorMode::full(),
24101 buffer,
24102 Some(project.clone()),
24103 window,
24104 cx,
24105 )
24106 });
24107 workspace
24108 .update(cx, |workspace, window, cx| {
24109 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24110 })
24111 .unwrap();
24112 editor.update_in(cx, |editor, window, cx| {
24113 editor.open_context_menu(&OpenContextMenu, window, cx);
24114 assert!(editor.mouse_context_menu.is_some());
24115 });
24116 workspace
24117 .update(cx, |workspace, window, cx| {
24118 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24119 })
24120 .unwrap();
24121 cx.read(|cx| {
24122 assert!(editor.read(cx).mouse_context_menu.is_none());
24123 });
24124}
24125
24126fn set_linked_edit_ranges(
24127 opening: (Point, Point),
24128 closing: (Point, Point),
24129 editor: &mut Editor,
24130 cx: &mut Context<Editor>,
24131) {
24132 let Some((buffer, _)) = editor
24133 .buffer
24134 .read(cx)
24135 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24136 else {
24137 panic!("Failed to get buffer for selection position");
24138 };
24139 let buffer = buffer.read(cx);
24140 let buffer_id = buffer.remote_id();
24141 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24142 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24143 let mut linked_ranges = HashMap::default();
24144 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24145 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24146}
24147
24148#[gpui::test]
24149async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24150 init_test(cx, |_| {});
24151
24152 let fs = FakeFs::new(cx.executor());
24153 fs.insert_file(path!("/file.html"), Default::default())
24154 .await;
24155
24156 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24157
24158 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24159 let html_language = Arc::new(Language::new(
24160 LanguageConfig {
24161 name: "HTML".into(),
24162 matcher: LanguageMatcher {
24163 path_suffixes: vec!["html".to_string()],
24164 ..LanguageMatcher::default()
24165 },
24166 brackets: BracketPairConfig {
24167 pairs: vec![BracketPair {
24168 start: "<".into(),
24169 end: ">".into(),
24170 close: true,
24171 ..Default::default()
24172 }],
24173 ..Default::default()
24174 },
24175 ..Default::default()
24176 },
24177 Some(tree_sitter_html::LANGUAGE.into()),
24178 ));
24179 language_registry.add(html_language);
24180 let mut fake_servers = language_registry.register_fake_lsp(
24181 "HTML",
24182 FakeLspAdapter {
24183 capabilities: lsp::ServerCapabilities {
24184 completion_provider: Some(lsp::CompletionOptions {
24185 resolve_provider: Some(true),
24186 ..Default::default()
24187 }),
24188 ..Default::default()
24189 },
24190 ..Default::default()
24191 },
24192 );
24193
24194 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24195 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24196
24197 let worktree_id = workspace
24198 .update(cx, |workspace, _window, cx| {
24199 workspace.project().update(cx, |project, cx| {
24200 project.worktrees(cx).next().unwrap().read(cx).id()
24201 })
24202 })
24203 .unwrap();
24204 project
24205 .update(cx, |project, cx| {
24206 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24207 })
24208 .await
24209 .unwrap();
24210 let editor = workspace
24211 .update(cx, |workspace, window, cx| {
24212 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24213 })
24214 .unwrap()
24215 .await
24216 .unwrap()
24217 .downcast::<Editor>()
24218 .unwrap();
24219
24220 let fake_server = fake_servers.next().await.unwrap();
24221 editor.update_in(cx, |editor, window, cx| {
24222 editor.set_text("<ad></ad>", window, cx);
24223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24224 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24225 });
24226 set_linked_edit_ranges(
24227 (Point::new(0, 1), Point::new(0, 3)),
24228 (Point::new(0, 6), Point::new(0, 8)),
24229 editor,
24230 cx,
24231 );
24232 });
24233 let mut completion_handle =
24234 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24235 Ok(Some(lsp::CompletionResponse::Array(vec![
24236 lsp::CompletionItem {
24237 label: "head".to_string(),
24238 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24239 lsp::InsertReplaceEdit {
24240 new_text: "head".to_string(),
24241 insert: lsp::Range::new(
24242 lsp::Position::new(0, 1),
24243 lsp::Position::new(0, 3),
24244 ),
24245 replace: lsp::Range::new(
24246 lsp::Position::new(0, 1),
24247 lsp::Position::new(0, 3),
24248 ),
24249 },
24250 )),
24251 ..Default::default()
24252 },
24253 ])))
24254 });
24255 editor.update_in(cx, |editor, window, cx| {
24256 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24257 });
24258 cx.run_until_parked();
24259 completion_handle.next().await.unwrap();
24260 editor.update(cx, |editor, _| {
24261 assert!(
24262 editor.context_menu_visible(),
24263 "Completion menu should be visible"
24264 );
24265 });
24266 editor.update_in(cx, |editor, window, cx| {
24267 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24268 });
24269 cx.executor().run_until_parked();
24270 editor.update(cx, |editor, cx| {
24271 assert_eq!(editor.text(cx), "<head></head>");
24272 });
24273}
24274
24275#[gpui::test]
24276async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24277 init_test(cx, |_| {});
24278
24279 let mut cx = EditorTestContext::new(cx).await;
24280 let language = Arc::new(Language::new(
24281 LanguageConfig {
24282 name: "TSX".into(),
24283 matcher: LanguageMatcher {
24284 path_suffixes: vec!["tsx".to_string()],
24285 ..LanguageMatcher::default()
24286 },
24287 brackets: BracketPairConfig {
24288 pairs: vec![BracketPair {
24289 start: "<".into(),
24290 end: ">".into(),
24291 close: true,
24292 ..Default::default()
24293 }],
24294 ..Default::default()
24295 },
24296 linked_edit_characters: HashSet::from_iter(['.']),
24297 ..Default::default()
24298 },
24299 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24300 ));
24301 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24302
24303 // Test typing > does not extend linked pair
24304 cx.set_state("<divˇ<div></div>");
24305 cx.update_editor(|editor, _, cx| {
24306 set_linked_edit_ranges(
24307 (Point::new(0, 1), Point::new(0, 4)),
24308 (Point::new(0, 11), Point::new(0, 14)),
24309 editor,
24310 cx,
24311 );
24312 });
24313 cx.update_editor(|editor, window, cx| {
24314 editor.handle_input(">", window, cx);
24315 });
24316 cx.assert_editor_state("<div>ˇ<div></div>");
24317
24318 // Test typing . do extend linked pair
24319 cx.set_state("<Animatedˇ></Animated>");
24320 cx.update_editor(|editor, _, cx| {
24321 set_linked_edit_ranges(
24322 (Point::new(0, 1), Point::new(0, 9)),
24323 (Point::new(0, 12), Point::new(0, 20)),
24324 editor,
24325 cx,
24326 );
24327 });
24328 cx.update_editor(|editor, window, cx| {
24329 editor.handle_input(".", window, cx);
24330 });
24331 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24332 cx.update_editor(|editor, _, cx| {
24333 set_linked_edit_ranges(
24334 (Point::new(0, 1), Point::new(0, 10)),
24335 (Point::new(0, 13), Point::new(0, 21)),
24336 editor,
24337 cx,
24338 );
24339 });
24340 cx.update_editor(|editor, window, cx| {
24341 editor.handle_input("V", window, cx);
24342 });
24343 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24344}
24345
24346#[gpui::test]
24347async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24348 init_test(cx, |_| {});
24349
24350 let fs = FakeFs::new(cx.executor());
24351 fs.insert_tree(
24352 path!("/root"),
24353 json!({
24354 "a": {
24355 "main.rs": "fn main() {}",
24356 },
24357 "foo": {
24358 "bar": {
24359 "external_file.rs": "pub mod external {}",
24360 }
24361 }
24362 }),
24363 )
24364 .await;
24365
24366 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24367 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24368 language_registry.add(rust_lang());
24369 let _fake_servers = language_registry.register_fake_lsp(
24370 "Rust",
24371 FakeLspAdapter {
24372 ..FakeLspAdapter::default()
24373 },
24374 );
24375 let (workspace, cx) =
24376 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24377 let worktree_id = workspace.update(cx, |workspace, cx| {
24378 workspace.project().update(cx, |project, cx| {
24379 project.worktrees(cx).next().unwrap().read(cx).id()
24380 })
24381 });
24382
24383 let assert_language_servers_count =
24384 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24385 project.update(cx, |project, cx| {
24386 let current = project
24387 .lsp_store()
24388 .read(cx)
24389 .as_local()
24390 .unwrap()
24391 .language_servers
24392 .len();
24393 assert_eq!(expected, current, "{context}");
24394 });
24395 };
24396
24397 assert_language_servers_count(
24398 0,
24399 "No servers should be running before any file is open",
24400 cx,
24401 );
24402 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24403 let main_editor = workspace
24404 .update_in(cx, |workspace, window, cx| {
24405 workspace.open_path(
24406 (worktree_id, rel_path("main.rs")),
24407 Some(pane.downgrade()),
24408 true,
24409 window,
24410 cx,
24411 )
24412 })
24413 .unwrap()
24414 .await
24415 .downcast::<Editor>()
24416 .unwrap();
24417 pane.update(cx, |pane, cx| {
24418 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24419 open_editor.update(cx, |editor, cx| {
24420 assert_eq!(
24421 editor.display_text(cx),
24422 "fn main() {}",
24423 "Original main.rs text on initial open",
24424 );
24425 });
24426 assert_eq!(open_editor, main_editor);
24427 });
24428 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24429
24430 let external_editor = workspace
24431 .update_in(cx, |workspace, window, cx| {
24432 workspace.open_abs_path(
24433 PathBuf::from("/root/foo/bar/external_file.rs"),
24434 OpenOptions::default(),
24435 window,
24436 cx,
24437 )
24438 })
24439 .await
24440 .expect("opening external file")
24441 .downcast::<Editor>()
24442 .expect("downcasted external file's open element to editor");
24443 pane.update(cx, |pane, cx| {
24444 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24445 open_editor.update(cx, |editor, cx| {
24446 assert_eq!(
24447 editor.display_text(cx),
24448 "pub mod external {}",
24449 "External file is open now",
24450 );
24451 });
24452 assert_eq!(open_editor, external_editor);
24453 });
24454 assert_language_servers_count(
24455 1,
24456 "Second, external, *.rs file should join the existing server",
24457 cx,
24458 );
24459
24460 pane.update_in(cx, |pane, window, cx| {
24461 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24462 })
24463 .await
24464 .unwrap();
24465 pane.update_in(cx, |pane, window, cx| {
24466 pane.navigate_backward(&Default::default(), window, cx);
24467 });
24468 cx.run_until_parked();
24469 pane.update(cx, |pane, cx| {
24470 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24471 open_editor.update(cx, |editor, cx| {
24472 assert_eq!(
24473 editor.display_text(cx),
24474 "pub mod external {}",
24475 "External file is open now",
24476 );
24477 });
24478 });
24479 assert_language_servers_count(
24480 1,
24481 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24482 cx,
24483 );
24484
24485 cx.update(|_, cx| {
24486 workspace::reload(cx);
24487 });
24488 assert_language_servers_count(
24489 1,
24490 "After reloading the worktree with local and external files opened, only one project should be started",
24491 cx,
24492 );
24493}
24494
24495#[gpui::test]
24496async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24497 init_test(cx, |_| {});
24498
24499 let mut cx = EditorTestContext::new(cx).await;
24500 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24501 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24502
24503 // test cursor move to start of each line on tab
24504 // for `if`, `elif`, `else`, `while`, `with` and `for`
24505 cx.set_state(indoc! {"
24506 def main():
24507 ˇ for item in items:
24508 ˇ while item.active:
24509 ˇ if item.value > 10:
24510 ˇ continue
24511 ˇ elif item.value < 0:
24512 ˇ break
24513 ˇ else:
24514 ˇ with item.context() as ctx:
24515 ˇ yield count
24516 ˇ else:
24517 ˇ log('while else')
24518 ˇ else:
24519 ˇ log('for else')
24520 "});
24521 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24522 cx.assert_editor_state(indoc! {"
24523 def main():
24524 ˇfor item in items:
24525 ˇwhile item.active:
24526 ˇif item.value > 10:
24527 ˇcontinue
24528 ˇelif item.value < 0:
24529 ˇbreak
24530 ˇelse:
24531 ˇwith item.context() as ctx:
24532 ˇyield count
24533 ˇelse:
24534 ˇlog('while else')
24535 ˇelse:
24536 ˇlog('for else')
24537 "});
24538 // test relative indent is preserved when tab
24539 // for `if`, `elif`, `else`, `while`, `with` and `for`
24540 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24541 cx.assert_editor_state(indoc! {"
24542 def main():
24543 ˇfor item in items:
24544 ˇwhile item.active:
24545 ˇif item.value > 10:
24546 ˇcontinue
24547 ˇelif item.value < 0:
24548 ˇbreak
24549 ˇelse:
24550 ˇwith item.context() as ctx:
24551 ˇyield count
24552 ˇelse:
24553 ˇlog('while else')
24554 ˇelse:
24555 ˇlog('for else')
24556 "});
24557
24558 // test cursor move to start of each line on tab
24559 // for `try`, `except`, `else`, `finally`, `match` and `def`
24560 cx.set_state(indoc! {"
24561 def main():
24562 ˇ try:
24563 ˇ fetch()
24564 ˇ except ValueError:
24565 ˇ handle_error()
24566 ˇ else:
24567 ˇ match value:
24568 ˇ case _:
24569 ˇ finally:
24570 ˇ def status():
24571 ˇ return 0
24572 "});
24573 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24574 cx.assert_editor_state(indoc! {"
24575 def main():
24576 ˇtry:
24577 ˇfetch()
24578 ˇexcept ValueError:
24579 ˇhandle_error()
24580 ˇelse:
24581 ˇmatch value:
24582 ˇcase _:
24583 ˇfinally:
24584 ˇdef status():
24585 ˇreturn 0
24586 "});
24587 // test relative indent is preserved when tab
24588 // for `try`, `except`, `else`, `finally`, `match` and `def`
24589 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24590 cx.assert_editor_state(indoc! {"
24591 def main():
24592 ˇtry:
24593 ˇfetch()
24594 ˇexcept ValueError:
24595 ˇhandle_error()
24596 ˇelse:
24597 ˇmatch value:
24598 ˇcase _:
24599 ˇfinally:
24600 ˇdef status():
24601 ˇreturn 0
24602 "});
24603}
24604
24605#[gpui::test]
24606async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24607 init_test(cx, |_| {});
24608
24609 let mut cx = EditorTestContext::new(cx).await;
24610 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24611 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24612
24613 // test `else` auto outdents when typed inside `if` block
24614 cx.set_state(indoc! {"
24615 def main():
24616 if i == 2:
24617 return
24618 ˇ
24619 "});
24620 cx.update_editor(|editor, window, cx| {
24621 editor.handle_input("else:", window, cx);
24622 });
24623 cx.assert_editor_state(indoc! {"
24624 def main():
24625 if i == 2:
24626 return
24627 else:ˇ
24628 "});
24629
24630 // test `except` auto outdents when typed inside `try` block
24631 cx.set_state(indoc! {"
24632 def main():
24633 try:
24634 i = 2
24635 ˇ
24636 "});
24637 cx.update_editor(|editor, window, cx| {
24638 editor.handle_input("except:", window, cx);
24639 });
24640 cx.assert_editor_state(indoc! {"
24641 def main():
24642 try:
24643 i = 2
24644 except:ˇ
24645 "});
24646
24647 // test `else` auto outdents when typed inside `except` block
24648 cx.set_state(indoc! {"
24649 def main():
24650 try:
24651 i = 2
24652 except:
24653 j = 2
24654 ˇ
24655 "});
24656 cx.update_editor(|editor, window, cx| {
24657 editor.handle_input("else:", window, cx);
24658 });
24659 cx.assert_editor_state(indoc! {"
24660 def main():
24661 try:
24662 i = 2
24663 except:
24664 j = 2
24665 else:ˇ
24666 "});
24667
24668 // test `finally` auto outdents when typed inside `else` block
24669 cx.set_state(indoc! {"
24670 def main():
24671 try:
24672 i = 2
24673 except:
24674 j = 2
24675 else:
24676 k = 2
24677 ˇ
24678 "});
24679 cx.update_editor(|editor, window, cx| {
24680 editor.handle_input("finally:", window, cx);
24681 });
24682 cx.assert_editor_state(indoc! {"
24683 def main():
24684 try:
24685 i = 2
24686 except:
24687 j = 2
24688 else:
24689 k = 2
24690 finally:ˇ
24691 "});
24692
24693 // test `else` does not outdents when typed inside `except` block right after for block
24694 cx.set_state(indoc! {"
24695 def main():
24696 try:
24697 i = 2
24698 except:
24699 for i in range(n):
24700 pass
24701 ˇ
24702 "});
24703 cx.update_editor(|editor, window, cx| {
24704 editor.handle_input("else:", window, cx);
24705 });
24706 cx.assert_editor_state(indoc! {"
24707 def main():
24708 try:
24709 i = 2
24710 except:
24711 for i in range(n):
24712 pass
24713 else:ˇ
24714 "});
24715
24716 // test `finally` auto outdents when typed inside `else` block right after for block
24717 cx.set_state(indoc! {"
24718 def main():
24719 try:
24720 i = 2
24721 except:
24722 j = 2
24723 else:
24724 for i in range(n):
24725 pass
24726 ˇ
24727 "});
24728 cx.update_editor(|editor, window, cx| {
24729 editor.handle_input("finally:", window, cx);
24730 });
24731 cx.assert_editor_state(indoc! {"
24732 def main():
24733 try:
24734 i = 2
24735 except:
24736 j = 2
24737 else:
24738 for i in range(n):
24739 pass
24740 finally:ˇ
24741 "});
24742
24743 // test `except` outdents to inner "try" block
24744 cx.set_state(indoc! {"
24745 def main():
24746 try:
24747 i = 2
24748 if i == 2:
24749 try:
24750 i = 3
24751 ˇ
24752 "});
24753 cx.update_editor(|editor, window, cx| {
24754 editor.handle_input("except:", window, cx);
24755 });
24756 cx.assert_editor_state(indoc! {"
24757 def main():
24758 try:
24759 i = 2
24760 if i == 2:
24761 try:
24762 i = 3
24763 except:ˇ
24764 "});
24765
24766 // test `except` outdents to outer "try" block
24767 cx.set_state(indoc! {"
24768 def main():
24769 try:
24770 i = 2
24771 if i == 2:
24772 try:
24773 i = 3
24774 ˇ
24775 "});
24776 cx.update_editor(|editor, window, cx| {
24777 editor.handle_input("except:", window, cx);
24778 });
24779 cx.assert_editor_state(indoc! {"
24780 def main():
24781 try:
24782 i = 2
24783 if i == 2:
24784 try:
24785 i = 3
24786 except:ˇ
24787 "});
24788
24789 // test `else` stays at correct indent when typed after `for` block
24790 cx.set_state(indoc! {"
24791 def main():
24792 for i in range(10):
24793 if i == 3:
24794 break
24795 ˇ
24796 "});
24797 cx.update_editor(|editor, window, cx| {
24798 editor.handle_input("else:", window, cx);
24799 });
24800 cx.assert_editor_state(indoc! {"
24801 def main():
24802 for i in range(10):
24803 if i == 3:
24804 break
24805 else:ˇ
24806 "});
24807
24808 // test does not outdent on typing after line with square brackets
24809 cx.set_state(indoc! {"
24810 def f() -> list[str]:
24811 ˇ
24812 "});
24813 cx.update_editor(|editor, window, cx| {
24814 editor.handle_input("a", window, cx);
24815 });
24816 cx.assert_editor_state(indoc! {"
24817 def f() -> list[str]:
24818 aˇ
24819 "});
24820
24821 // test does not outdent on typing : after case keyword
24822 cx.set_state(indoc! {"
24823 match 1:
24824 caseˇ
24825 "});
24826 cx.update_editor(|editor, window, cx| {
24827 editor.handle_input(":", window, cx);
24828 });
24829 cx.assert_editor_state(indoc! {"
24830 match 1:
24831 case:ˇ
24832 "});
24833}
24834
24835#[gpui::test]
24836async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24837 init_test(cx, |_| {});
24838 update_test_language_settings(cx, |settings| {
24839 settings.defaults.extend_comment_on_newline = Some(false);
24840 });
24841 let mut cx = EditorTestContext::new(cx).await;
24842 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24843 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24844
24845 // test correct indent after newline on comment
24846 cx.set_state(indoc! {"
24847 # COMMENT:ˇ
24848 "});
24849 cx.update_editor(|editor, window, cx| {
24850 editor.newline(&Newline, window, cx);
24851 });
24852 cx.assert_editor_state(indoc! {"
24853 # COMMENT:
24854 ˇ
24855 "});
24856
24857 // test correct indent after newline in brackets
24858 cx.set_state(indoc! {"
24859 {ˇ}
24860 "});
24861 cx.update_editor(|editor, window, cx| {
24862 editor.newline(&Newline, window, cx);
24863 });
24864 cx.run_until_parked();
24865 cx.assert_editor_state(indoc! {"
24866 {
24867 ˇ
24868 }
24869 "});
24870
24871 cx.set_state(indoc! {"
24872 (ˇ)
24873 "});
24874 cx.update_editor(|editor, window, cx| {
24875 editor.newline(&Newline, window, cx);
24876 });
24877 cx.run_until_parked();
24878 cx.assert_editor_state(indoc! {"
24879 (
24880 ˇ
24881 )
24882 "});
24883
24884 // do not indent after empty lists or dictionaries
24885 cx.set_state(indoc! {"
24886 a = []ˇ
24887 "});
24888 cx.update_editor(|editor, window, cx| {
24889 editor.newline(&Newline, window, cx);
24890 });
24891 cx.run_until_parked();
24892 cx.assert_editor_state(indoc! {"
24893 a = []
24894 ˇ
24895 "});
24896}
24897
24898#[gpui::test]
24899async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24900 init_test(cx, |_| {});
24901
24902 let mut cx = EditorTestContext::new(cx).await;
24903 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24904 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24905
24906 // test cursor move to start of each line on tab
24907 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24908 cx.set_state(indoc! {"
24909 function main() {
24910 ˇ for item in $items; do
24911 ˇ while [ -n \"$item\" ]; do
24912 ˇ if [ \"$value\" -gt 10 ]; then
24913 ˇ continue
24914 ˇ elif [ \"$value\" -lt 0 ]; then
24915 ˇ break
24916 ˇ else
24917 ˇ echo \"$item\"
24918 ˇ fi
24919 ˇ done
24920 ˇ done
24921 ˇ}
24922 "});
24923 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24924 cx.assert_editor_state(indoc! {"
24925 function main() {
24926 ˇfor item in $items; do
24927 ˇwhile [ -n \"$item\" ]; do
24928 ˇif [ \"$value\" -gt 10 ]; then
24929 ˇcontinue
24930 ˇelif [ \"$value\" -lt 0 ]; then
24931 ˇbreak
24932 ˇelse
24933 ˇecho \"$item\"
24934 ˇfi
24935 ˇdone
24936 ˇdone
24937 ˇ}
24938 "});
24939 // test relative indent is preserved when tab
24940 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24941 cx.assert_editor_state(indoc! {"
24942 function main() {
24943 ˇfor item in $items; do
24944 ˇwhile [ -n \"$item\" ]; do
24945 ˇif [ \"$value\" -gt 10 ]; then
24946 ˇcontinue
24947 ˇelif [ \"$value\" -lt 0 ]; then
24948 ˇbreak
24949 ˇelse
24950 ˇecho \"$item\"
24951 ˇfi
24952 ˇdone
24953 ˇdone
24954 ˇ}
24955 "});
24956
24957 // test cursor move to start of each line on tab
24958 // for `case` statement with patterns
24959 cx.set_state(indoc! {"
24960 function handle() {
24961 ˇ case \"$1\" in
24962 ˇ start)
24963 ˇ echo \"a\"
24964 ˇ ;;
24965 ˇ stop)
24966 ˇ echo \"b\"
24967 ˇ ;;
24968 ˇ *)
24969 ˇ echo \"c\"
24970 ˇ ;;
24971 ˇ esac
24972 ˇ}
24973 "});
24974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24975 cx.assert_editor_state(indoc! {"
24976 function handle() {
24977 ˇcase \"$1\" in
24978 ˇstart)
24979 ˇecho \"a\"
24980 ˇ;;
24981 ˇstop)
24982 ˇecho \"b\"
24983 ˇ;;
24984 ˇ*)
24985 ˇecho \"c\"
24986 ˇ;;
24987 ˇesac
24988 ˇ}
24989 "});
24990}
24991
24992#[gpui::test]
24993async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24994 init_test(cx, |_| {});
24995
24996 let mut cx = EditorTestContext::new(cx).await;
24997 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24998 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24999
25000 // test indents on comment insert
25001 cx.set_state(indoc! {"
25002 function main() {
25003 ˇ for item in $items; do
25004 ˇ while [ -n \"$item\" ]; do
25005 ˇ if [ \"$value\" -gt 10 ]; then
25006 ˇ continue
25007 ˇ elif [ \"$value\" -lt 0 ]; then
25008 ˇ break
25009 ˇ else
25010 ˇ echo \"$item\"
25011 ˇ fi
25012 ˇ done
25013 ˇ done
25014 ˇ}
25015 "});
25016 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25017 cx.assert_editor_state(indoc! {"
25018 function main() {
25019 #ˇ for item in $items; do
25020 #ˇ while [ -n \"$item\" ]; do
25021 #ˇ if [ \"$value\" -gt 10 ]; then
25022 #ˇ continue
25023 #ˇ elif [ \"$value\" -lt 0 ]; then
25024 #ˇ break
25025 #ˇ else
25026 #ˇ echo \"$item\"
25027 #ˇ fi
25028 #ˇ done
25029 #ˇ done
25030 #ˇ}
25031 "});
25032}
25033
25034#[gpui::test]
25035async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25036 init_test(cx, |_| {});
25037
25038 let mut cx = EditorTestContext::new(cx).await;
25039 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25040 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25041
25042 // test `else` auto outdents when typed inside `if` block
25043 cx.set_state(indoc! {"
25044 if [ \"$1\" = \"test\" ]; then
25045 echo \"foo bar\"
25046 ˇ
25047 "});
25048 cx.update_editor(|editor, window, cx| {
25049 editor.handle_input("else", window, cx);
25050 });
25051 cx.assert_editor_state(indoc! {"
25052 if [ \"$1\" = \"test\" ]; then
25053 echo \"foo bar\"
25054 elseˇ
25055 "});
25056
25057 // test `elif` auto outdents when typed inside `if` block
25058 cx.set_state(indoc! {"
25059 if [ \"$1\" = \"test\" ]; then
25060 echo \"foo bar\"
25061 ˇ
25062 "});
25063 cx.update_editor(|editor, window, cx| {
25064 editor.handle_input("elif", window, cx);
25065 });
25066 cx.assert_editor_state(indoc! {"
25067 if [ \"$1\" = \"test\" ]; then
25068 echo \"foo bar\"
25069 elifˇ
25070 "});
25071
25072 // test `fi` auto outdents when typed inside `else` block
25073 cx.set_state(indoc! {"
25074 if [ \"$1\" = \"test\" ]; then
25075 echo \"foo bar\"
25076 else
25077 echo \"bar baz\"
25078 ˇ
25079 "});
25080 cx.update_editor(|editor, window, cx| {
25081 editor.handle_input("fi", window, cx);
25082 });
25083 cx.assert_editor_state(indoc! {"
25084 if [ \"$1\" = \"test\" ]; then
25085 echo \"foo bar\"
25086 else
25087 echo \"bar baz\"
25088 fiˇ
25089 "});
25090
25091 // test `done` auto outdents when typed inside `while` block
25092 cx.set_state(indoc! {"
25093 while read line; do
25094 echo \"$line\"
25095 ˇ
25096 "});
25097 cx.update_editor(|editor, window, cx| {
25098 editor.handle_input("done", window, cx);
25099 });
25100 cx.assert_editor_state(indoc! {"
25101 while read line; do
25102 echo \"$line\"
25103 doneˇ
25104 "});
25105
25106 // test `done` auto outdents when typed inside `for` block
25107 cx.set_state(indoc! {"
25108 for file in *.txt; do
25109 cat \"$file\"
25110 ˇ
25111 "});
25112 cx.update_editor(|editor, window, cx| {
25113 editor.handle_input("done", window, cx);
25114 });
25115 cx.assert_editor_state(indoc! {"
25116 for file in *.txt; do
25117 cat \"$file\"
25118 doneˇ
25119 "});
25120
25121 // test `esac` auto outdents when typed inside `case` block
25122 cx.set_state(indoc! {"
25123 case \"$1\" in
25124 start)
25125 echo \"foo bar\"
25126 ;;
25127 stop)
25128 echo \"bar baz\"
25129 ;;
25130 ˇ
25131 "});
25132 cx.update_editor(|editor, window, cx| {
25133 editor.handle_input("esac", window, cx);
25134 });
25135 cx.assert_editor_state(indoc! {"
25136 case \"$1\" in
25137 start)
25138 echo \"foo bar\"
25139 ;;
25140 stop)
25141 echo \"bar baz\"
25142 ;;
25143 esacˇ
25144 "});
25145
25146 // test `*)` auto outdents when typed inside `case` block
25147 cx.set_state(indoc! {"
25148 case \"$1\" in
25149 start)
25150 echo \"foo bar\"
25151 ;;
25152 ˇ
25153 "});
25154 cx.update_editor(|editor, window, cx| {
25155 editor.handle_input("*)", window, cx);
25156 });
25157 cx.assert_editor_state(indoc! {"
25158 case \"$1\" in
25159 start)
25160 echo \"foo bar\"
25161 ;;
25162 *)ˇ
25163 "});
25164
25165 // test `fi` outdents to correct level with nested if blocks
25166 cx.set_state(indoc! {"
25167 if [ \"$1\" = \"test\" ]; then
25168 echo \"outer if\"
25169 if [ \"$2\" = \"debug\" ]; then
25170 echo \"inner if\"
25171 ˇ
25172 "});
25173 cx.update_editor(|editor, window, cx| {
25174 editor.handle_input("fi", window, cx);
25175 });
25176 cx.assert_editor_state(indoc! {"
25177 if [ \"$1\" = \"test\" ]; then
25178 echo \"outer if\"
25179 if [ \"$2\" = \"debug\" ]; then
25180 echo \"inner if\"
25181 fiˇ
25182 "});
25183}
25184
25185#[gpui::test]
25186async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25187 init_test(cx, |_| {});
25188 update_test_language_settings(cx, |settings| {
25189 settings.defaults.extend_comment_on_newline = Some(false);
25190 });
25191 let mut cx = EditorTestContext::new(cx).await;
25192 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25193 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25194
25195 // test correct indent after newline on comment
25196 cx.set_state(indoc! {"
25197 # COMMENT:ˇ
25198 "});
25199 cx.update_editor(|editor, window, cx| {
25200 editor.newline(&Newline, window, cx);
25201 });
25202 cx.assert_editor_state(indoc! {"
25203 # COMMENT:
25204 ˇ
25205 "});
25206
25207 // test correct indent after newline after `then`
25208 cx.set_state(indoc! {"
25209
25210 if [ \"$1\" = \"test\" ]; thenˇ
25211 "});
25212 cx.update_editor(|editor, window, cx| {
25213 editor.newline(&Newline, window, cx);
25214 });
25215 cx.run_until_parked();
25216 cx.assert_editor_state(indoc! {"
25217
25218 if [ \"$1\" = \"test\" ]; then
25219 ˇ
25220 "});
25221
25222 // test correct indent after newline after `else`
25223 cx.set_state(indoc! {"
25224 if [ \"$1\" = \"test\" ]; then
25225 elseˇ
25226 "});
25227 cx.update_editor(|editor, window, cx| {
25228 editor.newline(&Newline, window, cx);
25229 });
25230 cx.run_until_parked();
25231 cx.assert_editor_state(indoc! {"
25232 if [ \"$1\" = \"test\" ]; then
25233 else
25234 ˇ
25235 "});
25236
25237 // test correct indent after newline after `elif`
25238 cx.set_state(indoc! {"
25239 if [ \"$1\" = \"test\" ]; then
25240 elifˇ
25241 "});
25242 cx.update_editor(|editor, window, cx| {
25243 editor.newline(&Newline, window, cx);
25244 });
25245 cx.run_until_parked();
25246 cx.assert_editor_state(indoc! {"
25247 if [ \"$1\" = \"test\" ]; then
25248 elif
25249 ˇ
25250 "});
25251
25252 // test correct indent after newline after `do`
25253 cx.set_state(indoc! {"
25254 for file in *.txt; doˇ
25255 "});
25256 cx.update_editor(|editor, window, cx| {
25257 editor.newline(&Newline, window, cx);
25258 });
25259 cx.run_until_parked();
25260 cx.assert_editor_state(indoc! {"
25261 for file in *.txt; do
25262 ˇ
25263 "});
25264
25265 // test correct indent after newline after case pattern
25266 cx.set_state(indoc! {"
25267 case \"$1\" in
25268 start)ˇ
25269 "});
25270 cx.update_editor(|editor, window, cx| {
25271 editor.newline(&Newline, window, cx);
25272 });
25273 cx.run_until_parked();
25274 cx.assert_editor_state(indoc! {"
25275 case \"$1\" in
25276 start)
25277 ˇ
25278 "});
25279
25280 // test correct indent after newline after case pattern
25281 cx.set_state(indoc! {"
25282 case \"$1\" in
25283 start)
25284 ;;
25285 *)ˇ
25286 "});
25287 cx.update_editor(|editor, window, cx| {
25288 editor.newline(&Newline, window, cx);
25289 });
25290 cx.run_until_parked();
25291 cx.assert_editor_state(indoc! {"
25292 case \"$1\" in
25293 start)
25294 ;;
25295 *)
25296 ˇ
25297 "});
25298
25299 // test correct indent after newline after function opening brace
25300 cx.set_state(indoc! {"
25301 function test() {ˇ}
25302 "});
25303 cx.update_editor(|editor, window, cx| {
25304 editor.newline(&Newline, window, cx);
25305 });
25306 cx.run_until_parked();
25307 cx.assert_editor_state(indoc! {"
25308 function test() {
25309 ˇ
25310 }
25311 "});
25312
25313 // test no extra indent after semicolon on same line
25314 cx.set_state(indoc! {"
25315 echo \"test\";ˇ
25316 "});
25317 cx.update_editor(|editor, window, cx| {
25318 editor.newline(&Newline, window, cx);
25319 });
25320 cx.run_until_parked();
25321 cx.assert_editor_state(indoc! {"
25322 echo \"test\";
25323 ˇ
25324 "});
25325}
25326
25327fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25328 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25329 point..point
25330}
25331
25332#[track_caller]
25333fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25334 let (text, ranges) = marked_text_ranges(marked_text, true);
25335 assert_eq!(editor.text(cx), text);
25336 assert_eq!(
25337 editor.selections.ranges(&editor.display_snapshot(cx)),
25338 ranges,
25339 "Assert selections are {}",
25340 marked_text
25341 );
25342}
25343
25344pub fn handle_signature_help_request(
25345 cx: &mut EditorLspTestContext,
25346 mocked_response: lsp::SignatureHelp,
25347) -> impl Future<Output = ()> + use<> {
25348 let mut request =
25349 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25350 let mocked_response = mocked_response.clone();
25351 async move { Ok(Some(mocked_response)) }
25352 });
25353
25354 async move {
25355 request.next().await;
25356 }
25357}
25358
25359#[track_caller]
25360pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25361 cx.update_editor(|editor, _, _| {
25362 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25363 let entries = menu.entries.borrow();
25364 let entries = entries
25365 .iter()
25366 .map(|entry| entry.string.as_str())
25367 .collect::<Vec<_>>();
25368 assert_eq!(entries, expected);
25369 } else {
25370 panic!("Expected completions menu");
25371 }
25372 });
25373}
25374
25375/// Handle completion request passing a marked string specifying where the completion
25376/// should be triggered from using '|' character, what range should be replaced, and what completions
25377/// should be returned using '<' and '>' to delimit the range.
25378///
25379/// Also see `handle_completion_request_with_insert_and_replace`.
25380#[track_caller]
25381pub fn handle_completion_request(
25382 marked_string: &str,
25383 completions: Vec<&'static str>,
25384 is_incomplete: bool,
25385 counter: Arc<AtomicUsize>,
25386 cx: &mut EditorLspTestContext,
25387) -> impl Future<Output = ()> {
25388 let complete_from_marker: TextRangeMarker = '|'.into();
25389 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25390 let (_, mut marked_ranges) = marked_text_ranges_by(
25391 marked_string,
25392 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25393 );
25394
25395 let complete_from_position =
25396 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25397 let replace_range =
25398 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25399
25400 let mut request =
25401 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25402 let completions = completions.clone();
25403 counter.fetch_add(1, atomic::Ordering::Release);
25404 async move {
25405 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25406 assert_eq!(
25407 params.text_document_position.position,
25408 complete_from_position
25409 );
25410 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25411 is_incomplete,
25412 item_defaults: None,
25413 items: completions
25414 .iter()
25415 .map(|completion_text| lsp::CompletionItem {
25416 label: completion_text.to_string(),
25417 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25418 range: replace_range,
25419 new_text: completion_text.to_string(),
25420 })),
25421 ..Default::default()
25422 })
25423 .collect(),
25424 })))
25425 }
25426 });
25427
25428 async move {
25429 request.next().await;
25430 }
25431}
25432
25433/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25434/// given instead, which also contains an `insert` range.
25435///
25436/// This function uses markers to define ranges:
25437/// - `|` marks the cursor position
25438/// - `<>` marks the replace range
25439/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25440pub fn handle_completion_request_with_insert_and_replace(
25441 cx: &mut EditorLspTestContext,
25442 marked_string: &str,
25443 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25444 counter: Arc<AtomicUsize>,
25445) -> impl Future<Output = ()> {
25446 let complete_from_marker: TextRangeMarker = '|'.into();
25447 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25448 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25449
25450 let (_, mut marked_ranges) = marked_text_ranges_by(
25451 marked_string,
25452 vec![
25453 complete_from_marker.clone(),
25454 replace_range_marker.clone(),
25455 insert_range_marker.clone(),
25456 ],
25457 );
25458
25459 let complete_from_position =
25460 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25461 let replace_range =
25462 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25463
25464 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25465 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25466 _ => lsp::Range {
25467 start: replace_range.start,
25468 end: complete_from_position,
25469 },
25470 };
25471
25472 let mut request =
25473 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25474 let completions = completions.clone();
25475 counter.fetch_add(1, atomic::Ordering::Release);
25476 async move {
25477 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25478 assert_eq!(
25479 params.text_document_position.position, complete_from_position,
25480 "marker `|` position doesn't match",
25481 );
25482 Ok(Some(lsp::CompletionResponse::Array(
25483 completions
25484 .iter()
25485 .map(|(label, new_text)| lsp::CompletionItem {
25486 label: label.to_string(),
25487 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25488 lsp::InsertReplaceEdit {
25489 insert: insert_range,
25490 replace: replace_range,
25491 new_text: new_text.to_string(),
25492 },
25493 )),
25494 ..Default::default()
25495 })
25496 .collect(),
25497 )))
25498 }
25499 });
25500
25501 async move {
25502 request.next().await;
25503 }
25504}
25505
25506fn handle_resolve_completion_request(
25507 cx: &mut EditorLspTestContext,
25508 edits: Option<Vec<(&'static str, &'static str)>>,
25509) -> impl Future<Output = ()> {
25510 let edits = edits.map(|edits| {
25511 edits
25512 .iter()
25513 .map(|(marked_string, new_text)| {
25514 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25515 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25516 lsp::TextEdit::new(replace_range, new_text.to_string())
25517 })
25518 .collect::<Vec<_>>()
25519 });
25520
25521 let mut request =
25522 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25523 let edits = edits.clone();
25524 async move {
25525 Ok(lsp::CompletionItem {
25526 additional_text_edits: edits,
25527 ..Default::default()
25528 })
25529 }
25530 });
25531
25532 async move {
25533 request.next().await;
25534 }
25535}
25536
25537pub(crate) fn update_test_language_settings(
25538 cx: &mut TestAppContext,
25539 f: impl Fn(&mut AllLanguageSettingsContent),
25540) {
25541 cx.update(|cx| {
25542 SettingsStore::update_global(cx, |store, cx| {
25543 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25544 });
25545 });
25546}
25547
25548pub(crate) fn update_test_project_settings(
25549 cx: &mut TestAppContext,
25550 f: impl Fn(&mut ProjectSettingsContent),
25551) {
25552 cx.update(|cx| {
25553 SettingsStore::update_global(cx, |store, cx| {
25554 store.update_user_settings(cx, |settings| f(&mut settings.project));
25555 });
25556 });
25557}
25558
25559pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25560 cx.update(|cx| {
25561 assets::Assets.load_test_fonts(cx);
25562 let store = SettingsStore::test(cx);
25563 cx.set_global(store);
25564 theme::init(theme::LoadThemes::JustBase, cx);
25565 release_channel::init(SemanticVersion::default(), cx);
25566 client::init_settings(cx);
25567 language::init(cx);
25568 Project::init_settings(cx);
25569 workspace::init_settings(cx);
25570 crate::init(cx);
25571 });
25572 zlog::init_test();
25573 update_test_language_settings(cx, f);
25574}
25575
25576#[track_caller]
25577fn assert_hunk_revert(
25578 not_reverted_text_with_selections: &str,
25579 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25580 expected_reverted_text_with_selections: &str,
25581 base_text: &str,
25582 cx: &mut EditorLspTestContext,
25583) {
25584 cx.set_state(not_reverted_text_with_selections);
25585 cx.set_head_text(base_text);
25586 cx.executor().run_until_parked();
25587
25588 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25589 let snapshot = editor.snapshot(window, cx);
25590 let reverted_hunk_statuses = snapshot
25591 .buffer_snapshot()
25592 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25593 .map(|hunk| hunk.status().kind)
25594 .collect::<Vec<_>>();
25595
25596 editor.git_restore(&Default::default(), window, cx);
25597 reverted_hunk_statuses
25598 });
25599 cx.executor().run_until_parked();
25600 cx.assert_editor_state(expected_reverted_text_with_selections);
25601 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25602}
25603
25604#[gpui::test(iterations = 10)]
25605async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25606 init_test(cx, |_| {});
25607
25608 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25609 let counter = diagnostic_requests.clone();
25610
25611 let fs = FakeFs::new(cx.executor());
25612 fs.insert_tree(
25613 path!("/a"),
25614 json!({
25615 "first.rs": "fn main() { let a = 5; }",
25616 "second.rs": "// Test file",
25617 }),
25618 )
25619 .await;
25620
25621 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25622 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25623 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25624
25625 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25626 language_registry.add(rust_lang());
25627 let mut fake_servers = language_registry.register_fake_lsp(
25628 "Rust",
25629 FakeLspAdapter {
25630 capabilities: lsp::ServerCapabilities {
25631 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25632 lsp::DiagnosticOptions {
25633 identifier: None,
25634 inter_file_dependencies: true,
25635 workspace_diagnostics: true,
25636 work_done_progress_options: Default::default(),
25637 },
25638 )),
25639 ..Default::default()
25640 },
25641 ..Default::default()
25642 },
25643 );
25644
25645 let editor = workspace
25646 .update(cx, |workspace, window, cx| {
25647 workspace.open_abs_path(
25648 PathBuf::from(path!("/a/first.rs")),
25649 OpenOptions::default(),
25650 window,
25651 cx,
25652 )
25653 })
25654 .unwrap()
25655 .await
25656 .unwrap()
25657 .downcast::<Editor>()
25658 .unwrap();
25659 let fake_server = fake_servers.next().await.unwrap();
25660 let server_id = fake_server.server.server_id();
25661 let mut first_request = fake_server
25662 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25663 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25664 let result_id = Some(new_result_id.to_string());
25665 assert_eq!(
25666 params.text_document.uri,
25667 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25668 );
25669 async move {
25670 Ok(lsp::DocumentDiagnosticReportResult::Report(
25671 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25672 related_documents: None,
25673 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25674 items: Vec::new(),
25675 result_id,
25676 },
25677 }),
25678 ))
25679 }
25680 });
25681
25682 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25683 project.update(cx, |project, cx| {
25684 let buffer_id = editor
25685 .read(cx)
25686 .buffer()
25687 .read(cx)
25688 .as_singleton()
25689 .expect("created a singleton buffer")
25690 .read(cx)
25691 .remote_id();
25692 let buffer_result_id = project
25693 .lsp_store()
25694 .read(cx)
25695 .result_id(server_id, buffer_id, cx);
25696 assert_eq!(expected, buffer_result_id);
25697 });
25698 };
25699
25700 ensure_result_id(None, cx);
25701 cx.executor().advance_clock(Duration::from_millis(60));
25702 cx.executor().run_until_parked();
25703 assert_eq!(
25704 diagnostic_requests.load(atomic::Ordering::Acquire),
25705 1,
25706 "Opening file should trigger diagnostic request"
25707 );
25708 first_request
25709 .next()
25710 .await
25711 .expect("should have sent the first diagnostics pull request");
25712 ensure_result_id(Some("1".to_string()), cx);
25713
25714 // Editing should trigger diagnostics
25715 editor.update_in(cx, |editor, window, cx| {
25716 editor.handle_input("2", window, cx)
25717 });
25718 cx.executor().advance_clock(Duration::from_millis(60));
25719 cx.executor().run_until_parked();
25720 assert_eq!(
25721 diagnostic_requests.load(atomic::Ordering::Acquire),
25722 2,
25723 "Editing should trigger diagnostic request"
25724 );
25725 ensure_result_id(Some("2".to_string()), cx);
25726
25727 // Moving cursor should not trigger diagnostic request
25728 editor.update_in(cx, |editor, window, cx| {
25729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25730 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25731 });
25732 });
25733 cx.executor().advance_clock(Duration::from_millis(60));
25734 cx.executor().run_until_parked();
25735 assert_eq!(
25736 diagnostic_requests.load(atomic::Ordering::Acquire),
25737 2,
25738 "Cursor movement should not trigger diagnostic request"
25739 );
25740 ensure_result_id(Some("2".to_string()), cx);
25741 // Multiple rapid edits should be debounced
25742 for _ in 0..5 {
25743 editor.update_in(cx, |editor, window, cx| {
25744 editor.handle_input("x", window, cx)
25745 });
25746 }
25747 cx.executor().advance_clock(Duration::from_millis(60));
25748 cx.executor().run_until_parked();
25749
25750 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25751 assert!(
25752 final_requests <= 4,
25753 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25754 );
25755 ensure_result_id(Some(final_requests.to_string()), cx);
25756}
25757
25758#[gpui::test]
25759async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25760 // Regression test for issue #11671
25761 // Previously, adding a cursor after moving multiple cursors would reset
25762 // the cursor count instead of adding to the existing cursors.
25763 init_test(cx, |_| {});
25764 let mut cx = EditorTestContext::new(cx).await;
25765
25766 // Create a simple buffer with cursor at start
25767 cx.set_state(indoc! {"
25768 ˇaaaa
25769 bbbb
25770 cccc
25771 dddd
25772 eeee
25773 ffff
25774 gggg
25775 hhhh"});
25776
25777 // Add 2 cursors below (so we have 3 total)
25778 cx.update_editor(|editor, window, cx| {
25779 editor.add_selection_below(&Default::default(), window, cx);
25780 editor.add_selection_below(&Default::default(), window, cx);
25781 });
25782
25783 // Verify we have 3 cursors
25784 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25785 assert_eq!(
25786 initial_count, 3,
25787 "Should have 3 cursors after adding 2 below"
25788 );
25789
25790 // Move down one line
25791 cx.update_editor(|editor, window, cx| {
25792 editor.move_down(&MoveDown, window, cx);
25793 });
25794
25795 // Add another cursor below
25796 cx.update_editor(|editor, window, cx| {
25797 editor.add_selection_below(&Default::default(), window, cx);
25798 });
25799
25800 // Should now have 4 cursors (3 original + 1 new)
25801 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25802 assert_eq!(
25803 final_count, 4,
25804 "Should have 4 cursors after moving and adding another"
25805 );
25806}
25807
25808#[gpui::test]
25809async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25810 init_test(cx, |_| {});
25811
25812 let mut cx = EditorTestContext::new(cx).await;
25813
25814 cx.set_state(indoc!(
25815 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25816 Second line here"#
25817 ));
25818
25819 cx.update_editor(|editor, window, cx| {
25820 // Enable soft wrapping with a narrow width to force soft wrapping and
25821 // confirm that more than 2 rows are being displayed.
25822 editor.set_wrap_width(Some(100.0.into()), cx);
25823 assert!(editor.display_text(cx).lines().count() > 2);
25824
25825 editor.add_selection_below(
25826 &AddSelectionBelow {
25827 skip_soft_wrap: true,
25828 },
25829 window,
25830 cx,
25831 );
25832
25833 assert_eq!(
25834 editor.selections.display_ranges(cx),
25835 &[
25836 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25837 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25838 ]
25839 );
25840
25841 editor.add_selection_above(
25842 &AddSelectionAbove {
25843 skip_soft_wrap: true,
25844 },
25845 window,
25846 cx,
25847 );
25848
25849 assert_eq!(
25850 editor.selections.display_ranges(cx),
25851 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25852 );
25853
25854 editor.add_selection_below(
25855 &AddSelectionBelow {
25856 skip_soft_wrap: false,
25857 },
25858 window,
25859 cx,
25860 );
25861
25862 assert_eq!(
25863 editor.selections.display_ranges(cx),
25864 &[
25865 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25866 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25867 ]
25868 );
25869
25870 editor.add_selection_above(
25871 &AddSelectionAbove {
25872 skip_soft_wrap: false,
25873 },
25874 window,
25875 cx,
25876 );
25877
25878 assert_eq!(
25879 editor.selections.display_ranges(cx),
25880 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25881 );
25882 });
25883}
25884
25885#[gpui::test(iterations = 10)]
25886async fn test_document_colors(cx: &mut TestAppContext) {
25887 let expected_color = Rgba {
25888 r: 0.33,
25889 g: 0.33,
25890 b: 0.33,
25891 a: 0.33,
25892 };
25893
25894 init_test(cx, |_| {});
25895
25896 let fs = FakeFs::new(cx.executor());
25897 fs.insert_tree(
25898 path!("/a"),
25899 json!({
25900 "first.rs": "fn main() { let a = 5; }",
25901 }),
25902 )
25903 .await;
25904
25905 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25906 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25907 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25908
25909 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25910 language_registry.add(rust_lang());
25911 let mut fake_servers = language_registry.register_fake_lsp(
25912 "Rust",
25913 FakeLspAdapter {
25914 capabilities: lsp::ServerCapabilities {
25915 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25916 ..lsp::ServerCapabilities::default()
25917 },
25918 name: "rust-analyzer",
25919 ..FakeLspAdapter::default()
25920 },
25921 );
25922 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25923 "Rust",
25924 FakeLspAdapter {
25925 capabilities: lsp::ServerCapabilities {
25926 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25927 ..lsp::ServerCapabilities::default()
25928 },
25929 name: "not-rust-analyzer",
25930 ..FakeLspAdapter::default()
25931 },
25932 );
25933
25934 let editor = workspace
25935 .update(cx, |workspace, window, cx| {
25936 workspace.open_abs_path(
25937 PathBuf::from(path!("/a/first.rs")),
25938 OpenOptions::default(),
25939 window,
25940 cx,
25941 )
25942 })
25943 .unwrap()
25944 .await
25945 .unwrap()
25946 .downcast::<Editor>()
25947 .unwrap();
25948 let fake_language_server = fake_servers.next().await.unwrap();
25949 let fake_language_server_without_capabilities =
25950 fake_servers_without_capabilities.next().await.unwrap();
25951 let requests_made = Arc::new(AtomicUsize::new(0));
25952 let closure_requests_made = Arc::clone(&requests_made);
25953 let mut color_request_handle = fake_language_server
25954 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25955 let requests_made = Arc::clone(&closure_requests_made);
25956 async move {
25957 assert_eq!(
25958 params.text_document.uri,
25959 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25960 );
25961 requests_made.fetch_add(1, atomic::Ordering::Release);
25962 Ok(vec![
25963 lsp::ColorInformation {
25964 range: lsp::Range {
25965 start: lsp::Position {
25966 line: 0,
25967 character: 0,
25968 },
25969 end: lsp::Position {
25970 line: 0,
25971 character: 1,
25972 },
25973 },
25974 color: lsp::Color {
25975 red: 0.33,
25976 green: 0.33,
25977 blue: 0.33,
25978 alpha: 0.33,
25979 },
25980 },
25981 lsp::ColorInformation {
25982 range: lsp::Range {
25983 start: lsp::Position {
25984 line: 0,
25985 character: 0,
25986 },
25987 end: lsp::Position {
25988 line: 0,
25989 character: 1,
25990 },
25991 },
25992 color: lsp::Color {
25993 red: 0.33,
25994 green: 0.33,
25995 blue: 0.33,
25996 alpha: 0.33,
25997 },
25998 },
25999 ])
26000 }
26001 });
26002
26003 let _handle = fake_language_server_without_capabilities
26004 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26005 panic!("Should not be called");
26006 });
26007 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26008 color_request_handle.next().await.unwrap();
26009 cx.run_until_parked();
26010 assert_eq!(
26011 1,
26012 requests_made.load(atomic::Ordering::Acquire),
26013 "Should query for colors once per editor open"
26014 );
26015 editor.update_in(cx, |editor, _, cx| {
26016 assert_eq!(
26017 vec![expected_color],
26018 extract_color_inlays(editor, cx),
26019 "Should have an initial inlay"
26020 );
26021 });
26022
26023 // opening another file in a split should not influence the LSP query counter
26024 workspace
26025 .update(cx, |workspace, window, cx| {
26026 assert_eq!(
26027 workspace.panes().len(),
26028 1,
26029 "Should have one pane with one editor"
26030 );
26031 workspace.move_item_to_pane_in_direction(
26032 &MoveItemToPaneInDirection {
26033 direction: SplitDirection::Right,
26034 focus: false,
26035 clone: true,
26036 },
26037 window,
26038 cx,
26039 );
26040 })
26041 .unwrap();
26042 cx.run_until_parked();
26043 workspace
26044 .update(cx, |workspace, _, cx| {
26045 let panes = workspace.panes();
26046 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26047 for pane in panes {
26048 let editor = pane
26049 .read(cx)
26050 .active_item()
26051 .and_then(|item| item.downcast::<Editor>())
26052 .expect("Should have opened an editor in each split");
26053 let editor_file = editor
26054 .read(cx)
26055 .buffer()
26056 .read(cx)
26057 .as_singleton()
26058 .expect("test deals with singleton buffers")
26059 .read(cx)
26060 .file()
26061 .expect("test buffese should have a file")
26062 .path();
26063 assert_eq!(
26064 editor_file.as_ref(),
26065 rel_path("first.rs"),
26066 "Both editors should be opened for the same file"
26067 )
26068 }
26069 })
26070 .unwrap();
26071
26072 cx.executor().advance_clock(Duration::from_millis(500));
26073 let save = editor.update_in(cx, |editor, window, cx| {
26074 editor.move_to_end(&MoveToEnd, window, cx);
26075 editor.handle_input("dirty", window, cx);
26076 editor.save(
26077 SaveOptions {
26078 format: true,
26079 autosave: true,
26080 },
26081 project.clone(),
26082 window,
26083 cx,
26084 )
26085 });
26086 save.await.unwrap();
26087
26088 color_request_handle.next().await.unwrap();
26089 cx.run_until_parked();
26090 assert_eq!(
26091 2,
26092 requests_made.load(atomic::Ordering::Acquire),
26093 "Should query for colors once per save (deduplicated) and once per formatting after save"
26094 );
26095
26096 drop(editor);
26097 let close = workspace
26098 .update(cx, |workspace, window, cx| {
26099 workspace.active_pane().update(cx, |pane, cx| {
26100 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26101 })
26102 })
26103 .unwrap();
26104 close.await.unwrap();
26105 let close = workspace
26106 .update(cx, |workspace, window, cx| {
26107 workspace.active_pane().update(cx, |pane, cx| {
26108 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26109 })
26110 })
26111 .unwrap();
26112 close.await.unwrap();
26113 assert_eq!(
26114 2,
26115 requests_made.load(atomic::Ordering::Acquire),
26116 "After saving and closing all editors, no extra requests should be made"
26117 );
26118 workspace
26119 .update(cx, |workspace, _, cx| {
26120 assert!(
26121 workspace.active_item(cx).is_none(),
26122 "Should close all editors"
26123 )
26124 })
26125 .unwrap();
26126
26127 workspace
26128 .update(cx, |workspace, window, cx| {
26129 workspace.active_pane().update(cx, |pane, cx| {
26130 pane.navigate_backward(&workspace::GoBack, window, cx);
26131 })
26132 })
26133 .unwrap();
26134 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26135 cx.run_until_parked();
26136 let editor = workspace
26137 .update(cx, |workspace, _, cx| {
26138 workspace
26139 .active_item(cx)
26140 .expect("Should have reopened the editor again after navigating back")
26141 .downcast::<Editor>()
26142 .expect("Should be an editor")
26143 })
26144 .unwrap();
26145
26146 assert_eq!(
26147 2,
26148 requests_made.load(atomic::Ordering::Acquire),
26149 "Cache should be reused on buffer close and reopen"
26150 );
26151 editor.update(cx, |editor, cx| {
26152 assert_eq!(
26153 vec![expected_color],
26154 extract_color_inlays(editor, cx),
26155 "Should have an initial inlay"
26156 );
26157 });
26158
26159 drop(color_request_handle);
26160 let closure_requests_made = Arc::clone(&requests_made);
26161 let mut empty_color_request_handle = fake_language_server
26162 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26163 let requests_made = Arc::clone(&closure_requests_made);
26164 async move {
26165 assert_eq!(
26166 params.text_document.uri,
26167 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26168 );
26169 requests_made.fetch_add(1, atomic::Ordering::Release);
26170 Ok(Vec::new())
26171 }
26172 });
26173 let save = editor.update_in(cx, |editor, window, cx| {
26174 editor.move_to_end(&MoveToEnd, window, cx);
26175 editor.handle_input("dirty_again", window, cx);
26176 editor.save(
26177 SaveOptions {
26178 format: false,
26179 autosave: true,
26180 },
26181 project.clone(),
26182 window,
26183 cx,
26184 )
26185 });
26186 save.await.unwrap();
26187
26188 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26189 empty_color_request_handle.next().await.unwrap();
26190 cx.run_until_parked();
26191 assert_eq!(
26192 3,
26193 requests_made.load(atomic::Ordering::Acquire),
26194 "Should query for colors once per save only, as formatting was not requested"
26195 );
26196 editor.update(cx, |editor, cx| {
26197 assert_eq!(
26198 Vec::<Rgba>::new(),
26199 extract_color_inlays(editor, cx),
26200 "Should clear all colors when the server returns an empty response"
26201 );
26202 });
26203}
26204
26205#[gpui::test]
26206async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26207 init_test(cx, |_| {});
26208 let (editor, cx) = cx.add_window_view(Editor::single_line);
26209 editor.update_in(cx, |editor, window, cx| {
26210 editor.set_text("oops\n\nwow\n", window, cx)
26211 });
26212 cx.run_until_parked();
26213 editor.update(cx, |editor, cx| {
26214 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26215 });
26216 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26217 cx.run_until_parked();
26218 editor.update(cx, |editor, cx| {
26219 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26220 });
26221}
26222
26223#[gpui::test]
26224async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26225 init_test(cx, |_| {});
26226
26227 cx.update(|cx| {
26228 register_project_item::<Editor>(cx);
26229 });
26230
26231 let fs = FakeFs::new(cx.executor());
26232 fs.insert_tree("/root1", json!({})).await;
26233 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26234 .await;
26235
26236 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26237 let (workspace, cx) =
26238 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26239
26240 let worktree_id = project.update(cx, |project, cx| {
26241 project.worktrees(cx).next().unwrap().read(cx).id()
26242 });
26243
26244 let handle = workspace
26245 .update_in(cx, |workspace, window, cx| {
26246 let project_path = (worktree_id, rel_path("one.pdf"));
26247 workspace.open_path(project_path, None, true, window, cx)
26248 })
26249 .await
26250 .unwrap();
26251
26252 assert_eq!(
26253 handle.to_any().entity_type(),
26254 TypeId::of::<InvalidItemView>()
26255 );
26256}
26257
26258#[gpui::test]
26259async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26260 init_test(cx, |_| {});
26261
26262 let language = Arc::new(Language::new(
26263 LanguageConfig::default(),
26264 Some(tree_sitter_rust::LANGUAGE.into()),
26265 ));
26266
26267 // Test hierarchical sibling navigation
26268 let text = r#"
26269 fn outer() {
26270 if condition {
26271 let a = 1;
26272 }
26273 let b = 2;
26274 }
26275
26276 fn another() {
26277 let c = 3;
26278 }
26279 "#;
26280
26281 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26282 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26283 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26284
26285 // Wait for parsing to complete
26286 editor
26287 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26288 .await;
26289
26290 editor.update_in(cx, |editor, window, cx| {
26291 // Start by selecting "let a = 1;" inside the if block
26292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26293 s.select_display_ranges([
26294 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26295 ]);
26296 });
26297
26298 let initial_selection = editor.selections.display_ranges(cx);
26299 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26300
26301 // Test select next sibling - should move up levels to find the next sibling
26302 // Since "let a = 1;" has no siblings in the if block, it should move up
26303 // to find "let b = 2;" which is a sibling of the if block
26304 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26305 let next_selection = editor.selections.display_ranges(cx);
26306
26307 // Should have a selection and it should be different from the initial
26308 assert_eq!(
26309 next_selection.len(),
26310 1,
26311 "Should have one selection after next"
26312 );
26313 assert_ne!(
26314 next_selection[0], initial_selection[0],
26315 "Next sibling selection should be different"
26316 );
26317
26318 // Test hierarchical navigation by going to the end of the current function
26319 // and trying to navigate to the next function
26320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26321 s.select_display_ranges([
26322 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26323 ]);
26324 });
26325
26326 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26327 let function_next_selection = editor.selections.display_ranges(cx);
26328
26329 // Should move to the next function
26330 assert_eq!(
26331 function_next_selection.len(),
26332 1,
26333 "Should have one selection after function next"
26334 );
26335
26336 // Test select previous sibling navigation
26337 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26338 let prev_selection = editor.selections.display_ranges(cx);
26339
26340 // Should have a selection and it should be different
26341 assert_eq!(
26342 prev_selection.len(),
26343 1,
26344 "Should have one selection after prev"
26345 );
26346 assert_ne!(
26347 prev_selection[0], function_next_selection[0],
26348 "Previous sibling selection should be different from next"
26349 );
26350 });
26351}
26352
26353#[gpui::test]
26354async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26355 init_test(cx, |_| {});
26356
26357 let mut cx = EditorTestContext::new(cx).await;
26358 cx.set_state(
26359 "let ˇvariable = 42;
26360let another = variable + 1;
26361let result = variable * 2;",
26362 );
26363
26364 // Set up document highlights manually (simulating LSP response)
26365 cx.update_editor(|editor, _window, cx| {
26366 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26367
26368 // Create highlights for "variable" occurrences
26369 let highlight_ranges = [
26370 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26371 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26372 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26373 ];
26374
26375 let anchor_ranges: Vec<_> = highlight_ranges
26376 .iter()
26377 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26378 .collect();
26379
26380 editor.highlight_background::<DocumentHighlightRead>(
26381 &anchor_ranges,
26382 |theme| theme.colors().editor_document_highlight_read_background,
26383 cx,
26384 );
26385 });
26386
26387 // Go to next highlight - should move to second "variable"
26388 cx.update_editor(|editor, window, cx| {
26389 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26390 });
26391 cx.assert_editor_state(
26392 "let variable = 42;
26393let another = ˇvariable + 1;
26394let result = variable * 2;",
26395 );
26396
26397 // Go to next highlight - should move to third "variable"
26398 cx.update_editor(|editor, window, cx| {
26399 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26400 });
26401 cx.assert_editor_state(
26402 "let variable = 42;
26403let another = variable + 1;
26404let result = ˇvariable * 2;",
26405 );
26406
26407 // Go to next highlight - should stay at third "variable" (no wrap-around)
26408 cx.update_editor(|editor, window, cx| {
26409 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26410 });
26411 cx.assert_editor_state(
26412 "let variable = 42;
26413let another = variable + 1;
26414let result = ˇvariable * 2;",
26415 );
26416
26417 // Now test going backwards from third position
26418 cx.update_editor(|editor, window, cx| {
26419 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26420 });
26421 cx.assert_editor_state(
26422 "let variable = 42;
26423let another = ˇvariable + 1;
26424let result = variable * 2;",
26425 );
26426
26427 // Go to previous highlight - should move to first "variable"
26428 cx.update_editor(|editor, window, cx| {
26429 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26430 });
26431 cx.assert_editor_state(
26432 "let ˇvariable = 42;
26433let another = variable + 1;
26434let result = variable * 2;",
26435 );
26436
26437 // Go to previous highlight - should stay on first "variable"
26438 cx.update_editor(|editor, window, cx| {
26439 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26440 });
26441 cx.assert_editor_state(
26442 "let ˇvariable = 42;
26443let another = variable + 1;
26444let result = variable * 2;",
26445 );
26446}
26447
26448#[gpui::test]
26449async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26450 cx: &mut gpui::TestAppContext,
26451) {
26452 init_test(cx, |_| {});
26453
26454 let url = "https://zed.dev";
26455
26456 let markdown_language = Arc::new(Language::new(
26457 LanguageConfig {
26458 name: "Markdown".into(),
26459 ..LanguageConfig::default()
26460 },
26461 None,
26462 ));
26463
26464 let mut cx = EditorTestContext::new(cx).await;
26465 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26466 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26467
26468 cx.update_editor(|editor, window, cx| {
26469 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26470 editor.paste(&Paste, window, cx);
26471 });
26472
26473 cx.assert_editor_state(&format!(
26474 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26475 ));
26476}
26477
26478#[gpui::test]
26479async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26480 cx: &mut gpui::TestAppContext,
26481) {
26482 init_test(cx, |_| {});
26483
26484 let url = "https://zed.dev";
26485
26486 let markdown_language = Arc::new(Language::new(
26487 LanguageConfig {
26488 name: "Markdown".into(),
26489 ..LanguageConfig::default()
26490 },
26491 None,
26492 ));
26493
26494 let mut cx = EditorTestContext::new(cx).await;
26495 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26496 cx.set_state(&format!(
26497 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26498 ));
26499
26500 cx.update_editor(|editor, window, cx| {
26501 editor.copy(&Copy, window, cx);
26502 });
26503
26504 cx.set_state(&format!(
26505 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26506 ));
26507
26508 cx.update_editor(|editor, window, cx| {
26509 editor.paste(&Paste, window, cx);
26510 });
26511
26512 cx.assert_editor_state(&format!(
26513 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26514 ));
26515}
26516
26517#[gpui::test]
26518async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26519 cx: &mut gpui::TestAppContext,
26520) {
26521 init_test(cx, |_| {});
26522
26523 let url = "https://zed.dev";
26524
26525 let markdown_language = Arc::new(Language::new(
26526 LanguageConfig {
26527 name: "Markdown".into(),
26528 ..LanguageConfig::default()
26529 },
26530 None,
26531 ));
26532
26533 let mut cx = EditorTestContext::new(cx).await;
26534 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26535 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26536
26537 cx.update_editor(|editor, window, cx| {
26538 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26539 editor.paste(&Paste, window, cx);
26540 });
26541
26542 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26543}
26544
26545#[gpui::test]
26546async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26547 cx: &mut gpui::TestAppContext,
26548) {
26549 init_test(cx, |_| {});
26550
26551 let text = "Awesome";
26552
26553 let markdown_language = Arc::new(Language::new(
26554 LanguageConfig {
26555 name: "Markdown".into(),
26556 ..LanguageConfig::default()
26557 },
26558 None,
26559 ));
26560
26561 let mut cx = EditorTestContext::new(cx).await;
26562 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26563 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26564
26565 cx.update_editor(|editor, window, cx| {
26566 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26567 editor.paste(&Paste, window, cx);
26568 });
26569
26570 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26571}
26572
26573#[gpui::test]
26574async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26575 cx: &mut gpui::TestAppContext,
26576) {
26577 init_test(cx, |_| {});
26578
26579 let url = "https://zed.dev";
26580
26581 let markdown_language = Arc::new(Language::new(
26582 LanguageConfig {
26583 name: "Rust".into(),
26584 ..LanguageConfig::default()
26585 },
26586 None,
26587 ));
26588
26589 let mut cx = EditorTestContext::new(cx).await;
26590 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26591 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26592
26593 cx.update_editor(|editor, window, cx| {
26594 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26595 editor.paste(&Paste, window, cx);
26596 });
26597
26598 cx.assert_editor_state(&format!(
26599 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26600 ));
26601}
26602
26603#[gpui::test]
26604async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26605 cx: &mut TestAppContext,
26606) {
26607 init_test(cx, |_| {});
26608
26609 let url = "https://zed.dev";
26610
26611 let markdown_language = Arc::new(Language::new(
26612 LanguageConfig {
26613 name: "Markdown".into(),
26614 ..LanguageConfig::default()
26615 },
26616 None,
26617 ));
26618
26619 let (editor, cx) = cx.add_window_view(|window, cx| {
26620 let multi_buffer = MultiBuffer::build_multi(
26621 [
26622 ("this will embed -> link", vec![Point::row_range(0..1)]),
26623 ("this will replace -> link", vec![Point::row_range(0..1)]),
26624 ],
26625 cx,
26626 );
26627 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26628 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26629 s.select_ranges(vec![
26630 Point::new(0, 19)..Point::new(0, 23),
26631 Point::new(1, 21)..Point::new(1, 25),
26632 ])
26633 });
26634 let first_buffer_id = multi_buffer
26635 .read(cx)
26636 .excerpt_buffer_ids()
26637 .into_iter()
26638 .next()
26639 .unwrap();
26640 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26641 first_buffer.update(cx, |buffer, cx| {
26642 buffer.set_language(Some(markdown_language.clone()), cx);
26643 });
26644
26645 editor
26646 });
26647 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26648
26649 cx.update_editor(|editor, window, cx| {
26650 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26651 editor.paste(&Paste, window, cx);
26652 });
26653
26654 cx.assert_editor_state(&format!(
26655 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26656 ));
26657}
26658
26659#[gpui::test]
26660async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26661 init_test(cx, |_| {});
26662
26663 let fs = FakeFs::new(cx.executor());
26664 fs.insert_tree(
26665 path!("/project"),
26666 json!({
26667 "first.rs": "# First Document\nSome content here.",
26668 "second.rs": "Plain text content for second file.",
26669 }),
26670 )
26671 .await;
26672
26673 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26674 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26675 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26676
26677 let language = rust_lang();
26678 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26679 language_registry.add(language.clone());
26680 let mut fake_servers = language_registry.register_fake_lsp(
26681 "Rust",
26682 FakeLspAdapter {
26683 ..FakeLspAdapter::default()
26684 },
26685 );
26686
26687 let buffer1 = project
26688 .update(cx, |project, cx| {
26689 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26690 })
26691 .await
26692 .unwrap();
26693 let buffer2 = project
26694 .update(cx, |project, cx| {
26695 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26696 })
26697 .await
26698 .unwrap();
26699
26700 let multi_buffer = cx.new(|cx| {
26701 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26702 multi_buffer.set_excerpts_for_path(
26703 PathKey::for_buffer(&buffer1, cx),
26704 buffer1.clone(),
26705 [Point::zero()..buffer1.read(cx).max_point()],
26706 3,
26707 cx,
26708 );
26709 multi_buffer.set_excerpts_for_path(
26710 PathKey::for_buffer(&buffer2, cx),
26711 buffer2.clone(),
26712 [Point::zero()..buffer1.read(cx).max_point()],
26713 3,
26714 cx,
26715 );
26716 multi_buffer
26717 });
26718
26719 let (editor, cx) = cx.add_window_view(|window, cx| {
26720 Editor::new(
26721 EditorMode::full(),
26722 multi_buffer,
26723 Some(project.clone()),
26724 window,
26725 cx,
26726 )
26727 });
26728
26729 let fake_language_server = fake_servers.next().await.unwrap();
26730
26731 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26732
26733 let save = editor.update_in(cx, |editor, window, cx| {
26734 assert!(editor.is_dirty(cx));
26735
26736 editor.save(
26737 SaveOptions {
26738 format: true,
26739 autosave: true,
26740 },
26741 project,
26742 window,
26743 cx,
26744 )
26745 });
26746 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26747 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26748 let mut done_edit_rx = Some(done_edit_rx);
26749 let mut start_edit_tx = Some(start_edit_tx);
26750
26751 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26752 start_edit_tx.take().unwrap().send(()).unwrap();
26753 let done_edit_rx = done_edit_rx.take().unwrap();
26754 async move {
26755 done_edit_rx.await.unwrap();
26756 Ok(None)
26757 }
26758 });
26759
26760 start_edit_rx.await.unwrap();
26761 buffer2
26762 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26763 .unwrap();
26764
26765 done_edit_tx.send(()).unwrap();
26766
26767 save.await.unwrap();
26768 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26769}
26770
26771#[track_caller]
26772fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26773 editor
26774 .all_inlays(cx)
26775 .into_iter()
26776 .filter_map(|inlay| inlay.get_color())
26777 .map(Rgba::from)
26778 .collect()
26779}
26780
26781#[gpui::test]
26782fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26783 init_test(cx, |_| {});
26784
26785 let editor = cx.add_window(|window, cx| {
26786 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26787 build_editor(buffer, window, cx)
26788 });
26789
26790 editor
26791 .update(cx, |editor, window, cx| {
26792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26793 s.select_display_ranges([
26794 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26795 ])
26796 });
26797
26798 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26799
26800 assert_eq!(
26801 editor.display_text(cx),
26802 "line1\nline2\nline2",
26803 "Duplicating last line upward should create duplicate above, not on same line"
26804 );
26805
26806 assert_eq!(
26807 editor.selections.display_ranges(cx),
26808 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26809 "Selection should move to the duplicated line"
26810 );
26811 })
26812 .unwrap();
26813}
26814
26815#[gpui::test]
26816async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26817 init_test(cx, |_| {});
26818
26819 let mut cx = EditorTestContext::new(cx).await;
26820
26821 cx.set_state("line1\nline2ˇ");
26822
26823 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26824
26825 let clipboard_text = cx
26826 .read_from_clipboard()
26827 .and_then(|item| item.text().as_deref().map(str::to_string));
26828
26829 assert_eq!(
26830 clipboard_text,
26831 Some("line2\n".to_string()),
26832 "Copying a line without trailing newline should include a newline"
26833 );
26834
26835 cx.set_state("line1\nˇ");
26836
26837 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26838
26839 cx.assert_editor_state("line1\nline2\nˇ");
26840}
26841
26842#[gpui::test]
26843async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26844 init_test(cx, |_| {});
26845
26846 let mut cx = EditorTestContext::new(cx).await;
26847
26848 cx.set_state("line1\nline2ˇ");
26849 cx.update_editor(|e, window, cx| {
26850 e.set_mode(EditorMode::SingleLine);
26851 assert!(e.key_context(window, cx).contains("end_of_input"));
26852 });
26853 cx.set_state("ˇline1\nline2");
26854 cx.update_editor(|e, window, cx| {
26855 assert!(!e.key_context(window, cx).contains("end_of_input"));
26856 });
26857 cx.set_state("line1ˇ\nline2");
26858 cx.update_editor(|e, window, cx| {
26859 assert!(!e.key_context(window, cx).contains("end_of_input"));
26860 });
26861}
26862
26863#[gpui::test]
26864async fn test_next_prev_reference(cx: &mut TestAppContext) {
26865 const CYCLE_POSITIONS: &[&'static str] = &[
26866 indoc! {"
26867 fn foo() {
26868 let ˇabc = 123;
26869 let x = abc + 1;
26870 let y = abc + 2;
26871 let z = abc + 2;
26872 }
26873 "},
26874 indoc! {"
26875 fn foo() {
26876 let abc = 123;
26877 let x = ˇabc + 1;
26878 let y = abc + 2;
26879 let z = abc + 2;
26880 }
26881 "},
26882 indoc! {"
26883 fn foo() {
26884 let abc = 123;
26885 let x = abc + 1;
26886 let y = ˇabc + 2;
26887 let z = abc + 2;
26888 }
26889 "},
26890 indoc! {"
26891 fn foo() {
26892 let abc = 123;
26893 let x = abc + 1;
26894 let y = abc + 2;
26895 let z = ˇabc + 2;
26896 }
26897 "},
26898 ];
26899
26900 init_test(cx, |_| {});
26901
26902 let mut cx = EditorLspTestContext::new_rust(
26903 lsp::ServerCapabilities {
26904 references_provider: Some(lsp::OneOf::Left(true)),
26905 ..Default::default()
26906 },
26907 cx,
26908 )
26909 .await;
26910
26911 // importantly, the cursor is in the middle
26912 cx.set_state(indoc! {"
26913 fn foo() {
26914 let aˇbc = 123;
26915 let x = abc + 1;
26916 let y = abc + 2;
26917 let z = abc + 2;
26918 }
26919 "});
26920
26921 let reference_ranges = [
26922 lsp::Position::new(1, 8),
26923 lsp::Position::new(2, 12),
26924 lsp::Position::new(3, 12),
26925 lsp::Position::new(4, 12),
26926 ]
26927 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
26928
26929 cx.lsp
26930 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
26931 Ok(Some(
26932 reference_ranges
26933 .map(|range| lsp::Location {
26934 uri: params.text_document_position.text_document.uri.clone(),
26935 range,
26936 })
26937 .to_vec(),
26938 ))
26939 });
26940
26941 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
26942 cx.update_editor(|editor, window, cx| {
26943 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
26944 })
26945 .unwrap()
26946 .await
26947 .unwrap()
26948 };
26949
26950 _move(Direction::Next, 1, &mut cx).await;
26951 cx.assert_editor_state(CYCLE_POSITIONS[1]);
26952
26953 _move(Direction::Next, 1, &mut cx).await;
26954 cx.assert_editor_state(CYCLE_POSITIONS[2]);
26955
26956 _move(Direction::Next, 1, &mut cx).await;
26957 cx.assert_editor_state(CYCLE_POSITIONS[3]);
26958
26959 // loops back to the start
26960 _move(Direction::Next, 1, &mut cx).await;
26961 cx.assert_editor_state(CYCLE_POSITIONS[0]);
26962
26963 // loops back to the end
26964 _move(Direction::Prev, 1, &mut cx).await;
26965 cx.assert_editor_state(CYCLE_POSITIONS[3]);
26966
26967 _move(Direction::Prev, 1, &mut cx).await;
26968 cx.assert_editor_state(CYCLE_POSITIONS[2]);
26969
26970 _move(Direction::Prev, 1, &mut cx).await;
26971 cx.assert_editor_state(CYCLE_POSITIONS[1]);
26972
26973 _move(Direction::Prev, 1, &mut cx).await;
26974 cx.assert_editor_state(CYCLE_POSITIONS[0]);
26975
26976 _move(Direction::Next, 3, &mut cx).await;
26977 cx.assert_editor_state(CYCLE_POSITIONS[3]);
26978
26979 _move(Direction::Prev, 2, &mut cx).await;
26980 cx.assert_editor_state(CYCLE_POSITIONS[1]);
26981}