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
12633 #[cfg(target_os = "windows")]
12634 let line_ending = "\r\n";
12635 #[cfg(not(target_os = "windows"))]
12636 let line_ending = "\n";
12637
12638 // Handle formatting requests to the language server.
12639 cx.lsp
12640 .set_request_handler::<lsp::request::Formatting, _, _>({
12641 let buffer_changes = buffer_changes.clone();
12642 move |_, _| {
12643 let buffer_changes = buffer_changes.clone();
12644 // Insert blank lines between each line of the buffer.
12645 async move {
12646 // When formatting is requested, trailing whitespace has already been stripped,
12647 // and the trailing newline has already been added.
12648 assert_eq!(
12649 &buffer_changes.lock()[1..],
12650 &[
12651 (
12652 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12653 "".into()
12654 ),
12655 (
12656 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12657 "".into()
12658 ),
12659 (
12660 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12661 line_ending.into()
12662 ),
12663 ]
12664 );
12665
12666 Ok(Some(vec![
12667 lsp::TextEdit {
12668 range: lsp::Range::new(
12669 lsp::Position::new(1, 0),
12670 lsp::Position::new(1, 0),
12671 ),
12672 new_text: line_ending.into(),
12673 },
12674 lsp::TextEdit {
12675 range: lsp::Range::new(
12676 lsp::Position::new(2, 0),
12677 lsp::Position::new(2, 0),
12678 ),
12679 new_text: line_ending.into(),
12680 },
12681 ]))
12682 }
12683 }
12684 });
12685
12686 // Set up a buffer white some trailing whitespace and no trailing newline.
12687 cx.set_state(
12688 &[
12689 "one ", //
12690 "twoˇ", //
12691 "three ", //
12692 "four", //
12693 ]
12694 .join("\n"),
12695 );
12696 cx.run_until_parked();
12697
12698 // Submit a format request.
12699 let format = cx
12700 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12701 .unwrap();
12702
12703 cx.run_until_parked();
12704 // After formatting the buffer, the trailing whitespace is stripped,
12705 // a newline is appended, and the edits provided by the language server
12706 // have been applied.
12707 format.await.unwrap();
12708
12709 cx.assert_editor_state(
12710 &[
12711 "one", //
12712 "", //
12713 "twoˇ", //
12714 "", //
12715 "three", //
12716 "four", //
12717 "", //
12718 ]
12719 .join("\n"),
12720 );
12721
12722 // Undoing the formatting undoes the trailing whitespace removal, the
12723 // trailing newline, and the LSP edits.
12724 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12725 cx.assert_editor_state(
12726 &[
12727 "one ", //
12728 "twoˇ", //
12729 "three ", //
12730 "four", //
12731 ]
12732 .join("\n"),
12733 );
12734}
12735
12736#[gpui::test]
12737async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12738 cx: &mut TestAppContext,
12739) {
12740 init_test(cx, |_| {});
12741
12742 cx.update(|cx| {
12743 cx.update_global::<SettingsStore, _>(|settings, cx| {
12744 settings.update_user_settings(cx, |settings| {
12745 settings.editor.auto_signature_help = Some(true);
12746 });
12747 });
12748 });
12749
12750 let mut cx = EditorLspTestContext::new_rust(
12751 lsp::ServerCapabilities {
12752 signature_help_provider: Some(lsp::SignatureHelpOptions {
12753 ..Default::default()
12754 }),
12755 ..Default::default()
12756 },
12757 cx,
12758 )
12759 .await;
12760
12761 let language = Language::new(
12762 LanguageConfig {
12763 name: "Rust".into(),
12764 brackets: BracketPairConfig {
12765 pairs: vec![
12766 BracketPair {
12767 start: "{".to_string(),
12768 end: "}".to_string(),
12769 close: true,
12770 surround: true,
12771 newline: true,
12772 },
12773 BracketPair {
12774 start: "(".to_string(),
12775 end: ")".to_string(),
12776 close: true,
12777 surround: true,
12778 newline: true,
12779 },
12780 BracketPair {
12781 start: "/*".to_string(),
12782 end: " */".to_string(),
12783 close: true,
12784 surround: true,
12785 newline: true,
12786 },
12787 BracketPair {
12788 start: "[".to_string(),
12789 end: "]".to_string(),
12790 close: false,
12791 surround: false,
12792 newline: true,
12793 },
12794 BracketPair {
12795 start: "\"".to_string(),
12796 end: "\"".to_string(),
12797 close: true,
12798 surround: true,
12799 newline: false,
12800 },
12801 BracketPair {
12802 start: "<".to_string(),
12803 end: ">".to_string(),
12804 close: false,
12805 surround: true,
12806 newline: true,
12807 },
12808 ],
12809 ..Default::default()
12810 },
12811 autoclose_before: "})]".to_string(),
12812 ..Default::default()
12813 },
12814 Some(tree_sitter_rust::LANGUAGE.into()),
12815 );
12816 let language = Arc::new(language);
12817
12818 cx.language_registry().add(language.clone());
12819 cx.update_buffer(|buffer, cx| {
12820 buffer.set_language(Some(language), cx);
12821 });
12822
12823 cx.set_state(
12824 &r#"
12825 fn main() {
12826 sampleˇ
12827 }
12828 "#
12829 .unindent(),
12830 );
12831
12832 cx.update_editor(|editor, window, cx| {
12833 editor.handle_input("(", window, cx);
12834 });
12835 cx.assert_editor_state(
12836 &"
12837 fn main() {
12838 sample(ˇ)
12839 }
12840 "
12841 .unindent(),
12842 );
12843
12844 let mocked_response = lsp::SignatureHelp {
12845 signatures: vec![lsp::SignatureInformation {
12846 label: "fn sample(param1: u8, param2: u8)".to_string(),
12847 documentation: None,
12848 parameters: Some(vec![
12849 lsp::ParameterInformation {
12850 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12851 documentation: None,
12852 },
12853 lsp::ParameterInformation {
12854 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12855 documentation: None,
12856 },
12857 ]),
12858 active_parameter: None,
12859 }],
12860 active_signature: Some(0),
12861 active_parameter: Some(0),
12862 };
12863 handle_signature_help_request(&mut cx, mocked_response).await;
12864
12865 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12866 .await;
12867
12868 cx.editor(|editor, _, _| {
12869 let signature_help_state = editor.signature_help_state.popover().cloned();
12870 let signature = signature_help_state.unwrap();
12871 assert_eq!(
12872 signature.signatures[signature.current_signature].label,
12873 "fn sample(param1: u8, param2: u8)"
12874 );
12875 });
12876}
12877
12878#[gpui::test]
12879async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12880 init_test(cx, |_| {});
12881
12882 cx.update(|cx| {
12883 cx.update_global::<SettingsStore, _>(|settings, cx| {
12884 settings.update_user_settings(cx, |settings| {
12885 settings.editor.auto_signature_help = Some(false);
12886 settings.editor.show_signature_help_after_edits = Some(false);
12887 });
12888 });
12889 });
12890
12891 let mut cx = EditorLspTestContext::new_rust(
12892 lsp::ServerCapabilities {
12893 signature_help_provider: Some(lsp::SignatureHelpOptions {
12894 ..Default::default()
12895 }),
12896 ..Default::default()
12897 },
12898 cx,
12899 )
12900 .await;
12901
12902 let language = Language::new(
12903 LanguageConfig {
12904 name: "Rust".into(),
12905 brackets: BracketPairConfig {
12906 pairs: vec![
12907 BracketPair {
12908 start: "{".to_string(),
12909 end: "}".to_string(),
12910 close: true,
12911 surround: true,
12912 newline: true,
12913 },
12914 BracketPair {
12915 start: "(".to_string(),
12916 end: ")".to_string(),
12917 close: true,
12918 surround: true,
12919 newline: true,
12920 },
12921 BracketPair {
12922 start: "/*".to_string(),
12923 end: " */".to_string(),
12924 close: true,
12925 surround: true,
12926 newline: true,
12927 },
12928 BracketPair {
12929 start: "[".to_string(),
12930 end: "]".to_string(),
12931 close: false,
12932 surround: false,
12933 newline: true,
12934 },
12935 BracketPair {
12936 start: "\"".to_string(),
12937 end: "\"".to_string(),
12938 close: true,
12939 surround: true,
12940 newline: false,
12941 },
12942 BracketPair {
12943 start: "<".to_string(),
12944 end: ">".to_string(),
12945 close: false,
12946 surround: true,
12947 newline: true,
12948 },
12949 ],
12950 ..Default::default()
12951 },
12952 autoclose_before: "})]".to_string(),
12953 ..Default::default()
12954 },
12955 Some(tree_sitter_rust::LANGUAGE.into()),
12956 );
12957 let language = Arc::new(language);
12958
12959 cx.language_registry().add(language.clone());
12960 cx.update_buffer(|buffer, cx| {
12961 buffer.set_language(Some(language), cx);
12962 });
12963
12964 // Ensure that signature_help is not called when no signature help is enabled.
12965 cx.set_state(
12966 &r#"
12967 fn main() {
12968 sampleˇ
12969 }
12970 "#
12971 .unindent(),
12972 );
12973 cx.update_editor(|editor, window, cx| {
12974 editor.handle_input("(", window, cx);
12975 });
12976 cx.assert_editor_state(
12977 &"
12978 fn main() {
12979 sample(ˇ)
12980 }
12981 "
12982 .unindent(),
12983 );
12984 cx.editor(|editor, _, _| {
12985 assert!(editor.signature_help_state.task().is_none());
12986 });
12987
12988 let mocked_response = lsp::SignatureHelp {
12989 signatures: vec![lsp::SignatureInformation {
12990 label: "fn sample(param1: u8, param2: u8)".to_string(),
12991 documentation: None,
12992 parameters: Some(vec![
12993 lsp::ParameterInformation {
12994 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12995 documentation: None,
12996 },
12997 lsp::ParameterInformation {
12998 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12999 documentation: None,
13000 },
13001 ]),
13002 active_parameter: None,
13003 }],
13004 active_signature: Some(0),
13005 active_parameter: Some(0),
13006 };
13007
13008 // Ensure that signature_help is called when enabled afte edits
13009 cx.update(|_, cx| {
13010 cx.update_global::<SettingsStore, _>(|settings, cx| {
13011 settings.update_user_settings(cx, |settings| {
13012 settings.editor.auto_signature_help = Some(false);
13013 settings.editor.show_signature_help_after_edits = Some(true);
13014 });
13015 });
13016 });
13017 cx.set_state(
13018 &r#"
13019 fn main() {
13020 sampleˇ
13021 }
13022 "#
13023 .unindent(),
13024 );
13025 cx.update_editor(|editor, window, cx| {
13026 editor.handle_input("(", window, cx);
13027 });
13028 cx.assert_editor_state(
13029 &"
13030 fn main() {
13031 sample(ˇ)
13032 }
13033 "
13034 .unindent(),
13035 );
13036 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13037 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13038 .await;
13039 cx.update_editor(|editor, _, _| {
13040 let signature_help_state = editor.signature_help_state.popover().cloned();
13041 assert!(signature_help_state.is_some());
13042 let signature = signature_help_state.unwrap();
13043 assert_eq!(
13044 signature.signatures[signature.current_signature].label,
13045 "fn sample(param1: u8, param2: u8)"
13046 );
13047 editor.signature_help_state = SignatureHelpState::default();
13048 });
13049
13050 // Ensure that signature_help is called when auto signature help override is enabled
13051 cx.update(|_, cx| {
13052 cx.update_global::<SettingsStore, _>(|settings, cx| {
13053 settings.update_user_settings(cx, |settings| {
13054 settings.editor.auto_signature_help = Some(true);
13055 settings.editor.show_signature_help_after_edits = Some(false);
13056 });
13057 });
13058 });
13059 cx.set_state(
13060 &r#"
13061 fn main() {
13062 sampleˇ
13063 }
13064 "#
13065 .unindent(),
13066 );
13067 cx.update_editor(|editor, window, cx| {
13068 editor.handle_input("(", window, cx);
13069 });
13070 cx.assert_editor_state(
13071 &"
13072 fn main() {
13073 sample(ˇ)
13074 }
13075 "
13076 .unindent(),
13077 );
13078 handle_signature_help_request(&mut cx, mocked_response).await;
13079 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13080 .await;
13081 cx.editor(|editor, _, _| {
13082 let signature_help_state = editor.signature_help_state.popover().cloned();
13083 assert!(signature_help_state.is_some());
13084 let signature = signature_help_state.unwrap();
13085 assert_eq!(
13086 signature.signatures[signature.current_signature].label,
13087 "fn sample(param1: u8, param2: u8)"
13088 );
13089 });
13090}
13091
13092#[gpui::test]
13093async fn test_signature_help(cx: &mut TestAppContext) {
13094 init_test(cx, |_| {});
13095 cx.update(|cx| {
13096 cx.update_global::<SettingsStore, _>(|settings, cx| {
13097 settings.update_user_settings(cx, |settings| {
13098 settings.editor.auto_signature_help = Some(true);
13099 });
13100 });
13101 });
13102
13103 let mut cx = EditorLspTestContext::new_rust(
13104 lsp::ServerCapabilities {
13105 signature_help_provider: Some(lsp::SignatureHelpOptions {
13106 ..Default::default()
13107 }),
13108 ..Default::default()
13109 },
13110 cx,
13111 )
13112 .await;
13113
13114 // A test that directly calls `show_signature_help`
13115 cx.update_editor(|editor, window, cx| {
13116 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13117 });
13118
13119 let mocked_response = lsp::SignatureHelp {
13120 signatures: vec![lsp::SignatureInformation {
13121 label: "fn sample(param1: u8, param2: u8)".to_string(),
13122 documentation: None,
13123 parameters: Some(vec![
13124 lsp::ParameterInformation {
13125 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13126 documentation: None,
13127 },
13128 lsp::ParameterInformation {
13129 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13130 documentation: None,
13131 },
13132 ]),
13133 active_parameter: None,
13134 }],
13135 active_signature: Some(0),
13136 active_parameter: Some(0),
13137 };
13138 handle_signature_help_request(&mut cx, mocked_response).await;
13139
13140 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13141 .await;
13142
13143 cx.editor(|editor, _, _| {
13144 let signature_help_state = editor.signature_help_state.popover().cloned();
13145 assert!(signature_help_state.is_some());
13146 let signature = signature_help_state.unwrap();
13147 assert_eq!(
13148 signature.signatures[signature.current_signature].label,
13149 "fn sample(param1: u8, param2: u8)"
13150 );
13151 });
13152
13153 // When exiting outside from inside the brackets, `signature_help` is closed.
13154 cx.set_state(indoc! {"
13155 fn main() {
13156 sample(ˇ);
13157 }
13158
13159 fn sample(param1: u8, param2: u8) {}
13160 "});
13161
13162 cx.update_editor(|editor, window, cx| {
13163 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13164 s.select_ranges([0..0])
13165 });
13166 });
13167
13168 let mocked_response = lsp::SignatureHelp {
13169 signatures: Vec::new(),
13170 active_signature: None,
13171 active_parameter: None,
13172 };
13173 handle_signature_help_request(&mut cx, mocked_response).await;
13174
13175 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13176 .await;
13177
13178 cx.editor(|editor, _, _| {
13179 assert!(!editor.signature_help_state.is_shown());
13180 });
13181
13182 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13183 cx.set_state(indoc! {"
13184 fn main() {
13185 sample(ˇ);
13186 }
13187
13188 fn sample(param1: u8, param2: u8) {}
13189 "});
13190
13191 let mocked_response = lsp::SignatureHelp {
13192 signatures: vec![lsp::SignatureInformation {
13193 label: "fn sample(param1: u8, param2: u8)".to_string(),
13194 documentation: None,
13195 parameters: Some(vec![
13196 lsp::ParameterInformation {
13197 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13198 documentation: None,
13199 },
13200 lsp::ParameterInformation {
13201 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13202 documentation: None,
13203 },
13204 ]),
13205 active_parameter: None,
13206 }],
13207 active_signature: Some(0),
13208 active_parameter: Some(0),
13209 };
13210 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13211 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13212 .await;
13213 cx.editor(|editor, _, _| {
13214 assert!(editor.signature_help_state.is_shown());
13215 });
13216
13217 // Restore the popover with more parameter input
13218 cx.set_state(indoc! {"
13219 fn main() {
13220 sample(param1, param2ˇ);
13221 }
13222
13223 fn sample(param1: u8, param2: u8) {}
13224 "});
13225
13226 let mocked_response = lsp::SignatureHelp {
13227 signatures: vec![lsp::SignatureInformation {
13228 label: "fn sample(param1: u8, param2: u8)".to_string(),
13229 documentation: None,
13230 parameters: Some(vec![
13231 lsp::ParameterInformation {
13232 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13233 documentation: None,
13234 },
13235 lsp::ParameterInformation {
13236 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13237 documentation: None,
13238 },
13239 ]),
13240 active_parameter: None,
13241 }],
13242 active_signature: Some(0),
13243 active_parameter: Some(1),
13244 };
13245 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13246 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13247 .await;
13248
13249 // When selecting a range, the popover is gone.
13250 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13251 cx.update_editor(|editor, window, cx| {
13252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13253 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13254 })
13255 });
13256 cx.assert_editor_state(indoc! {"
13257 fn main() {
13258 sample(param1, «ˇparam2»);
13259 }
13260
13261 fn sample(param1: u8, param2: u8) {}
13262 "});
13263 cx.editor(|editor, _, _| {
13264 assert!(!editor.signature_help_state.is_shown());
13265 });
13266
13267 // When unselecting again, the popover is back if within the brackets.
13268 cx.update_editor(|editor, window, cx| {
13269 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13270 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13271 })
13272 });
13273 cx.assert_editor_state(indoc! {"
13274 fn main() {
13275 sample(param1, ˇparam2);
13276 }
13277
13278 fn sample(param1: u8, param2: u8) {}
13279 "});
13280 handle_signature_help_request(&mut cx, mocked_response).await;
13281 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13282 .await;
13283 cx.editor(|editor, _, _| {
13284 assert!(editor.signature_help_state.is_shown());
13285 });
13286
13287 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13288 cx.update_editor(|editor, window, cx| {
13289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13290 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13291 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13292 })
13293 });
13294 cx.assert_editor_state(indoc! {"
13295 fn main() {
13296 sample(param1, ˇparam2);
13297 }
13298
13299 fn sample(param1: u8, param2: u8) {}
13300 "});
13301
13302 let mocked_response = lsp::SignatureHelp {
13303 signatures: vec![lsp::SignatureInformation {
13304 label: "fn sample(param1: u8, param2: u8)".to_string(),
13305 documentation: None,
13306 parameters: Some(vec![
13307 lsp::ParameterInformation {
13308 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13309 documentation: None,
13310 },
13311 lsp::ParameterInformation {
13312 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13313 documentation: None,
13314 },
13315 ]),
13316 active_parameter: None,
13317 }],
13318 active_signature: Some(0),
13319 active_parameter: Some(1),
13320 };
13321 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13322 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13323 .await;
13324 cx.update_editor(|editor, _, cx| {
13325 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13326 });
13327 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13328 .await;
13329 cx.update_editor(|editor, window, cx| {
13330 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13331 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13332 })
13333 });
13334 cx.assert_editor_state(indoc! {"
13335 fn main() {
13336 sample(param1, «ˇparam2»);
13337 }
13338
13339 fn sample(param1: u8, param2: u8) {}
13340 "});
13341 cx.update_editor(|editor, window, cx| {
13342 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13343 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13344 })
13345 });
13346 cx.assert_editor_state(indoc! {"
13347 fn main() {
13348 sample(param1, ˇparam2);
13349 }
13350
13351 fn sample(param1: u8, param2: u8) {}
13352 "});
13353 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13354 .await;
13355}
13356
13357#[gpui::test]
13358async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13359 init_test(cx, |_| {});
13360
13361 let mut cx = EditorLspTestContext::new_rust(
13362 lsp::ServerCapabilities {
13363 signature_help_provider: Some(lsp::SignatureHelpOptions {
13364 ..Default::default()
13365 }),
13366 ..Default::default()
13367 },
13368 cx,
13369 )
13370 .await;
13371
13372 cx.set_state(indoc! {"
13373 fn main() {
13374 overloadedˇ
13375 }
13376 "});
13377
13378 cx.update_editor(|editor, window, cx| {
13379 editor.handle_input("(", window, cx);
13380 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13381 });
13382
13383 // Mock response with 3 signatures
13384 let mocked_response = lsp::SignatureHelp {
13385 signatures: vec![
13386 lsp::SignatureInformation {
13387 label: "fn overloaded(x: i32)".to_string(),
13388 documentation: None,
13389 parameters: Some(vec![lsp::ParameterInformation {
13390 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13391 documentation: None,
13392 }]),
13393 active_parameter: None,
13394 },
13395 lsp::SignatureInformation {
13396 label: "fn overloaded(x: i32, y: i32)".to_string(),
13397 documentation: None,
13398 parameters: Some(vec![
13399 lsp::ParameterInformation {
13400 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13401 documentation: None,
13402 },
13403 lsp::ParameterInformation {
13404 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13405 documentation: None,
13406 },
13407 ]),
13408 active_parameter: None,
13409 },
13410 lsp::SignatureInformation {
13411 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13412 documentation: None,
13413 parameters: Some(vec![
13414 lsp::ParameterInformation {
13415 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13416 documentation: None,
13417 },
13418 lsp::ParameterInformation {
13419 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13420 documentation: None,
13421 },
13422 lsp::ParameterInformation {
13423 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13424 documentation: None,
13425 },
13426 ]),
13427 active_parameter: None,
13428 },
13429 ],
13430 active_signature: Some(1),
13431 active_parameter: Some(0),
13432 };
13433 handle_signature_help_request(&mut cx, mocked_response).await;
13434
13435 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13436 .await;
13437
13438 // Verify we have multiple signatures and the right one is selected
13439 cx.editor(|editor, _, _| {
13440 let popover = editor.signature_help_state.popover().cloned().unwrap();
13441 assert_eq!(popover.signatures.len(), 3);
13442 // active_signature was 1, so that should be the current
13443 assert_eq!(popover.current_signature, 1);
13444 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13445 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13446 assert_eq!(
13447 popover.signatures[2].label,
13448 "fn overloaded(x: i32, y: i32, z: i32)"
13449 );
13450 });
13451
13452 // Test navigation functionality
13453 cx.update_editor(|editor, window, cx| {
13454 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13455 });
13456
13457 cx.editor(|editor, _, _| {
13458 let popover = editor.signature_help_state.popover().cloned().unwrap();
13459 assert_eq!(popover.current_signature, 2);
13460 });
13461
13462 // Test wrap around
13463 cx.update_editor(|editor, window, cx| {
13464 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13465 });
13466
13467 cx.editor(|editor, _, _| {
13468 let popover = editor.signature_help_state.popover().cloned().unwrap();
13469 assert_eq!(popover.current_signature, 0);
13470 });
13471
13472 // Test previous navigation
13473 cx.update_editor(|editor, window, cx| {
13474 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13475 });
13476
13477 cx.editor(|editor, _, _| {
13478 let popover = editor.signature_help_state.popover().cloned().unwrap();
13479 assert_eq!(popover.current_signature, 2);
13480 });
13481}
13482
13483#[gpui::test]
13484async fn test_completion_mode(cx: &mut TestAppContext) {
13485 init_test(cx, |_| {});
13486 let mut cx = EditorLspTestContext::new_rust(
13487 lsp::ServerCapabilities {
13488 completion_provider: Some(lsp::CompletionOptions {
13489 resolve_provider: Some(true),
13490 ..Default::default()
13491 }),
13492 ..Default::default()
13493 },
13494 cx,
13495 )
13496 .await;
13497
13498 struct Run {
13499 run_description: &'static str,
13500 initial_state: String,
13501 buffer_marked_text: String,
13502 completion_label: &'static str,
13503 completion_text: &'static str,
13504 expected_with_insert_mode: String,
13505 expected_with_replace_mode: String,
13506 expected_with_replace_subsequence_mode: String,
13507 expected_with_replace_suffix_mode: String,
13508 }
13509
13510 let runs = [
13511 Run {
13512 run_description: "Start of word matches completion text",
13513 initial_state: "before ediˇ after".into(),
13514 buffer_marked_text: "before <edi|> after".into(),
13515 completion_label: "editor",
13516 completion_text: "editor",
13517 expected_with_insert_mode: "before editorˇ after".into(),
13518 expected_with_replace_mode: "before editorˇ after".into(),
13519 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13520 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13521 },
13522 Run {
13523 run_description: "Accept same text at the middle of the word",
13524 initial_state: "before ediˇtor after".into(),
13525 buffer_marked_text: "before <edi|tor> after".into(),
13526 completion_label: "editor",
13527 completion_text: "editor",
13528 expected_with_insert_mode: "before editorˇtor after".into(),
13529 expected_with_replace_mode: "before editorˇ after".into(),
13530 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13531 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13532 },
13533 Run {
13534 run_description: "End of word matches completion text -- cursor at end",
13535 initial_state: "before torˇ after".into(),
13536 buffer_marked_text: "before <tor|> after".into(),
13537 completion_label: "editor",
13538 completion_text: "editor",
13539 expected_with_insert_mode: "before editorˇ after".into(),
13540 expected_with_replace_mode: "before editorˇ after".into(),
13541 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13542 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13543 },
13544 Run {
13545 run_description: "End of word matches completion text -- cursor at start",
13546 initial_state: "before ˇtor after".into(),
13547 buffer_marked_text: "before <|tor> after".into(),
13548 completion_label: "editor",
13549 completion_text: "editor",
13550 expected_with_insert_mode: "before editorˇtor after".into(),
13551 expected_with_replace_mode: "before editorˇ after".into(),
13552 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13553 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13554 },
13555 Run {
13556 run_description: "Prepend text containing whitespace",
13557 initial_state: "pˇfield: bool".into(),
13558 buffer_marked_text: "<p|field>: bool".into(),
13559 completion_label: "pub ",
13560 completion_text: "pub ",
13561 expected_with_insert_mode: "pub ˇfield: bool".into(),
13562 expected_with_replace_mode: "pub ˇ: bool".into(),
13563 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13564 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13565 },
13566 Run {
13567 run_description: "Add element to start of list",
13568 initial_state: "[element_ˇelement_2]".into(),
13569 buffer_marked_text: "[<element_|element_2>]".into(),
13570 completion_label: "element_1",
13571 completion_text: "element_1",
13572 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13573 expected_with_replace_mode: "[element_1ˇ]".into(),
13574 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13575 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13576 },
13577 Run {
13578 run_description: "Add element to start of list -- first and second elements are equal",
13579 initial_state: "[elˇelement]".into(),
13580 buffer_marked_text: "[<el|element>]".into(),
13581 completion_label: "element",
13582 completion_text: "element",
13583 expected_with_insert_mode: "[elementˇelement]".into(),
13584 expected_with_replace_mode: "[elementˇ]".into(),
13585 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13586 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13587 },
13588 Run {
13589 run_description: "Ends with matching suffix",
13590 initial_state: "SubˇError".into(),
13591 buffer_marked_text: "<Sub|Error>".into(),
13592 completion_label: "SubscriptionError",
13593 completion_text: "SubscriptionError",
13594 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13595 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13596 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13597 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13598 },
13599 Run {
13600 run_description: "Suffix is a subsequence -- contiguous",
13601 initial_state: "SubˇErr".into(),
13602 buffer_marked_text: "<Sub|Err>".into(),
13603 completion_label: "SubscriptionError",
13604 completion_text: "SubscriptionError",
13605 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13606 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13607 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13608 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13609 },
13610 Run {
13611 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13612 initial_state: "Suˇscrirr".into(),
13613 buffer_marked_text: "<Su|scrirr>".into(),
13614 completion_label: "SubscriptionError",
13615 completion_text: "SubscriptionError",
13616 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13617 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13618 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13619 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13620 },
13621 Run {
13622 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13623 initial_state: "foo(indˇix)".into(),
13624 buffer_marked_text: "foo(<ind|ix>)".into(),
13625 completion_label: "node_index",
13626 completion_text: "node_index",
13627 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13628 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13629 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13630 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13631 },
13632 Run {
13633 run_description: "Replace range ends before cursor - should extend to cursor",
13634 initial_state: "before editˇo after".into(),
13635 buffer_marked_text: "before <{ed}>it|o after".into(),
13636 completion_label: "editor",
13637 completion_text: "editor",
13638 expected_with_insert_mode: "before editorˇo after".into(),
13639 expected_with_replace_mode: "before editorˇo after".into(),
13640 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13641 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13642 },
13643 Run {
13644 run_description: "Uses label for suffix matching",
13645 initial_state: "before ediˇtor after".into(),
13646 buffer_marked_text: "before <edi|tor> after".into(),
13647 completion_label: "editor",
13648 completion_text: "editor()",
13649 expected_with_insert_mode: "before editor()ˇtor after".into(),
13650 expected_with_replace_mode: "before editor()ˇ after".into(),
13651 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13652 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13653 },
13654 Run {
13655 run_description: "Case insensitive subsequence and suffix matching",
13656 initial_state: "before EDiˇtoR after".into(),
13657 buffer_marked_text: "before <EDi|toR> after".into(),
13658 completion_label: "editor",
13659 completion_text: "editor",
13660 expected_with_insert_mode: "before editorˇtoR after".into(),
13661 expected_with_replace_mode: "before editorˇ after".into(),
13662 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13663 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13664 },
13665 ];
13666
13667 for run in runs {
13668 let run_variations = [
13669 (LspInsertMode::Insert, run.expected_with_insert_mode),
13670 (LspInsertMode::Replace, run.expected_with_replace_mode),
13671 (
13672 LspInsertMode::ReplaceSubsequence,
13673 run.expected_with_replace_subsequence_mode,
13674 ),
13675 (
13676 LspInsertMode::ReplaceSuffix,
13677 run.expected_with_replace_suffix_mode,
13678 ),
13679 ];
13680
13681 for (lsp_insert_mode, expected_text) in run_variations {
13682 eprintln!(
13683 "run = {:?}, mode = {lsp_insert_mode:.?}",
13684 run.run_description,
13685 );
13686
13687 update_test_language_settings(&mut cx, |settings| {
13688 settings.defaults.completions = Some(CompletionSettingsContent {
13689 lsp_insert_mode: Some(lsp_insert_mode),
13690 words: Some(WordsCompletionMode::Disabled),
13691 words_min_length: Some(0),
13692 ..Default::default()
13693 });
13694 });
13695
13696 cx.set_state(&run.initial_state);
13697 cx.update_editor(|editor, window, cx| {
13698 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13699 });
13700
13701 let counter = Arc::new(AtomicUsize::new(0));
13702 handle_completion_request_with_insert_and_replace(
13703 &mut cx,
13704 &run.buffer_marked_text,
13705 vec![(run.completion_label, run.completion_text)],
13706 counter.clone(),
13707 )
13708 .await;
13709 cx.condition(|editor, _| editor.context_menu_visible())
13710 .await;
13711 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13712
13713 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13714 editor
13715 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13716 .unwrap()
13717 });
13718 cx.assert_editor_state(&expected_text);
13719 handle_resolve_completion_request(&mut cx, None).await;
13720 apply_additional_edits.await.unwrap();
13721 }
13722 }
13723}
13724
13725#[gpui::test]
13726async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13727 init_test(cx, |_| {});
13728 let mut cx = EditorLspTestContext::new_rust(
13729 lsp::ServerCapabilities {
13730 completion_provider: Some(lsp::CompletionOptions {
13731 resolve_provider: Some(true),
13732 ..Default::default()
13733 }),
13734 ..Default::default()
13735 },
13736 cx,
13737 )
13738 .await;
13739
13740 let initial_state = "SubˇError";
13741 let buffer_marked_text = "<Sub|Error>";
13742 let completion_text = "SubscriptionError";
13743 let expected_with_insert_mode = "SubscriptionErrorˇError";
13744 let expected_with_replace_mode = "SubscriptionErrorˇ";
13745
13746 update_test_language_settings(&mut cx, |settings| {
13747 settings.defaults.completions = Some(CompletionSettingsContent {
13748 words: Some(WordsCompletionMode::Disabled),
13749 words_min_length: Some(0),
13750 // set the opposite here to ensure that the action is overriding the default behavior
13751 lsp_insert_mode: Some(LspInsertMode::Insert),
13752 ..Default::default()
13753 });
13754 });
13755
13756 cx.set_state(initial_state);
13757 cx.update_editor(|editor, window, cx| {
13758 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13759 });
13760
13761 let counter = Arc::new(AtomicUsize::new(0));
13762 handle_completion_request_with_insert_and_replace(
13763 &mut cx,
13764 buffer_marked_text,
13765 vec![(completion_text, completion_text)],
13766 counter.clone(),
13767 )
13768 .await;
13769 cx.condition(|editor, _| editor.context_menu_visible())
13770 .await;
13771 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13772
13773 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13774 editor
13775 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13776 .unwrap()
13777 });
13778 cx.assert_editor_state(expected_with_replace_mode);
13779 handle_resolve_completion_request(&mut cx, None).await;
13780 apply_additional_edits.await.unwrap();
13781
13782 update_test_language_settings(&mut cx, |settings| {
13783 settings.defaults.completions = Some(CompletionSettingsContent {
13784 words: Some(WordsCompletionMode::Disabled),
13785 words_min_length: Some(0),
13786 // set the opposite here to ensure that the action is overriding the default behavior
13787 lsp_insert_mode: Some(LspInsertMode::Replace),
13788 ..Default::default()
13789 });
13790 });
13791
13792 cx.set_state(initial_state);
13793 cx.update_editor(|editor, window, cx| {
13794 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13795 });
13796 handle_completion_request_with_insert_and_replace(
13797 &mut cx,
13798 buffer_marked_text,
13799 vec![(completion_text, completion_text)],
13800 counter.clone(),
13801 )
13802 .await;
13803 cx.condition(|editor, _| editor.context_menu_visible())
13804 .await;
13805 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13806
13807 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13808 editor
13809 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13810 .unwrap()
13811 });
13812 cx.assert_editor_state(expected_with_insert_mode);
13813 handle_resolve_completion_request(&mut cx, None).await;
13814 apply_additional_edits.await.unwrap();
13815}
13816
13817#[gpui::test]
13818async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13819 init_test(cx, |_| {});
13820 let mut cx = EditorLspTestContext::new_rust(
13821 lsp::ServerCapabilities {
13822 completion_provider: Some(lsp::CompletionOptions {
13823 resolve_provider: Some(true),
13824 ..Default::default()
13825 }),
13826 ..Default::default()
13827 },
13828 cx,
13829 )
13830 .await;
13831
13832 // scenario: surrounding text matches completion text
13833 let completion_text = "to_offset";
13834 let initial_state = indoc! {"
13835 1. buf.to_offˇsuffix
13836 2. buf.to_offˇsuf
13837 3. buf.to_offˇfix
13838 4. buf.to_offˇ
13839 5. into_offˇensive
13840 6. ˇsuffix
13841 7. let ˇ //
13842 8. aaˇzz
13843 9. buf.to_off«zzzzzˇ»suffix
13844 10. buf.«ˇzzzzz»suffix
13845 11. to_off«ˇzzzzz»
13846
13847 buf.to_offˇsuffix // newest cursor
13848 "};
13849 let completion_marked_buffer = indoc! {"
13850 1. buf.to_offsuffix
13851 2. buf.to_offsuf
13852 3. buf.to_offfix
13853 4. buf.to_off
13854 5. into_offensive
13855 6. suffix
13856 7. let //
13857 8. aazz
13858 9. buf.to_offzzzzzsuffix
13859 10. buf.zzzzzsuffix
13860 11. to_offzzzzz
13861
13862 buf.<to_off|suffix> // newest cursor
13863 "};
13864 let expected = indoc! {"
13865 1. buf.to_offsetˇ
13866 2. buf.to_offsetˇsuf
13867 3. buf.to_offsetˇfix
13868 4. buf.to_offsetˇ
13869 5. into_offsetˇensive
13870 6. to_offsetˇsuffix
13871 7. let to_offsetˇ //
13872 8. aato_offsetˇzz
13873 9. buf.to_offsetˇ
13874 10. buf.to_offsetˇsuffix
13875 11. to_offsetˇ
13876
13877 buf.to_offsetˇ // newest cursor
13878 "};
13879 cx.set_state(initial_state);
13880 cx.update_editor(|editor, window, cx| {
13881 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13882 });
13883 handle_completion_request_with_insert_and_replace(
13884 &mut cx,
13885 completion_marked_buffer,
13886 vec![(completion_text, completion_text)],
13887 Arc::new(AtomicUsize::new(0)),
13888 )
13889 .await;
13890 cx.condition(|editor, _| editor.context_menu_visible())
13891 .await;
13892 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13893 editor
13894 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13895 .unwrap()
13896 });
13897 cx.assert_editor_state(expected);
13898 handle_resolve_completion_request(&mut cx, None).await;
13899 apply_additional_edits.await.unwrap();
13900
13901 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13902 let completion_text = "foo_and_bar";
13903 let initial_state = indoc! {"
13904 1. ooanbˇ
13905 2. zooanbˇ
13906 3. ooanbˇz
13907 4. zooanbˇz
13908 5. ooanˇ
13909 6. oanbˇ
13910
13911 ooanbˇ
13912 "};
13913 let completion_marked_buffer = indoc! {"
13914 1. ooanb
13915 2. zooanb
13916 3. ooanbz
13917 4. zooanbz
13918 5. ooan
13919 6. oanb
13920
13921 <ooanb|>
13922 "};
13923 let expected = indoc! {"
13924 1. foo_and_barˇ
13925 2. zfoo_and_barˇ
13926 3. foo_and_barˇz
13927 4. zfoo_and_barˇz
13928 5. ooanfoo_and_barˇ
13929 6. oanbfoo_and_barˇ
13930
13931 foo_and_barˇ
13932 "};
13933 cx.set_state(initial_state);
13934 cx.update_editor(|editor, window, cx| {
13935 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13936 });
13937 handle_completion_request_with_insert_and_replace(
13938 &mut cx,
13939 completion_marked_buffer,
13940 vec![(completion_text, completion_text)],
13941 Arc::new(AtomicUsize::new(0)),
13942 )
13943 .await;
13944 cx.condition(|editor, _| editor.context_menu_visible())
13945 .await;
13946 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13947 editor
13948 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13949 .unwrap()
13950 });
13951 cx.assert_editor_state(expected);
13952 handle_resolve_completion_request(&mut cx, None).await;
13953 apply_additional_edits.await.unwrap();
13954
13955 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13956 // (expects the same as if it was inserted at the end)
13957 let completion_text = "foo_and_bar";
13958 let initial_state = indoc! {"
13959 1. ooˇanb
13960 2. zooˇanb
13961 3. ooˇanbz
13962 4. zooˇanbz
13963
13964 ooˇanb
13965 "};
13966 let completion_marked_buffer = indoc! {"
13967 1. ooanb
13968 2. zooanb
13969 3. ooanbz
13970 4. zooanbz
13971
13972 <oo|anb>
13973 "};
13974 let expected = indoc! {"
13975 1. foo_and_barˇ
13976 2. zfoo_and_barˇ
13977 3. foo_and_barˇz
13978 4. zfoo_and_barˇz
13979
13980 foo_and_barˇ
13981 "};
13982 cx.set_state(initial_state);
13983 cx.update_editor(|editor, window, cx| {
13984 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13985 });
13986 handle_completion_request_with_insert_and_replace(
13987 &mut cx,
13988 completion_marked_buffer,
13989 vec![(completion_text, completion_text)],
13990 Arc::new(AtomicUsize::new(0)),
13991 )
13992 .await;
13993 cx.condition(|editor, _| editor.context_menu_visible())
13994 .await;
13995 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13996 editor
13997 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13998 .unwrap()
13999 });
14000 cx.assert_editor_state(expected);
14001 handle_resolve_completion_request(&mut cx, None).await;
14002 apply_additional_edits.await.unwrap();
14003}
14004
14005// This used to crash
14006#[gpui::test]
14007async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14008 init_test(cx, |_| {});
14009
14010 let buffer_text = indoc! {"
14011 fn main() {
14012 10.satu;
14013
14014 //
14015 // separate cursors so they open in different excerpts (manually reproducible)
14016 //
14017
14018 10.satu20;
14019 }
14020 "};
14021 let multibuffer_text_with_selections = indoc! {"
14022 fn main() {
14023 10.satuˇ;
14024
14025 //
14026
14027 //
14028
14029 10.satuˇ20;
14030 }
14031 "};
14032 let expected_multibuffer = indoc! {"
14033 fn main() {
14034 10.saturating_sub()ˇ;
14035
14036 //
14037
14038 //
14039
14040 10.saturating_sub()ˇ;
14041 }
14042 "};
14043
14044 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14045 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14046
14047 let fs = FakeFs::new(cx.executor());
14048 fs.insert_tree(
14049 path!("/a"),
14050 json!({
14051 "main.rs": buffer_text,
14052 }),
14053 )
14054 .await;
14055
14056 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14057 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14058 language_registry.add(rust_lang());
14059 let mut fake_servers = language_registry.register_fake_lsp(
14060 "Rust",
14061 FakeLspAdapter {
14062 capabilities: lsp::ServerCapabilities {
14063 completion_provider: Some(lsp::CompletionOptions {
14064 resolve_provider: None,
14065 ..lsp::CompletionOptions::default()
14066 }),
14067 ..lsp::ServerCapabilities::default()
14068 },
14069 ..FakeLspAdapter::default()
14070 },
14071 );
14072 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14073 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14074 let buffer = project
14075 .update(cx, |project, cx| {
14076 project.open_local_buffer(path!("/a/main.rs"), cx)
14077 })
14078 .await
14079 .unwrap();
14080
14081 let multi_buffer = cx.new(|cx| {
14082 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14083 multi_buffer.push_excerpts(
14084 buffer.clone(),
14085 [ExcerptRange::new(0..first_excerpt_end)],
14086 cx,
14087 );
14088 multi_buffer.push_excerpts(
14089 buffer.clone(),
14090 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14091 cx,
14092 );
14093 multi_buffer
14094 });
14095
14096 let editor = workspace
14097 .update(cx, |_, window, cx| {
14098 cx.new(|cx| {
14099 Editor::new(
14100 EditorMode::Full {
14101 scale_ui_elements_with_buffer_font_size: false,
14102 show_active_line_background: false,
14103 sized_by_content: false,
14104 },
14105 multi_buffer.clone(),
14106 Some(project.clone()),
14107 window,
14108 cx,
14109 )
14110 })
14111 })
14112 .unwrap();
14113
14114 let pane = workspace
14115 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14116 .unwrap();
14117 pane.update_in(cx, |pane, window, cx| {
14118 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14119 });
14120
14121 let fake_server = fake_servers.next().await.unwrap();
14122
14123 editor.update_in(cx, |editor, window, cx| {
14124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14125 s.select_ranges([
14126 Point::new(1, 11)..Point::new(1, 11),
14127 Point::new(7, 11)..Point::new(7, 11),
14128 ])
14129 });
14130
14131 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14132 });
14133
14134 editor.update_in(cx, |editor, window, cx| {
14135 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14136 });
14137
14138 fake_server
14139 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14140 let completion_item = lsp::CompletionItem {
14141 label: "saturating_sub()".into(),
14142 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14143 lsp::InsertReplaceEdit {
14144 new_text: "saturating_sub()".to_owned(),
14145 insert: lsp::Range::new(
14146 lsp::Position::new(7, 7),
14147 lsp::Position::new(7, 11),
14148 ),
14149 replace: lsp::Range::new(
14150 lsp::Position::new(7, 7),
14151 lsp::Position::new(7, 13),
14152 ),
14153 },
14154 )),
14155 ..lsp::CompletionItem::default()
14156 };
14157
14158 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14159 })
14160 .next()
14161 .await
14162 .unwrap();
14163
14164 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14165 .await;
14166
14167 editor
14168 .update_in(cx, |editor, window, cx| {
14169 editor
14170 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14171 .unwrap()
14172 })
14173 .await
14174 .unwrap();
14175
14176 editor.update(cx, |editor, cx| {
14177 assert_text_with_selections(editor, expected_multibuffer, cx);
14178 })
14179}
14180
14181#[gpui::test]
14182async fn test_completion(cx: &mut TestAppContext) {
14183 init_test(cx, |_| {});
14184
14185 let mut cx = EditorLspTestContext::new_rust(
14186 lsp::ServerCapabilities {
14187 completion_provider: Some(lsp::CompletionOptions {
14188 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14189 resolve_provider: Some(true),
14190 ..Default::default()
14191 }),
14192 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14193 ..Default::default()
14194 },
14195 cx,
14196 )
14197 .await;
14198 let counter = Arc::new(AtomicUsize::new(0));
14199
14200 cx.set_state(indoc! {"
14201 oneˇ
14202 two
14203 three
14204 "});
14205 cx.simulate_keystroke(".");
14206 handle_completion_request(
14207 indoc! {"
14208 one.|<>
14209 two
14210 three
14211 "},
14212 vec!["first_completion", "second_completion"],
14213 true,
14214 counter.clone(),
14215 &mut cx,
14216 )
14217 .await;
14218 cx.condition(|editor, _| editor.context_menu_visible())
14219 .await;
14220 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14221
14222 let _handler = handle_signature_help_request(
14223 &mut cx,
14224 lsp::SignatureHelp {
14225 signatures: vec![lsp::SignatureInformation {
14226 label: "test signature".to_string(),
14227 documentation: None,
14228 parameters: Some(vec![lsp::ParameterInformation {
14229 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14230 documentation: None,
14231 }]),
14232 active_parameter: None,
14233 }],
14234 active_signature: None,
14235 active_parameter: None,
14236 },
14237 );
14238 cx.update_editor(|editor, window, cx| {
14239 assert!(
14240 !editor.signature_help_state.is_shown(),
14241 "No signature help was called for"
14242 );
14243 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14244 });
14245 cx.run_until_parked();
14246 cx.update_editor(|editor, _, _| {
14247 assert!(
14248 !editor.signature_help_state.is_shown(),
14249 "No signature help should be shown when completions menu is open"
14250 );
14251 });
14252
14253 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14254 editor.context_menu_next(&Default::default(), window, cx);
14255 editor
14256 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14257 .unwrap()
14258 });
14259 cx.assert_editor_state(indoc! {"
14260 one.second_completionˇ
14261 two
14262 three
14263 "});
14264
14265 handle_resolve_completion_request(
14266 &mut cx,
14267 Some(vec![
14268 (
14269 //This overlaps with the primary completion edit which is
14270 //misbehavior from the LSP spec, test that we filter it out
14271 indoc! {"
14272 one.second_ˇcompletion
14273 two
14274 threeˇ
14275 "},
14276 "overlapping additional edit",
14277 ),
14278 (
14279 indoc! {"
14280 one.second_completion
14281 two
14282 threeˇ
14283 "},
14284 "\nadditional edit",
14285 ),
14286 ]),
14287 )
14288 .await;
14289 apply_additional_edits.await.unwrap();
14290 cx.assert_editor_state(indoc! {"
14291 one.second_completionˇ
14292 two
14293 three
14294 additional edit
14295 "});
14296
14297 cx.set_state(indoc! {"
14298 one.second_completion
14299 twoˇ
14300 threeˇ
14301 additional edit
14302 "});
14303 cx.simulate_keystroke(" ");
14304 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14305 cx.simulate_keystroke("s");
14306 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14307
14308 cx.assert_editor_state(indoc! {"
14309 one.second_completion
14310 two sˇ
14311 three sˇ
14312 additional edit
14313 "});
14314 handle_completion_request(
14315 indoc! {"
14316 one.second_completion
14317 two s
14318 three <s|>
14319 additional edit
14320 "},
14321 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14322 true,
14323 counter.clone(),
14324 &mut cx,
14325 )
14326 .await;
14327 cx.condition(|editor, _| editor.context_menu_visible())
14328 .await;
14329 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14330
14331 cx.simulate_keystroke("i");
14332
14333 handle_completion_request(
14334 indoc! {"
14335 one.second_completion
14336 two si
14337 three <si|>
14338 additional edit
14339 "},
14340 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14341 true,
14342 counter.clone(),
14343 &mut cx,
14344 )
14345 .await;
14346 cx.condition(|editor, _| editor.context_menu_visible())
14347 .await;
14348 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14349
14350 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14351 editor
14352 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14353 .unwrap()
14354 });
14355 cx.assert_editor_state(indoc! {"
14356 one.second_completion
14357 two sixth_completionˇ
14358 three sixth_completionˇ
14359 additional edit
14360 "});
14361
14362 apply_additional_edits.await.unwrap();
14363
14364 update_test_language_settings(&mut cx, |settings| {
14365 settings.defaults.show_completions_on_input = Some(false);
14366 });
14367 cx.set_state("editorˇ");
14368 cx.simulate_keystroke(".");
14369 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14370 cx.simulate_keystrokes("c l o");
14371 cx.assert_editor_state("editor.cloˇ");
14372 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14373 cx.update_editor(|editor, window, cx| {
14374 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14375 });
14376 handle_completion_request(
14377 "editor.<clo|>",
14378 vec!["close", "clobber"],
14379 true,
14380 counter.clone(),
14381 &mut cx,
14382 )
14383 .await;
14384 cx.condition(|editor, _| editor.context_menu_visible())
14385 .await;
14386 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14387
14388 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14389 editor
14390 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14391 .unwrap()
14392 });
14393 cx.assert_editor_state("editor.clobberˇ");
14394 handle_resolve_completion_request(&mut cx, None).await;
14395 apply_additional_edits.await.unwrap();
14396}
14397
14398#[gpui::test]
14399async fn test_completion_reuse(cx: &mut TestAppContext) {
14400 init_test(cx, |_| {});
14401
14402 let mut cx = EditorLspTestContext::new_rust(
14403 lsp::ServerCapabilities {
14404 completion_provider: Some(lsp::CompletionOptions {
14405 trigger_characters: Some(vec![".".to_string()]),
14406 ..Default::default()
14407 }),
14408 ..Default::default()
14409 },
14410 cx,
14411 )
14412 .await;
14413
14414 let counter = Arc::new(AtomicUsize::new(0));
14415 cx.set_state("objˇ");
14416 cx.simulate_keystroke(".");
14417
14418 // Initial completion request returns complete results
14419 let is_incomplete = false;
14420 handle_completion_request(
14421 "obj.|<>",
14422 vec!["a", "ab", "abc"],
14423 is_incomplete,
14424 counter.clone(),
14425 &mut cx,
14426 )
14427 .await;
14428 cx.run_until_parked();
14429 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14430 cx.assert_editor_state("obj.ˇ");
14431 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14432
14433 // Type "a" - filters existing completions
14434 cx.simulate_keystroke("a");
14435 cx.run_until_parked();
14436 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14437 cx.assert_editor_state("obj.aˇ");
14438 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14439
14440 // Type "b" - filters existing completions
14441 cx.simulate_keystroke("b");
14442 cx.run_until_parked();
14443 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14444 cx.assert_editor_state("obj.abˇ");
14445 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14446
14447 // Type "c" - filters existing completions
14448 cx.simulate_keystroke("c");
14449 cx.run_until_parked();
14450 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14451 cx.assert_editor_state("obj.abcˇ");
14452 check_displayed_completions(vec!["abc"], &mut cx);
14453
14454 // Backspace to delete "c" - filters existing completions
14455 cx.update_editor(|editor, window, cx| {
14456 editor.backspace(&Backspace, window, cx);
14457 });
14458 cx.run_until_parked();
14459 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14460 cx.assert_editor_state("obj.abˇ");
14461 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14462
14463 // Moving cursor to the left dismisses menu.
14464 cx.update_editor(|editor, window, cx| {
14465 editor.move_left(&MoveLeft, window, cx);
14466 });
14467 cx.run_until_parked();
14468 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14469 cx.assert_editor_state("obj.aˇb");
14470 cx.update_editor(|editor, _, _| {
14471 assert_eq!(editor.context_menu_visible(), false);
14472 });
14473
14474 // Type "b" - new request
14475 cx.simulate_keystroke("b");
14476 let is_incomplete = false;
14477 handle_completion_request(
14478 "obj.<ab|>a",
14479 vec!["ab", "abc"],
14480 is_incomplete,
14481 counter.clone(),
14482 &mut cx,
14483 )
14484 .await;
14485 cx.run_until_parked();
14486 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14487 cx.assert_editor_state("obj.abˇb");
14488 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14489
14490 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14491 cx.update_editor(|editor, window, cx| {
14492 editor.backspace(&Backspace, window, cx);
14493 });
14494 let is_incomplete = false;
14495 handle_completion_request(
14496 "obj.<a|>b",
14497 vec!["a", "ab", "abc"],
14498 is_incomplete,
14499 counter.clone(),
14500 &mut cx,
14501 )
14502 .await;
14503 cx.run_until_parked();
14504 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14505 cx.assert_editor_state("obj.aˇb");
14506 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14507
14508 // Backspace to delete "a" - dismisses menu.
14509 cx.update_editor(|editor, window, cx| {
14510 editor.backspace(&Backspace, window, cx);
14511 });
14512 cx.run_until_parked();
14513 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14514 cx.assert_editor_state("obj.ˇb");
14515 cx.update_editor(|editor, _, _| {
14516 assert_eq!(editor.context_menu_visible(), false);
14517 });
14518}
14519
14520#[gpui::test]
14521async fn test_word_completion(cx: &mut TestAppContext) {
14522 let lsp_fetch_timeout_ms = 10;
14523 init_test(cx, |language_settings| {
14524 language_settings.defaults.completions = Some(CompletionSettingsContent {
14525 words_min_length: Some(0),
14526 lsp_fetch_timeout_ms: Some(10),
14527 lsp_insert_mode: Some(LspInsertMode::Insert),
14528 ..Default::default()
14529 });
14530 });
14531
14532 let mut cx = EditorLspTestContext::new_rust(
14533 lsp::ServerCapabilities {
14534 completion_provider: Some(lsp::CompletionOptions {
14535 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14536 ..lsp::CompletionOptions::default()
14537 }),
14538 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14539 ..lsp::ServerCapabilities::default()
14540 },
14541 cx,
14542 )
14543 .await;
14544
14545 let throttle_completions = Arc::new(AtomicBool::new(false));
14546
14547 let lsp_throttle_completions = throttle_completions.clone();
14548 let _completion_requests_handler =
14549 cx.lsp
14550 .server
14551 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14552 let lsp_throttle_completions = lsp_throttle_completions.clone();
14553 let cx = cx.clone();
14554 async move {
14555 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14556 cx.background_executor()
14557 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14558 .await;
14559 }
14560 Ok(Some(lsp::CompletionResponse::Array(vec![
14561 lsp::CompletionItem {
14562 label: "first".into(),
14563 ..lsp::CompletionItem::default()
14564 },
14565 lsp::CompletionItem {
14566 label: "last".into(),
14567 ..lsp::CompletionItem::default()
14568 },
14569 ])))
14570 }
14571 });
14572
14573 cx.set_state(indoc! {"
14574 oneˇ
14575 two
14576 three
14577 "});
14578 cx.simulate_keystroke(".");
14579 cx.executor().run_until_parked();
14580 cx.condition(|editor, _| editor.context_menu_visible())
14581 .await;
14582 cx.update_editor(|editor, window, cx| {
14583 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14584 {
14585 assert_eq!(
14586 completion_menu_entries(menu),
14587 &["first", "last"],
14588 "When LSP server is fast to reply, no fallback word completions are used"
14589 );
14590 } else {
14591 panic!("expected completion menu to be open");
14592 }
14593 editor.cancel(&Cancel, window, cx);
14594 });
14595 cx.executor().run_until_parked();
14596 cx.condition(|editor, _| !editor.context_menu_visible())
14597 .await;
14598
14599 throttle_completions.store(true, atomic::Ordering::Release);
14600 cx.simulate_keystroke(".");
14601 cx.executor()
14602 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14603 cx.executor().run_until_parked();
14604 cx.condition(|editor, _| editor.context_menu_visible())
14605 .await;
14606 cx.update_editor(|editor, _, _| {
14607 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14608 {
14609 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14610 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14611 } else {
14612 panic!("expected completion menu to be open");
14613 }
14614 });
14615}
14616
14617#[gpui::test]
14618async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14619 init_test(cx, |language_settings| {
14620 language_settings.defaults.completions = Some(CompletionSettingsContent {
14621 words: Some(WordsCompletionMode::Enabled),
14622 words_min_length: Some(0),
14623 lsp_insert_mode: Some(LspInsertMode::Insert),
14624 ..Default::default()
14625 });
14626 });
14627
14628 let mut cx = EditorLspTestContext::new_rust(
14629 lsp::ServerCapabilities {
14630 completion_provider: Some(lsp::CompletionOptions {
14631 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14632 ..lsp::CompletionOptions::default()
14633 }),
14634 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14635 ..lsp::ServerCapabilities::default()
14636 },
14637 cx,
14638 )
14639 .await;
14640
14641 let _completion_requests_handler =
14642 cx.lsp
14643 .server
14644 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14645 Ok(Some(lsp::CompletionResponse::Array(vec![
14646 lsp::CompletionItem {
14647 label: "first".into(),
14648 ..lsp::CompletionItem::default()
14649 },
14650 lsp::CompletionItem {
14651 label: "last".into(),
14652 ..lsp::CompletionItem::default()
14653 },
14654 ])))
14655 });
14656
14657 cx.set_state(indoc! {"ˇ
14658 first
14659 last
14660 second
14661 "});
14662 cx.simulate_keystroke(".");
14663 cx.executor().run_until_parked();
14664 cx.condition(|editor, _| editor.context_menu_visible())
14665 .await;
14666 cx.update_editor(|editor, _, _| {
14667 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14668 {
14669 assert_eq!(
14670 completion_menu_entries(menu),
14671 &["first", "last", "second"],
14672 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14673 );
14674 } else {
14675 panic!("expected completion menu to be open");
14676 }
14677 });
14678}
14679
14680#[gpui::test]
14681async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14682 init_test(cx, |language_settings| {
14683 language_settings.defaults.completions = Some(CompletionSettingsContent {
14684 words: Some(WordsCompletionMode::Disabled),
14685 words_min_length: Some(0),
14686 lsp_insert_mode: Some(LspInsertMode::Insert),
14687 ..Default::default()
14688 });
14689 });
14690
14691 let mut cx = EditorLspTestContext::new_rust(
14692 lsp::ServerCapabilities {
14693 completion_provider: Some(lsp::CompletionOptions {
14694 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14695 ..lsp::CompletionOptions::default()
14696 }),
14697 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14698 ..lsp::ServerCapabilities::default()
14699 },
14700 cx,
14701 )
14702 .await;
14703
14704 let _completion_requests_handler =
14705 cx.lsp
14706 .server
14707 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14708 panic!("LSP completions should not be queried when dealing with word completions")
14709 });
14710
14711 cx.set_state(indoc! {"ˇ
14712 first
14713 last
14714 second
14715 "});
14716 cx.update_editor(|editor, window, cx| {
14717 editor.show_word_completions(&ShowWordCompletions, window, cx);
14718 });
14719 cx.executor().run_until_parked();
14720 cx.condition(|editor, _| editor.context_menu_visible())
14721 .await;
14722 cx.update_editor(|editor, _, _| {
14723 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14724 {
14725 assert_eq!(
14726 completion_menu_entries(menu),
14727 &["first", "last", "second"],
14728 "`ShowWordCompletions` action should show word completions"
14729 );
14730 } else {
14731 panic!("expected completion menu to be open");
14732 }
14733 });
14734
14735 cx.simulate_keystroke("l");
14736 cx.executor().run_until_parked();
14737 cx.condition(|editor, _| editor.context_menu_visible())
14738 .await;
14739 cx.update_editor(|editor, _, _| {
14740 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14741 {
14742 assert_eq!(
14743 completion_menu_entries(menu),
14744 &["last"],
14745 "After showing word completions, further editing should filter them and not query the LSP"
14746 );
14747 } else {
14748 panic!("expected completion menu to be open");
14749 }
14750 });
14751}
14752
14753#[gpui::test]
14754async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14755 init_test(cx, |language_settings| {
14756 language_settings.defaults.completions = Some(CompletionSettingsContent {
14757 words_min_length: Some(0),
14758 lsp: Some(false),
14759 lsp_insert_mode: Some(LspInsertMode::Insert),
14760 ..Default::default()
14761 });
14762 });
14763
14764 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14765
14766 cx.set_state(indoc! {"ˇ
14767 0_usize
14768 let
14769 33
14770 4.5f32
14771 "});
14772 cx.update_editor(|editor, window, cx| {
14773 editor.show_completions(&ShowCompletions::default(), window, cx);
14774 });
14775 cx.executor().run_until_parked();
14776 cx.condition(|editor, _| editor.context_menu_visible())
14777 .await;
14778 cx.update_editor(|editor, window, cx| {
14779 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14780 {
14781 assert_eq!(
14782 completion_menu_entries(menu),
14783 &["let"],
14784 "With no digits in the completion query, no digits should be in the word completions"
14785 );
14786 } else {
14787 panic!("expected completion menu to be open");
14788 }
14789 editor.cancel(&Cancel, window, cx);
14790 });
14791
14792 cx.set_state(indoc! {"3ˇ
14793 0_usize
14794 let
14795 3
14796 33.35f32
14797 "});
14798 cx.update_editor(|editor, window, cx| {
14799 editor.show_completions(&ShowCompletions::default(), window, cx);
14800 });
14801 cx.executor().run_until_parked();
14802 cx.condition(|editor, _| editor.context_menu_visible())
14803 .await;
14804 cx.update_editor(|editor, _, _| {
14805 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14806 {
14807 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14808 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14809 } else {
14810 panic!("expected completion menu to be open");
14811 }
14812 });
14813}
14814
14815#[gpui::test]
14816async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14817 init_test(cx, |language_settings| {
14818 language_settings.defaults.completions = Some(CompletionSettingsContent {
14819 words: Some(WordsCompletionMode::Enabled),
14820 words_min_length: Some(3),
14821 lsp_insert_mode: Some(LspInsertMode::Insert),
14822 ..Default::default()
14823 });
14824 });
14825
14826 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14827 cx.set_state(indoc! {"ˇ
14828 wow
14829 wowen
14830 wowser
14831 "});
14832 cx.simulate_keystroke("w");
14833 cx.executor().run_until_parked();
14834 cx.update_editor(|editor, _, _| {
14835 if editor.context_menu.borrow_mut().is_some() {
14836 panic!(
14837 "expected completion menu to be hidden, as words completion threshold is not met"
14838 );
14839 }
14840 });
14841
14842 cx.update_editor(|editor, window, cx| {
14843 editor.show_word_completions(&ShowWordCompletions, window, cx);
14844 });
14845 cx.executor().run_until_parked();
14846 cx.update_editor(|editor, window, cx| {
14847 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14848 {
14849 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");
14850 } else {
14851 panic!("expected completion menu to be open after the word completions are called with an action");
14852 }
14853
14854 editor.cancel(&Cancel, window, cx);
14855 });
14856 cx.update_editor(|editor, _, _| {
14857 if editor.context_menu.borrow_mut().is_some() {
14858 panic!("expected completion menu to be hidden after canceling");
14859 }
14860 });
14861
14862 cx.simulate_keystroke("o");
14863 cx.executor().run_until_parked();
14864 cx.update_editor(|editor, _, _| {
14865 if editor.context_menu.borrow_mut().is_some() {
14866 panic!(
14867 "expected completion menu to be hidden, as words completion threshold is not met still"
14868 );
14869 }
14870 });
14871
14872 cx.simulate_keystroke("w");
14873 cx.executor().run_until_parked();
14874 cx.update_editor(|editor, _, _| {
14875 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14876 {
14877 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14878 } else {
14879 panic!("expected completion menu to be open after the word completions threshold is met");
14880 }
14881 });
14882}
14883
14884#[gpui::test]
14885async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14886 init_test(cx, |language_settings| {
14887 language_settings.defaults.completions = Some(CompletionSettingsContent {
14888 words: Some(WordsCompletionMode::Enabled),
14889 words_min_length: Some(0),
14890 lsp_insert_mode: Some(LspInsertMode::Insert),
14891 ..Default::default()
14892 });
14893 });
14894
14895 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14896 cx.update_editor(|editor, _, _| {
14897 editor.disable_word_completions();
14898 });
14899 cx.set_state(indoc! {"ˇ
14900 wow
14901 wowen
14902 wowser
14903 "});
14904 cx.simulate_keystroke("w");
14905 cx.executor().run_until_parked();
14906 cx.update_editor(|editor, _, _| {
14907 if editor.context_menu.borrow_mut().is_some() {
14908 panic!(
14909 "expected completion menu to be hidden, as words completion are disabled for this editor"
14910 );
14911 }
14912 });
14913
14914 cx.update_editor(|editor, window, cx| {
14915 editor.show_word_completions(&ShowWordCompletions, window, cx);
14916 });
14917 cx.executor().run_until_parked();
14918 cx.update_editor(|editor, _, _| {
14919 if editor.context_menu.borrow_mut().is_some() {
14920 panic!(
14921 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14922 );
14923 }
14924 });
14925}
14926
14927fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14928 let position = || lsp::Position {
14929 line: params.text_document_position.position.line,
14930 character: params.text_document_position.position.character,
14931 };
14932 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14933 range: lsp::Range {
14934 start: position(),
14935 end: position(),
14936 },
14937 new_text: text.to_string(),
14938 }))
14939}
14940
14941#[gpui::test]
14942async fn test_multiline_completion(cx: &mut TestAppContext) {
14943 init_test(cx, |_| {});
14944
14945 let fs = FakeFs::new(cx.executor());
14946 fs.insert_tree(
14947 path!("/a"),
14948 json!({
14949 "main.ts": "a",
14950 }),
14951 )
14952 .await;
14953
14954 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14955 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14956 let typescript_language = Arc::new(Language::new(
14957 LanguageConfig {
14958 name: "TypeScript".into(),
14959 matcher: LanguageMatcher {
14960 path_suffixes: vec!["ts".to_string()],
14961 ..LanguageMatcher::default()
14962 },
14963 line_comments: vec!["// ".into()],
14964 ..LanguageConfig::default()
14965 },
14966 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14967 ));
14968 language_registry.add(typescript_language.clone());
14969 let mut fake_servers = language_registry.register_fake_lsp(
14970 "TypeScript",
14971 FakeLspAdapter {
14972 capabilities: lsp::ServerCapabilities {
14973 completion_provider: Some(lsp::CompletionOptions {
14974 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14975 ..lsp::CompletionOptions::default()
14976 }),
14977 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14978 ..lsp::ServerCapabilities::default()
14979 },
14980 // Emulate vtsls label generation
14981 label_for_completion: Some(Box::new(|item, _| {
14982 let text = if let Some(description) = item
14983 .label_details
14984 .as_ref()
14985 .and_then(|label_details| label_details.description.as_ref())
14986 {
14987 format!("{} {}", item.label, description)
14988 } else if let Some(detail) = &item.detail {
14989 format!("{} {}", item.label, detail)
14990 } else {
14991 item.label.clone()
14992 };
14993 Some(language::CodeLabel::plain(text, None))
14994 })),
14995 ..FakeLspAdapter::default()
14996 },
14997 );
14998 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14999 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15000 let worktree_id = workspace
15001 .update(cx, |workspace, _window, cx| {
15002 workspace.project().update(cx, |project, cx| {
15003 project.worktrees(cx).next().unwrap().read(cx).id()
15004 })
15005 })
15006 .unwrap();
15007 let _buffer = project
15008 .update(cx, |project, cx| {
15009 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15010 })
15011 .await
15012 .unwrap();
15013 let editor = workspace
15014 .update(cx, |workspace, window, cx| {
15015 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15016 })
15017 .unwrap()
15018 .await
15019 .unwrap()
15020 .downcast::<Editor>()
15021 .unwrap();
15022 let fake_server = fake_servers.next().await.unwrap();
15023
15024 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15025 let multiline_label_2 = "a\nb\nc\n";
15026 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15027 let multiline_description = "d\ne\nf\n";
15028 let multiline_detail_2 = "g\nh\ni\n";
15029
15030 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15031 move |params, _| async move {
15032 Ok(Some(lsp::CompletionResponse::Array(vec![
15033 lsp::CompletionItem {
15034 label: multiline_label.to_string(),
15035 text_edit: gen_text_edit(¶ms, "new_text_1"),
15036 ..lsp::CompletionItem::default()
15037 },
15038 lsp::CompletionItem {
15039 label: "single line label 1".to_string(),
15040 detail: Some(multiline_detail.to_string()),
15041 text_edit: gen_text_edit(¶ms, "new_text_2"),
15042 ..lsp::CompletionItem::default()
15043 },
15044 lsp::CompletionItem {
15045 label: "single line label 2".to_string(),
15046 label_details: Some(lsp::CompletionItemLabelDetails {
15047 description: Some(multiline_description.to_string()),
15048 detail: None,
15049 }),
15050 text_edit: gen_text_edit(¶ms, "new_text_2"),
15051 ..lsp::CompletionItem::default()
15052 },
15053 lsp::CompletionItem {
15054 label: multiline_label_2.to_string(),
15055 detail: Some(multiline_detail_2.to_string()),
15056 text_edit: gen_text_edit(¶ms, "new_text_3"),
15057 ..lsp::CompletionItem::default()
15058 },
15059 lsp::CompletionItem {
15060 label: "Label with many spaces and \t but without newlines".to_string(),
15061 detail: Some(
15062 "Details with many spaces and \t but without newlines".to_string(),
15063 ),
15064 text_edit: gen_text_edit(¶ms, "new_text_4"),
15065 ..lsp::CompletionItem::default()
15066 },
15067 ])))
15068 },
15069 );
15070
15071 editor.update_in(cx, |editor, window, cx| {
15072 cx.focus_self(window);
15073 editor.move_to_end(&MoveToEnd, window, cx);
15074 editor.handle_input(".", window, cx);
15075 });
15076 cx.run_until_parked();
15077 completion_handle.next().await.unwrap();
15078
15079 editor.update(cx, |editor, _| {
15080 assert!(editor.context_menu_visible());
15081 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15082 {
15083 let completion_labels = menu
15084 .completions
15085 .borrow()
15086 .iter()
15087 .map(|c| c.label.text.clone())
15088 .collect::<Vec<_>>();
15089 assert_eq!(
15090 completion_labels,
15091 &[
15092 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15093 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15094 "single line label 2 d e f ",
15095 "a b c g h i ",
15096 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15097 ],
15098 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15099 );
15100
15101 for completion in menu
15102 .completions
15103 .borrow()
15104 .iter() {
15105 assert_eq!(
15106 completion.label.filter_range,
15107 0..completion.label.text.len(),
15108 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15109 );
15110 }
15111 } else {
15112 panic!("expected completion menu to be open");
15113 }
15114 });
15115}
15116
15117#[gpui::test]
15118async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15119 init_test(cx, |_| {});
15120 let mut cx = EditorLspTestContext::new_rust(
15121 lsp::ServerCapabilities {
15122 completion_provider: Some(lsp::CompletionOptions {
15123 trigger_characters: Some(vec![".".to_string()]),
15124 ..Default::default()
15125 }),
15126 ..Default::default()
15127 },
15128 cx,
15129 )
15130 .await;
15131 cx.lsp
15132 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15133 Ok(Some(lsp::CompletionResponse::Array(vec![
15134 lsp::CompletionItem {
15135 label: "first".into(),
15136 ..Default::default()
15137 },
15138 lsp::CompletionItem {
15139 label: "last".into(),
15140 ..Default::default()
15141 },
15142 ])))
15143 });
15144 cx.set_state("variableˇ");
15145 cx.simulate_keystroke(".");
15146 cx.executor().run_until_parked();
15147
15148 cx.update_editor(|editor, _, _| {
15149 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15150 {
15151 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15152 } else {
15153 panic!("expected completion menu to be open");
15154 }
15155 });
15156
15157 cx.update_editor(|editor, window, cx| {
15158 editor.move_page_down(&MovePageDown::default(), window, cx);
15159 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15160 {
15161 assert!(
15162 menu.selected_item == 1,
15163 "expected PageDown to select the last item from the context menu"
15164 );
15165 } else {
15166 panic!("expected completion menu to stay open after PageDown");
15167 }
15168 });
15169
15170 cx.update_editor(|editor, window, cx| {
15171 editor.move_page_up(&MovePageUp::default(), window, cx);
15172 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15173 {
15174 assert!(
15175 menu.selected_item == 0,
15176 "expected PageUp to select the first item from the context menu"
15177 );
15178 } else {
15179 panic!("expected completion menu to stay open after PageUp");
15180 }
15181 });
15182}
15183
15184#[gpui::test]
15185async fn test_as_is_completions(cx: &mut TestAppContext) {
15186 init_test(cx, |_| {});
15187 let mut cx = EditorLspTestContext::new_rust(
15188 lsp::ServerCapabilities {
15189 completion_provider: Some(lsp::CompletionOptions {
15190 ..Default::default()
15191 }),
15192 ..Default::default()
15193 },
15194 cx,
15195 )
15196 .await;
15197 cx.lsp
15198 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15199 Ok(Some(lsp::CompletionResponse::Array(vec![
15200 lsp::CompletionItem {
15201 label: "unsafe".into(),
15202 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15203 range: lsp::Range {
15204 start: lsp::Position {
15205 line: 1,
15206 character: 2,
15207 },
15208 end: lsp::Position {
15209 line: 1,
15210 character: 3,
15211 },
15212 },
15213 new_text: "unsafe".to_string(),
15214 })),
15215 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15216 ..Default::default()
15217 },
15218 ])))
15219 });
15220 cx.set_state("fn a() {}\n nˇ");
15221 cx.executor().run_until_parked();
15222 cx.update_editor(|editor, window, cx| {
15223 editor.show_completions(
15224 &ShowCompletions {
15225 trigger: Some("\n".into()),
15226 },
15227 window,
15228 cx,
15229 );
15230 });
15231 cx.executor().run_until_parked();
15232
15233 cx.update_editor(|editor, window, cx| {
15234 editor.confirm_completion(&Default::default(), window, cx)
15235 });
15236 cx.executor().run_until_parked();
15237 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15238}
15239
15240#[gpui::test]
15241async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15242 init_test(cx, |_| {});
15243 let language =
15244 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15245 let mut cx = EditorLspTestContext::new(
15246 language,
15247 lsp::ServerCapabilities {
15248 completion_provider: Some(lsp::CompletionOptions {
15249 ..lsp::CompletionOptions::default()
15250 }),
15251 ..lsp::ServerCapabilities::default()
15252 },
15253 cx,
15254 )
15255 .await;
15256
15257 cx.set_state(
15258 "#ifndef BAR_H
15259#define BAR_H
15260
15261#include <stdbool.h>
15262
15263int fn_branch(bool do_branch1, bool do_branch2);
15264
15265#endif // BAR_H
15266ˇ",
15267 );
15268 cx.executor().run_until_parked();
15269 cx.update_editor(|editor, window, cx| {
15270 editor.handle_input("#", window, cx);
15271 });
15272 cx.executor().run_until_parked();
15273 cx.update_editor(|editor, window, cx| {
15274 editor.handle_input("i", window, cx);
15275 });
15276 cx.executor().run_until_parked();
15277 cx.update_editor(|editor, window, cx| {
15278 editor.handle_input("n", window, cx);
15279 });
15280 cx.executor().run_until_parked();
15281 cx.assert_editor_state(
15282 "#ifndef BAR_H
15283#define BAR_H
15284
15285#include <stdbool.h>
15286
15287int fn_branch(bool do_branch1, bool do_branch2);
15288
15289#endif // BAR_H
15290#inˇ",
15291 );
15292
15293 cx.lsp
15294 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15295 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15296 is_incomplete: false,
15297 item_defaults: None,
15298 items: vec![lsp::CompletionItem {
15299 kind: Some(lsp::CompletionItemKind::SNIPPET),
15300 label_details: Some(lsp::CompletionItemLabelDetails {
15301 detail: Some("header".to_string()),
15302 description: None,
15303 }),
15304 label: " include".to_string(),
15305 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15306 range: lsp::Range {
15307 start: lsp::Position {
15308 line: 8,
15309 character: 1,
15310 },
15311 end: lsp::Position {
15312 line: 8,
15313 character: 1,
15314 },
15315 },
15316 new_text: "include \"$0\"".to_string(),
15317 })),
15318 sort_text: Some("40b67681include".to_string()),
15319 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15320 filter_text: Some("include".to_string()),
15321 insert_text: Some("include \"$0\"".to_string()),
15322 ..lsp::CompletionItem::default()
15323 }],
15324 })))
15325 });
15326 cx.update_editor(|editor, window, cx| {
15327 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15328 });
15329 cx.executor().run_until_parked();
15330 cx.update_editor(|editor, window, cx| {
15331 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15332 });
15333 cx.executor().run_until_parked();
15334 cx.assert_editor_state(
15335 "#ifndef BAR_H
15336#define BAR_H
15337
15338#include <stdbool.h>
15339
15340int fn_branch(bool do_branch1, bool do_branch2);
15341
15342#endif // BAR_H
15343#include \"ˇ\"",
15344 );
15345
15346 cx.lsp
15347 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15348 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15349 is_incomplete: true,
15350 item_defaults: None,
15351 items: vec![lsp::CompletionItem {
15352 kind: Some(lsp::CompletionItemKind::FILE),
15353 label: "AGL/".to_string(),
15354 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15355 range: lsp::Range {
15356 start: lsp::Position {
15357 line: 8,
15358 character: 10,
15359 },
15360 end: lsp::Position {
15361 line: 8,
15362 character: 11,
15363 },
15364 },
15365 new_text: "AGL/".to_string(),
15366 })),
15367 sort_text: Some("40b67681AGL/".to_string()),
15368 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15369 filter_text: Some("AGL/".to_string()),
15370 insert_text: Some("AGL/".to_string()),
15371 ..lsp::CompletionItem::default()
15372 }],
15373 })))
15374 });
15375 cx.update_editor(|editor, window, cx| {
15376 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15377 });
15378 cx.executor().run_until_parked();
15379 cx.update_editor(|editor, window, cx| {
15380 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15381 });
15382 cx.executor().run_until_parked();
15383 cx.assert_editor_state(
15384 r##"#ifndef BAR_H
15385#define BAR_H
15386
15387#include <stdbool.h>
15388
15389int fn_branch(bool do_branch1, bool do_branch2);
15390
15391#endif // BAR_H
15392#include "AGL/ˇ"##,
15393 );
15394
15395 cx.update_editor(|editor, window, cx| {
15396 editor.handle_input("\"", window, cx);
15397 });
15398 cx.executor().run_until_parked();
15399 cx.assert_editor_state(
15400 r##"#ifndef BAR_H
15401#define BAR_H
15402
15403#include <stdbool.h>
15404
15405int fn_branch(bool do_branch1, bool do_branch2);
15406
15407#endif // BAR_H
15408#include "AGL/"ˇ"##,
15409 );
15410}
15411
15412#[gpui::test]
15413async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15414 init_test(cx, |_| {});
15415
15416 let mut cx = EditorLspTestContext::new_rust(
15417 lsp::ServerCapabilities {
15418 completion_provider: Some(lsp::CompletionOptions {
15419 trigger_characters: Some(vec![".".to_string()]),
15420 resolve_provider: Some(true),
15421 ..Default::default()
15422 }),
15423 ..Default::default()
15424 },
15425 cx,
15426 )
15427 .await;
15428
15429 cx.set_state("fn main() { let a = 2ˇ; }");
15430 cx.simulate_keystroke(".");
15431 let completion_item = lsp::CompletionItem {
15432 label: "Some".into(),
15433 kind: Some(lsp::CompletionItemKind::SNIPPET),
15434 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15435 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15436 kind: lsp::MarkupKind::Markdown,
15437 value: "```rust\nSome(2)\n```".to_string(),
15438 })),
15439 deprecated: Some(false),
15440 sort_text: Some("Some".to_string()),
15441 filter_text: Some("Some".to_string()),
15442 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15443 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15444 range: lsp::Range {
15445 start: lsp::Position {
15446 line: 0,
15447 character: 22,
15448 },
15449 end: lsp::Position {
15450 line: 0,
15451 character: 22,
15452 },
15453 },
15454 new_text: "Some(2)".to_string(),
15455 })),
15456 additional_text_edits: Some(vec![lsp::TextEdit {
15457 range: lsp::Range {
15458 start: lsp::Position {
15459 line: 0,
15460 character: 20,
15461 },
15462 end: lsp::Position {
15463 line: 0,
15464 character: 22,
15465 },
15466 },
15467 new_text: "".to_string(),
15468 }]),
15469 ..Default::default()
15470 };
15471
15472 let closure_completion_item = completion_item.clone();
15473 let counter = Arc::new(AtomicUsize::new(0));
15474 let counter_clone = counter.clone();
15475 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15476 let task_completion_item = closure_completion_item.clone();
15477 counter_clone.fetch_add(1, atomic::Ordering::Release);
15478 async move {
15479 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15480 is_incomplete: true,
15481 item_defaults: None,
15482 items: vec![task_completion_item],
15483 })))
15484 }
15485 });
15486
15487 cx.condition(|editor, _| editor.context_menu_visible())
15488 .await;
15489 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15490 assert!(request.next().await.is_some());
15491 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15492
15493 cx.simulate_keystrokes("S o m");
15494 cx.condition(|editor, _| editor.context_menu_visible())
15495 .await;
15496 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15497 assert!(request.next().await.is_some());
15498 assert!(request.next().await.is_some());
15499 assert!(request.next().await.is_some());
15500 request.close();
15501 assert!(request.next().await.is_none());
15502 assert_eq!(
15503 counter.load(atomic::Ordering::Acquire),
15504 4,
15505 "With the completions menu open, only one LSP request should happen per input"
15506 );
15507}
15508
15509#[gpui::test]
15510async fn test_toggle_comment(cx: &mut TestAppContext) {
15511 init_test(cx, |_| {});
15512 let mut cx = EditorTestContext::new(cx).await;
15513 let language = Arc::new(Language::new(
15514 LanguageConfig {
15515 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15516 ..Default::default()
15517 },
15518 Some(tree_sitter_rust::LANGUAGE.into()),
15519 ));
15520 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15521
15522 // If multiple selections intersect a line, the line is only toggled once.
15523 cx.set_state(indoc! {"
15524 fn a() {
15525 «//b();
15526 ˇ»// «c();
15527 //ˇ» d();
15528 }
15529 "});
15530
15531 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15532
15533 cx.assert_editor_state(indoc! {"
15534 fn a() {
15535 «b();
15536 c();
15537 ˇ» d();
15538 }
15539 "});
15540
15541 // The comment prefix is inserted at the same column for every line in a
15542 // selection.
15543 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15544
15545 cx.assert_editor_state(indoc! {"
15546 fn a() {
15547 // «b();
15548 // c();
15549 ˇ»// d();
15550 }
15551 "});
15552
15553 // If a selection ends at the beginning of a line, that line is not toggled.
15554 cx.set_selections_state(indoc! {"
15555 fn a() {
15556 // b();
15557 «// c();
15558 ˇ» // d();
15559 }
15560 "});
15561
15562 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15563
15564 cx.assert_editor_state(indoc! {"
15565 fn a() {
15566 // b();
15567 «c();
15568 ˇ» // d();
15569 }
15570 "});
15571
15572 // If a selection span a single line and is empty, the line is toggled.
15573 cx.set_state(indoc! {"
15574 fn a() {
15575 a();
15576 b();
15577 ˇ
15578 }
15579 "});
15580
15581 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15582
15583 cx.assert_editor_state(indoc! {"
15584 fn a() {
15585 a();
15586 b();
15587 //•ˇ
15588 }
15589 "});
15590
15591 // If a selection span multiple lines, empty lines are not toggled.
15592 cx.set_state(indoc! {"
15593 fn a() {
15594 «a();
15595
15596 c();ˇ»
15597 }
15598 "});
15599
15600 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15601
15602 cx.assert_editor_state(indoc! {"
15603 fn a() {
15604 // «a();
15605
15606 // c();ˇ»
15607 }
15608 "});
15609
15610 // If a selection includes multiple comment prefixes, all lines are uncommented.
15611 cx.set_state(indoc! {"
15612 fn a() {
15613 «// a();
15614 /// b();
15615 //! c();ˇ»
15616 }
15617 "});
15618
15619 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15620
15621 cx.assert_editor_state(indoc! {"
15622 fn a() {
15623 «a();
15624 b();
15625 c();ˇ»
15626 }
15627 "});
15628}
15629
15630#[gpui::test]
15631async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15632 init_test(cx, |_| {});
15633 let mut cx = EditorTestContext::new(cx).await;
15634 let language = Arc::new(Language::new(
15635 LanguageConfig {
15636 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15637 ..Default::default()
15638 },
15639 Some(tree_sitter_rust::LANGUAGE.into()),
15640 ));
15641 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15642
15643 let toggle_comments = &ToggleComments {
15644 advance_downwards: false,
15645 ignore_indent: true,
15646 };
15647
15648 // If multiple selections intersect a line, the line is only toggled once.
15649 cx.set_state(indoc! {"
15650 fn a() {
15651 // «b();
15652 // c();
15653 // ˇ» d();
15654 }
15655 "});
15656
15657 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15658
15659 cx.assert_editor_state(indoc! {"
15660 fn a() {
15661 «b();
15662 c();
15663 ˇ» d();
15664 }
15665 "});
15666
15667 // The comment prefix is inserted at the beginning of each line
15668 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15669
15670 cx.assert_editor_state(indoc! {"
15671 fn a() {
15672 // «b();
15673 // c();
15674 // ˇ» d();
15675 }
15676 "});
15677
15678 // If a selection ends at the beginning of a line, that line is not toggled.
15679 cx.set_selections_state(indoc! {"
15680 fn a() {
15681 // b();
15682 // «c();
15683 ˇ»// d();
15684 }
15685 "});
15686
15687 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15688
15689 cx.assert_editor_state(indoc! {"
15690 fn a() {
15691 // b();
15692 «c();
15693 ˇ»// d();
15694 }
15695 "});
15696
15697 // If a selection span a single line and is empty, the line is toggled.
15698 cx.set_state(indoc! {"
15699 fn a() {
15700 a();
15701 b();
15702 ˇ
15703 }
15704 "});
15705
15706 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15707
15708 cx.assert_editor_state(indoc! {"
15709 fn a() {
15710 a();
15711 b();
15712 //ˇ
15713 }
15714 "});
15715
15716 // If a selection span multiple lines, empty lines are not toggled.
15717 cx.set_state(indoc! {"
15718 fn a() {
15719 «a();
15720
15721 c();ˇ»
15722 }
15723 "});
15724
15725 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15726
15727 cx.assert_editor_state(indoc! {"
15728 fn a() {
15729 // «a();
15730
15731 // c();ˇ»
15732 }
15733 "});
15734
15735 // If a selection includes multiple comment prefixes, all lines are uncommented.
15736 cx.set_state(indoc! {"
15737 fn a() {
15738 // «a();
15739 /// b();
15740 //! c();ˇ»
15741 }
15742 "});
15743
15744 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15745
15746 cx.assert_editor_state(indoc! {"
15747 fn a() {
15748 «a();
15749 b();
15750 c();ˇ»
15751 }
15752 "});
15753}
15754
15755#[gpui::test]
15756async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15757 init_test(cx, |_| {});
15758
15759 let language = Arc::new(Language::new(
15760 LanguageConfig {
15761 line_comments: vec!["// ".into()],
15762 ..Default::default()
15763 },
15764 Some(tree_sitter_rust::LANGUAGE.into()),
15765 ));
15766
15767 let mut cx = EditorTestContext::new(cx).await;
15768
15769 cx.language_registry().add(language.clone());
15770 cx.update_buffer(|buffer, cx| {
15771 buffer.set_language(Some(language), cx);
15772 });
15773
15774 let toggle_comments = &ToggleComments {
15775 advance_downwards: true,
15776 ignore_indent: false,
15777 };
15778
15779 // Single cursor on one line -> advance
15780 // Cursor moves horizontally 3 characters as well on non-blank line
15781 cx.set_state(indoc!(
15782 "fn a() {
15783 ˇdog();
15784 cat();
15785 }"
15786 ));
15787 cx.update_editor(|editor, window, cx| {
15788 editor.toggle_comments(toggle_comments, window, cx);
15789 });
15790 cx.assert_editor_state(indoc!(
15791 "fn a() {
15792 // dog();
15793 catˇ();
15794 }"
15795 ));
15796
15797 // Single selection on one line -> don't advance
15798 cx.set_state(indoc!(
15799 "fn a() {
15800 «dog()ˇ»;
15801 cat();
15802 }"
15803 ));
15804 cx.update_editor(|editor, window, cx| {
15805 editor.toggle_comments(toggle_comments, window, cx);
15806 });
15807 cx.assert_editor_state(indoc!(
15808 "fn a() {
15809 // «dog()ˇ»;
15810 cat();
15811 }"
15812 ));
15813
15814 // Multiple cursors on one line -> advance
15815 cx.set_state(indoc!(
15816 "fn a() {
15817 ˇdˇog();
15818 cat();
15819 }"
15820 ));
15821 cx.update_editor(|editor, window, cx| {
15822 editor.toggle_comments(toggle_comments, window, cx);
15823 });
15824 cx.assert_editor_state(indoc!(
15825 "fn a() {
15826 // dog();
15827 catˇ(ˇ);
15828 }"
15829 ));
15830
15831 // Multiple cursors on one line, with selection -> don't advance
15832 cx.set_state(indoc!(
15833 "fn a() {
15834 ˇdˇog«()ˇ»;
15835 cat();
15836 }"
15837 ));
15838 cx.update_editor(|editor, window, cx| {
15839 editor.toggle_comments(toggle_comments, window, cx);
15840 });
15841 cx.assert_editor_state(indoc!(
15842 "fn a() {
15843 // ˇdˇog«()ˇ»;
15844 cat();
15845 }"
15846 ));
15847
15848 // Single cursor on one line -> advance
15849 // Cursor moves to column 0 on blank line
15850 cx.set_state(indoc!(
15851 "fn a() {
15852 ˇdog();
15853
15854 cat();
15855 }"
15856 ));
15857 cx.update_editor(|editor, window, cx| {
15858 editor.toggle_comments(toggle_comments, window, cx);
15859 });
15860 cx.assert_editor_state(indoc!(
15861 "fn a() {
15862 // dog();
15863 ˇ
15864 cat();
15865 }"
15866 ));
15867
15868 // Single cursor on one line -> advance
15869 // Cursor starts and ends at column 0
15870 cx.set_state(indoc!(
15871 "fn a() {
15872 ˇ dog();
15873 cat();
15874 }"
15875 ));
15876 cx.update_editor(|editor, window, cx| {
15877 editor.toggle_comments(toggle_comments, window, cx);
15878 });
15879 cx.assert_editor_state(indoc!(
15880 "fn a() {
15881 // dog();
15882 ˇ cat();
15883 }"
15884 ));
15885}
15886
15887#[gpui::test]
15888async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15889 init_test(cx, |_| {});
15890
15891 let mut cx = EditorTestContext::new(cx).await;
15892
15893 let html_language = Arc::new(
15894 Language::new(
15895 LanguageConfig {
15896 name: "HTML".into(),
15897 block_comment: Some(BlockCommentConfig {
15898 start: "<!-- ".into(),
15899 prefix: "".into(),
15900 end: " -->".into(),
15901 tab_size: 0,
15902 }),
15903 ..Default::default()
15904 },
15905 Some(tree_sitter_html::LANGUAGE.into()),
15906 )
15907 .with_injection_query(
15908 r#"
15909 (script_element
15910 (raw_text) @injection.content
15911 (#set! injection.language "javascript"))
15912 "#,
15913 )
15914 .unwrap(),
15915 );
15916
15917 let javascript_language = Arc::new(Language::new(
15918 LanguageConfig {
15919 name: "JavaScript".into(),
15920 line_comments: vec!["// ".into()],
15921 ..Default::default()
15922 },
15923 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15924 ));
15925
15926 cx.language_registry().add(html_language.clone());
15927 cx.language_registry().add(javascript_language);
15928 cx.update_buffer(|buffer, cx| {
15929 buffer.set_language(Some(html_language), cx);
15930 });
15931
15932 // Toggle comments for empty selections
15933 cx.set_state(
15934 &r#"
15935 <p>A</p>ˇ
15936 <p>B</p>ˇ
15937 <p>C</p>ˇ
15938 "#
15939 .unindent(),
15940 );
15941 cx.update_editor(|editor, window, cx| {
15942 editor.toggle_comments(&ToggleComments::default(), window, cx)
15943 });
15944 cx.assert_editor_state(
15945 &r#"
15946 <!-- <p>A</p>ˇ -->
15947 <!-- <p>B</p>ˇ -->
15948 <!-- <p>C</p>ˇ -->
15949 "#
15950 .unindent(),
15951 );
15952 cx.update_editor(|editor, window, cx| {
15953 editor.toggle_comments(&ToggleComments::default(), window, cx)
15954 });
15955 cx.assert_editor_state(
15956 &r#"
15957 <p>A</p>ˇ
15958 <p>B</p>ˇ
15959 <p>C</p>ˇ
15960 "#
15961 .unindent(),
15962 );
15963
15964 // Toggle comments for mixture of empty and non-empty selections, where
15965 // multiple selections occupy a given line.
15966 cx.set_state(
15967 &r#"
15968 <p>A«</p>
15969 <p>ˇ»B</p>ˇ
15970 <p>C«</p>
15971 <p>ˇ»D</p>ˇ
15972 "#
15973 .unindent(),
15974 );
15975
15976 cx.update_editor(|editor, window, cx| {
15977 editor.toggle_comments(&ToggleComments::default(), window, cx)
15978 });
15979 cx.assert_editor_state(
15980 &r#"
15981 <!-- <p>A«</p>
15982 <p>ˇ»B</p>ˇ -->
15983 <!-- <p>C«</p>
15984 <p>ˇ»D</p>ˇ -->
15985 "#
15986 .unindent(),
15987 );
15988 cx.update_editor(|editor, window, cx| {
15989 editor.toggle_comments(&ToggleComments::default(), window, cx)
15990 });
15991 cx.assert_editor_state(
15992 &r#"
15993 <p>A«</p>
15994 <p>ˇ»B</p>ˇ
15995 <p>C«</p>
15996 <p>ˇ»D</p>ˇ
15997 "#
15998 .unindent(),
15999 );
16000
16001 // Toggle comments when different languages are active for different
16002 // selections.
16003 cx.set_state(
16004 &r#"
16005 ˇ<script>
16006 ˇvar x = new Y();
16007 ˇ</script>
16008 "#
16009 .unindent(),
16010 );
16011 cx.executor().run_until_parked();
16012 cx.update_editor(|editor, window, cx| {
16013 editor.toggle_comments(&ToggleComments::default(), window, cx)
16014 });
16015 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16016 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16017 cx.assert_editor_state(
16018 &r#"
16019 <!-- ˇ<script> -->
16020 // ˇvar x = new Y();
16021 <!-- ˇ</script> -->
16022 "#
16023 .unindent(),
16024 );
16025}
16026
16027#[gpui::test]
16028fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16029 init_test(cx, |_| {});
16030
16031 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16032 let multibuffer = cx.new(|cx| {
16033 let mut multibuffer = MultiBuffer::new(ReadWrite);
16034 multibuffer.push_excerpts(
16035 buffer.clone(),
16036 [
16037 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16038 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16039 ],
16040 cx,
16041 );
16042 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16043 multibuffer
16044 });
16045
16046 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16047 editor.update_in(cx, |editor, window, cx| {
16048 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16050 s.select_ranges([
16051 Point::new(0, 0)..Point::new(0, 0),
16052 Point::new(1, 0)..Point::new(1, 0),
16053 ])
16054 });
16055
16056 editor.handle_input("X", window, cx);
16057 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16058 assert_eq!(
16059 editor.selections.ranges(&editor.display_snapshot(cx)),
16060 [
16061 Point::new(0, 1)..Point::new(0, 1),
16062 Point::new(1, 1)..Point::new(1, 1),
16063 ]
16064 );
16065
16066 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16067 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16068 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16069 });
16070 editor.backspace(&Default::default(), window, cx);
16071 assert_eq!(editor.text(cx), "Xa\nbbb");
16072 assert_eq!(
16073 editor.selections.ranges(&editor.display_snapshot(cx)),
16074 [Point::new(1, 0)..Point::new(1, 0)]
16075 );
16076
16077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16078 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16079 });
16080 editor.backspace(&Default::default(), window, cx);
16081 assert_eq!(editor.text(cx), "X\nbb");
16082 assert_eq!(
16083 editor.selections.ranges(&editor.display_snapshot(cx)),
16084 [Point::new(0, 1)..Point::new(0, 1)]
16085 );
16086 });
16087}
16088
16089#[gpui::test]
16090fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16091 init_test(cx, |_| {});
16092
16093 let markers = vec![('[', ']').into(), ('(', ')').into()];
16094 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16095 indoc! {"
16096 [aaaa
16097 (bbbb]
16098 cccc)",
16099 },
16100 markers.clone(),
16101 );
16102 let excerpt_ranges = markers.into_iter().map(|marker| {
16103 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16104 ExcerptRange::new(context)
16105 });
16106 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16107 let multibuffer = cx.new(|cx| {
16108 let mut multibuffer = MultiBuffer::new(ReadWrite);
16109 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16110 multibuffer
16111 });
16112
16113 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16114 editor.update_in(cx, |editor, window, cx| {
16115 let (expected_text, selection_ranges) = marked_text_ranges(
16116 indoc! {"
16117 aaaa
16118 bˇbbb
16119 bˇbbˇb
16120 cccc"
16121 },
16122 true,
16123 );
16124 assert_eq!(editor.text(cx), expected_text);
16125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16126 s.select_ranges(selection_ranges)
16127 });
16128
16129 editor.handle_input("X", window, cx);
16130
16131 let (expected_text, expected_selections) = marked_text_ranges(
16132 indoc! {"
16133 aaaa
16134 bXˇbbXb
16135 bXˇbbXˇb
16136 cccc"
16137 },
16138 false,
16139 );
16140 assert_eq!(editor.text(cx), expected_text);
16141 assert_eq!(
16142 editor.selections.ranges(&editor.display_snapshot(cx)),
16143 expected_selections
16144 );
16145
16146 editor.newline(&Newline, window, cx);
16147 let (expected_text, expected_selections) = marked_text_ranges(
16148 indoc! {"
16149 aaaa
16150 bX
16151 ˇbbX
16152 b
16153 bX
16154 ˇbbX
16155 ˇb
16156 cccc"
16157 },
16158 false,
16159 );
16160 assert_eq!(editor.text(cx), expected_text);
16161 assert_eq!(
16162 editor.selections.ranges(&editor.display_snapshot(cx)),
16163 expected_selections
16164 );
16165 });
16166}
16167
16168#[gpui::test]
16169fn test_refresh_selections(cx: &mut TestAppContext) {
16170 init_test(cx, |_| {});
16171
16172 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16173 let mut excerpt1_id = None;
16174 let multibuffer = cx.new(|cx| {
16175 let mut multibuffer = MultiBuffer::new(ReadWrite);
16176 excerpt1_id = multibuffer
16177 .push_excerpts(
16178 buffer.clone(),
16179 [
16180 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16181 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16182 ],
16183 cx,
16184 )
16185 .into_iter()
16186 .next();
16187 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16188 multibuffer
16189 });
16190
16191 let editor = cx.add_window(|window, cx| {
16192 let mut editor = build_editor(multibuffer.clone(), window, cx);
16193 let snapshot = editor.snapshot(window, cx);
16194 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16195 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16196 });
16197 editor.begin_selection(
16198 Point::new(2, 1).to_display_point(&snapshot),
16199 true,
16200 1,
16201 window,
16202 cx,
16203 );
16204 assert_eq!(
16205 editor.selections.ranges(&editor.display_snapshot(cx)),
16206 [
16207 Point::new(1, 3)..Point::new(1, 3),
16208 Point::new(2, 1)..Point::new(2, 1),
16209 ]
16210 );
16211 editor
16212 });
16213
16214 // Refreshing selections is a no-op when excerpts haven't changed.
16215 _ = editor.update(cx, |editor, window, cx| {
16216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16217 assert_eq!(
16218 editor.selections.ranges(&editor.display_snapshot(cx)),
16219 [
16220 Point::new(1, 3)..Point::new(1, 3),
16221 Point::new(2, 1)..Point::new(2, 1),
16222 ]
16223 );
16224 });
16225
16226 multibuffer.update(cx, |multibuffer, cx| {
16227 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16228 });
16229 _ = editor.update(cx, |editor, window, cx| {
16230 // Removing an excerpt causes the first selection to become degenerate.
16231 assert_eq!(
16232 editor.selections.ranges(&editor.display_snapshot(cx)),
16233 [
16234 Point::new(0, 0)..Point::new(0, 0),
16235 Point::new(0, 1)..Point::new(0, 1)
16236 ]
16237 );
16238
16239 // Refreshing selections will relocate the first selection to the original buffer
16240 // location.
16241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16242 assert_eq!(
16243 editor.selections.ranges(&editor.display_snapshot(cx)),
16244 [
16245 Point::new(0, 1)..Point::new(0, 1),
16246 Point::new(0, 3)..Point::new(0, 3)
16247 ]
16248 );
16249 assert!(editor.selections.pending_anchor().is_some());
16250 });
16251}
16252
16253#[gpui::test]
16254fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16255 init_test(cx, |_| {});
16256
16257 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16258 let mut excerpt1_id = None;
16259 let multibuffer = cx.new(|cx| {
16260 let mut multibuffer = MultiBuffer::new(ReadWrite);
16261 excerpt1_id = multibuffer
16262 .push_excerpts(
16263 buffer.clone(),
16264 [
16265 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16266 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16267 ],
16268 cx,
16269 )
16270 .into_iter()
16271 .next();
16272 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16273 multibuffer
16274 });
16275
16276 let editor = cx.add_window(|window, cx| {
16277 let mut editor = build_editor(multibuffer.clone(), window, cx);
16278 let snapshot = editor.snapshot(window, cx);
16279 editor.begin_selection(
16280 Point::new(1, 3).to_display_point(&snapshot),
16281 false,
16282 1,
16283 window,
16284 cx,
16285 );
16286 assert_eq!(
16287 editor.selections.ranges(&editor.display_snapshot(cx)),
16288 [Point::new(1, 3)..Point::new(1, 3)]
16289 );
16290 editor
16291 });
16292
16293 multibuffer.update(cx, |multibuffer, cx| {
16294 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16295 });
16296 _ = editor.update(cx, |editor, window, cx| {
16297 assert_eq!(
16298 editor.selections.ranges(&editor.display_snapshot(cx)),
16299 [Point::new(0, 0)..Point::new(0, 0)]
16300 );
16301
16302 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16303 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16304 assert_eq!(
16305 editor.selections.ranges(&editor.display_snapshot(cx)),
16306 [Point::new(0, 3)..Point::new(0, 3)]
16307 );
16308 assert!(editor.selections.pending_anchor().is_some());
16309 });
16310}
16311
16312#[gpui::test]
16313async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16314 init_test(cx, |_| {});
16315
16316 let language = Arc::new(
16317 Language::new(
16318 LanguageConfig {
16319 brackets: BracketPairConfig {
16320 pairs: vec![
16321 BracketPair {
16322 start: "{".to_string(),
16323 end: "}".to_string(),
16324 close: true,
16325 surround: true,
16326 newline: true,
16327 },
16328 BracketPair {
16329 start: "/* ".to_string(),
16330 end: " */".to_string(),
16331 close: true,
16332 surround: true,
16333 newline: true,
16334 },
16335 ],
16336 ..Default::default()
16337 },
16338 ..Default::default()
16339 },
16340 Some(tree_sitter_rust::LANGUAGE.into()),
16341 )
16342 .with_indents_query("")
16343 .unwrap(),
16344 );
16345
16346 let text = concat!(
16347 "{ }\n", //
16348 " x\n", //
16349 " /* */\n", //
16350 "x\n", //
16351 "{{} }\n", //
16352 );
16353
16354 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16355 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16356 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16357 editor
16358 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16359 .await;
16360
16361 editor.update_in(cx, |editor, window, cx| {
16362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16363 s.select_display_ranges([
16364 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16365 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16366 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16367 ])
16368 });
16369 editor.newline(&Newline, window, cx);
16370
16371 assert_eq!(
16372 editor.buffer().read(cx).read(cx).text(),
16373 concat!(
16374 "{ \n", // Suppress rustfmt
16375 "\n", //
16376 "}\n", //
16377 " x\n", //
16378 " /* \n", //
16379 " \n", //
16380 " */\n", //
16381 "x\n", //
16382 "{{} \n", //
16383 "}\n", //
16384 )
16385 );
16386 });
16387}
16388
16389#[gpui::test]
16390fn test_highlighted_ranges(cx: &mut TestAppContext) {
16391 init_test(cx, |_| {});
16392
16393 let editor = cx.add_window(|window, cx| {
16394 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16395 build_editor(buffer, window, cx)
16396 });
16397
16398 _ = editor.update(cx, |editor, window, cx| {
16399 struct Type1;
16400 struct Type2;
16401
16402 let buffer = editor.buffer.read(cx).snapshot(cx);
16403
16404 let anchor_range =
16405 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16406
16407 editor.highlight_background::<Type1>(
16408 &[
16409 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16410 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16411 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16412 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16413 ],
16414 |_| Hsla::red(),
16415 cx,
16416 );
16417 editor.highlight_background::<Type2>(
16418 &[
16419 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16420 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16421 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16422 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16423 ],
16424 |_| Hsla::green(),
16425 cx,
16426 );
16427
16428 let snapshot = editor.snapshot(window, cx);
16429 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16430 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16431 &snapshot,
16432 cx.theme(),
16433 );
16434 assert_eq!(
16435 highlighted_ranges,
16436 &[
16437 (
16438 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16439 Hsla::green(),
16440 ),
16441 (
16442 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16443 Hsla::red(),
16444 ),
16445 (
16446 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16447 Hsla::green(),
16448 ),
16449 (
16450 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16451 Hsla::red(),
16452 ),
16453 ]
16454 );
16455 assert_eq!(
16456 editor.sorted_background_highlights_in_range(
16457 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16458 &snapshot,
16459 cx.theme(),
16460 ),
16461 &[(
16462 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16463 Hsla::red(),
16464 )]
16465 );
16466 });
16467}
16468
16469#[gpui::test]
16470async fn test_following(cx: &mut TestAppContext) {
16471 init_test(cx, |_| {});
16472
16473 let fs = FakeFs::new(cx.executor());
16474 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16475
16476 let buffer = project.update(cx, |project, cx| {
16477 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16478 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16479 });
16480 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16481 let follower = cx.update(|cx| {
16482 cx.open_window(
16483 WindowOptions {
16484 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16485 gpui::Point::new(px(0.), px(0.)),
16486 gpui::Point::new(px(10.), px(80.)),
16487 ))),
16488 ..Default::default()
16489 },
16490 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16491 )
16492 .unwrap()
16493 });
16494
16495 let is_still_following = Rc::new(RefCell::new(true));
16496 let follower_edit_event_count = Rc::new(RefCell::new(0));
16497 let pending_update = Rc::new(RefCell::new(None));
16498 let leader_entity = leader.root(cx).unwrap();
16499 let follower_entity = follower.root(cx).unwrap();
16500 _ = follower.update(cx, {
16501 let update = pending_update.clone();
16502 let is_still_following = is_still_following.clone();
16503 let follower_edit_event_count = follower_edit_event_count.clone();
16504 |_, window, cx| {
16505 cx.subscribe_in(
16506 &leader_entity,
16507 window,
16508 move |_, leader, event, window, cx| {
16509 leader.read(cx).add_event_to_update_proto(
16510 event,
16511 &mut update.borrow_mut(),
16512 window,
16513 cx,
16514 );
16515 },
16516 )
16517 .detach();
16518
16519 cx.subscribe_in(
16520 &follower_entity,
16521 window,
16522 move |_, _, event: &EditorEvent, _window, _cx| {
16523 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16524 *is_still_following.borrow_mut() = false;
16525 }
16526
16527 if let EditorEvent::BufferEdited = event {
16528 *follower_edit_event_count.borrow_mut() += 1;
16529 }
16530 },
16531 )
16532 .detach();
16533 }
16534 });
16535
16536 // Update the selections only
16537 _ = leader.update(cx, |leader, window, cx| {
16538 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16539 s.select_ranges([1..1])
16540 });
16541 });
16542 follower
16543 .update(cx, |follower, window, cx| {
16544 follower.apply_update_proto(
16545 &project,
16546 pending_update.borrow_mut().take().unwrap(),
16547 window,
16548 cx,
16549 )
16550 })
16551 .unwrap()
16552 .await
16553 .unwrap();
16554 _ = follower.update(cx, |follower, _, cx| {
16555 assert_eq!(
16556 follower.selections.ranges(&follower.display_snapshot(cx)),
16557 vec![1..1]
16558 );
16559 });
16560 assert!(*is_still_following.borrow());
16561 assert_eq!(*follower_edit_event_count.borrow(), 0);
16562
16563 // Update the scroll position only
16564 _ = leader.update(cx, |leader, window, cx| {
16565 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16566 });
16567 follower
16568 .update(cx, |follower, window, cx| {
16569 follower.apply_update_proto(
16570 &project,
16571 pending_update.borrow_mut().take().unwrap(),
16572 window,
16573 cx,
16574 )
16575 })
16576 .unwrap()
16577 .await
16578 .unwrap();
16579 assert_eq!(
16580 follower
16581 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16582 .unwrap(),
16583 gpui::Point::new(1.5, 3.5)
16584 );
16585 assert!(*is_still_following.borrow());
16586 assert_eq!(*follower_edit_event_count.borrow(), 0);
16587
16588 // Update the selections and scroll position. The follower's scroll position is updated
16589 // via autoscroll, not via the leader's exact scroll position.
16590 _ = leader.update(cx, |leader, window, cx| {
16591 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16592 s.select_ranges([0..0])
16593 });
16594 leader.request_autoscroll(Autoscroll::newest(), cx);
16595 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16596 });
16597 follower
16598 .update(cx, |follower, window, cx| {
16599 follower.apply_update_proto(
16600 &project,
16601 pending_update.borrow_mut().take().unwrap(),
16602 window,
16603 cx,
16604 )
16605 })
16606 .unwrap()
16607 .await
16608 .unwrap();
16609 _ = follower.update(cx, |follower, _, cx| {
16610 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16611 assert_eq!(
16612 follower.selections.ranges(&follower.display_snapshot(cx)),
16613 vec![0..0]
16614 );
16615 });
16616 assert!(*is_still_following.borrow());
16617
16618 // Creating a pending selection that precedes another selection
16619 _ = leader.update(cx, |leader, window, cx| {
16620 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16621 s.select_ranges([1..1])
16622 });
16623 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16624 });
16625 follower
16626 .update(cx, |follower, window, cx| {
16627 follower.apply_update_proto(
16628 &project,
16629 pending_update.borrow_mut().take().unwrap(),
16630 window,
16631 cx,
16632 )
16633 })
16634 .unwrap()
16635 .await
16636 .unwrap();
16637 _ = follower.update(cx, |follower, _, cx| {
16638 assert_eq!(
16639 follower.selections.ranges(&follower.display_snapshot(cx)),
16640 vec![0..0, 1..1]
16641 );
16642 });
16643 assert!(*is_still_following.borrow());
16644
16645 // Extend the pending selection so that it surrounds another selection
16646 _ = leader.update(cx, |leader, window, cx| {
16647 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16648 });
16649 follower
16650 .update(cx, |follower, window, cx| {
16651 follower.apply_update_proto(
16652 &project,
16653 pending_update.borrow_mut().take().unwrap(),
16654 window,
16655 cx,
16656 )
16657 })
16658 .unwrap()
16659 .await
16660 .unwrap();
16661 _ = follower.update(cx, |follower, _, cx| {
16662 assert_eq!(
16663 follower.selections.ranges(&follower.display_snapshot(cx)),
16664 vec![0..2]
16665 );
16666 });
16667
16668 // Scrolling locally breaks the follow
16669 _ = follower.update(cx, |follower, window, cx| {
16670 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16671 follower.set_scroll_anchor(
16672 ScrollAnchor {
16673 anchor: top_anchor,
16674 offset: gpui::Point::new(0.0, 0.5),
16675 },
16676 window,
16677 cx,
16678 );
16679 });
16680 assert!(!(*is_still_following.borrow()));
16681}
16682
16683#[gpui::test]
16684async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16685 init_test(cx, |_| {});
16686
16687 let fs = FakeFs::new(cx.executor());
16688 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16689 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16690 let pane = workspace
16691 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16692 .unwrap();
16693
16694 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16695
16696 let leader = pane.update_in(cx, |_, window, cx| {
16697 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16698 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16699 });
16700
16701 // Start following the editor when it has no excerpts.
16702 let mut state_message =
16703 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16704 let workspace_entity = workspace.root(cx).unwrap();
16705 let follower_1 = cx
16706 .update_window(*workspace.deref(), |_, window, cx| {
16707 Editor::from_state_proto(
16708 workspace_entity,
16709 ViewId {
16710 creator: CollaboratorId::PeerId(PeerId::default()),
16711 id: 0,
16712 },
16713 &mut state_message,
16714 window,
16715 cx,
16716 )
16717 })
16718 .unwrap()
16719 .unwrap()
16720 .await
16721 .unwrap();
16722
16723 let update_message = Rc::new(RefCell::new(None));
16724 follower_1.update_in(cx, {
16725 let update = update_message.clone();
16726 |_, window, cx| {
16727 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16728 leader.read(cx).add_event_to_update_proto(
16729 event,
16730 &mut update.borrow_mut(),
16731 window,
16732 cx,
16733 );
16734 })
16735 .detach();
16736 }
16737 });
16738
16739 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16740 (
16741 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16742 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16743 )
16744 });
16745
16746 // Insert some excerpts.
16747 leader.update(cx, |leader, cx| {
16748 leader.buffer.update(cx, |multibuffer, cx| {
16749 multibuffer.set_excerpts_for_path(
16750 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16751 buffer_1.clone(),
16752 vec![
16753 Point::row_range(0..3),
16754 Point::row_range(1..6),
16755 Point::row_range(12..15),
16756 ],
16757 0,
16758 cx,
16759 );
16760 multibuffer.set_excerpts_for_path(
16761 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16762 buffer_2.clone(),
16763 vec![Point::row_range(0..6), Point::row_range(8..12)],
16764 0,
16765 cx,
16766 );
16767 });
16768 });
16769
16770 // Apply the update of adding the excerpts.
16771 follower_1
16772 .update_in(cx, |follower, window, cx| {
16773 follower.apply_update_proto(
16774 &project,
16775 update_message.borrow().clone().unwrap(),
16776 window,
16777 cx,
16778 )
16779 })
16780 .await
16781 .unwrap();
16782 assert_eq!(
16783 follower_1.update(cx, |editor, cx| editor.text(cx)),
16784 leader.update(cx, |editor, cx| editor.text(cx))
16785 );
16786 update_message.borrow_mut().take();
16787
16788 // Start following separately after it already has excerpts.
16789 let mut state_message =
16790 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16791 let workspace_entity = workspace.root(cx).unwrap();
16792 let follower_2 = cx
16793 .update_window(*workspace.deref(), |_, window, cx| {
16794 Editor::from_state_proto(
16795 workspace_entity,
16796 ViewId {
16797 creator: CollaboratorId::PeerId(PeerId::default()),
16798 id: 0,
16799 },
16800 &mut state_message,
16801 window,
16802 cx,
16803 )
16804 })
16805 .unwrap()
16806 .unwrap()
16807 .await
16808 .unwrap();
16809 assert_eq!(
16810 follower_2.update(cx, |editor, cx| editor.text(cx)),
16811 leader.update(cx, |editor, cx| editor.text(cx))
16812 );
16813
16814 // Remove some excerpts.
16815 leader.update(cx, |leader, cx| {
16816 leader.buffer.update(cx, |multibuffer, cx| {
16817 let excerpt_ids = multibuffer.excerpt_ids();
16818 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16819 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16820 });
16821 });
16822
16823 // Apply the update of removing the excerpts.
16824 follower_1
16825 .update_in(cx, |follower, window, cx| {
16826 follower.apply_update_proto(
16827 &project,
16828 update_message.borrow().clone().unwrap(),
16829 window,
16830 cx,
16831 )
16832 })
16833 .await
16834 .unwrap();
16835 follower_2
16836 .update_in(cx, |follower, window, cx| {
16837 follower.apply_update_proto(
16838 &project,
16839 update_message.borrow().clone().unwrap(),
16840 window,
16841 cx,
16842 )
16843 })
16844 .await
16845 .unwrap();
16846 update_message.borrow_mut().take();
16847 assert_eq!(
16848 follower_1.update(cx, |editor, cx| editor.text(cx)),
16849 leader.update(cx, |editor, cx| editor.text(cx))
16850 );
16851}
16852
16853#[gpui::test]
16854async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16855 init_test(cx, |_| {});
16856
16857 let mut cx = EditorTestContext::new(cx).await;
16858 let lsp_store =
16859 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16860
16861 cx.set_state(indoc! {"
16862 ˇfn func(abc def: i32) -> u32 {
16863 }
16864 "});
16865
16866 cx.update(|_, cx| {
16867 lsp_store.update(cx, |lsp_store, cx| {
16868 lsp_store
16869 .update_diagnostics(
16870 LanguageServerId(0),
16871 lsp::PublishDiagnosticsParams {
16872 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16873 version: None,
16874 diagnostics: vec![
16875 lsp::Diagnostic {
16876 range: lsp::Range::new(
16877 lsp::Position::new(0, 11),
16878 lsp::Position::new(0, 12),
16879 ),
16880 severity: Some(lsp::DiagnosticSeverity::ERROR),
16881 ..Default::default()
16882 },
16883 lsp::Diagnostic {
16884 range: lsp::Range::new(
16885 lsp::Position::new(0, 12),
16886 lsp::Position::new(0, 15),
16887 ),
16888 severity: Some(lsp::DiagnosticSeverity::ERROR),
16889 ..Default::default()
16890 },
16891 lsp::Diagnostic {
16892 range: lsp::Range::new(
16893 lsp::Position::new(0, 25),
16894 lsp::Position::new(0, 28),
16895 ),
16896 severity: Some(lsp::DiagnosticSeverity::ERROR),
16897 ..Default::default()
16898 },
16899 ],
16900 },
16901 None,
16902 DiagnosticSourceKind::Pushed,
16903 &[],
16904 cx,
16905 )
16906 .unwrap()
16907 });
16908 });
16909
16910 executor.run_until_parked();
16911
16912 cx.update_editor(|editor, window, cx| {
16913 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16914 });
16915
16916 cx.assert_editor_state(indoc! {"
16917 fn func(abc def: i32) -> ˇu32 {
16918 }
16919 "});
16920
16921 cx.update_editor(|editor, window, cx| {
16922 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16923 });
16924
16925 cx.assert_editor_state(indoc! {"
16926 fn func(abc ˇdef: i32) -> u32 {
16927 }
16928 "});
16929
16930 cx.update_editor(|editor, window, cx| {
16931 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16932 });
16933
16934 cx.assert_editor_state(indoc! {"
16935 fn func(abcˇ def: i32) -> u32 {
16936 }
16937 "});
16938
16939 cx.update_editor(|editor, window, cx| {
16940 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16941 });
16942
16943 cx.assert_editor_state(indoc! {"
16944 fn func(abc def: i32) -> ˇu32 {
16945 }
16946 "});
16947}
16948
16949#[gpui::test]
16950async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16951 init_test(cx, |_| {});
16952
16953 let mut cx = EditorTestContext::new(cx).await;
16954
16955 let diff_base = r#"
16956 use some::mod;
16957
16958 const A: u32 = 42;
16959
16960 fn main() {
16961 println!("hello");
16962
16963 println!("world");
16964 }
16965 "#
16966 .unindent();
16967
16968 // Edits are modified, removed, modified, added
16969 cx.set_state(
16970 &r#"
16971 use some::modified;
16972
16973 ˇ
16974 fn main() {
16975 println!("hello there");
16976
16977 println!("around the");
16978 println!("world");
16979 }
16980 "#
16981 .unindent(),
16982 );
16983
16984 cx.set_head_text(&diff_base);
16985 executor.run_until_parked();
16986
16987 cx.update_editor(|editor, window, cx| {
16988 //Wrap around the bottom of the buffer
16989 for _ in 0..3 {
16990 editor.go_to_next_hunk(&GoToHunk, window, cx);
16991 }
16992 });
16993
16994 cx.assert_editor_state(
16995 &r#"
16996 ˇuse some::modified;
16997
16998
16999 fn main() {
17000 println!("hello there");
17001
17002 println!("around the");
17003 println!("world");
17004 }
17005 "#
17006 .unindent(),
17007 );
17008
17009 cx.update_editor(|editor, window, cx| {
17010 //Wrap around the top of the buffer
17011 for _ in 0..2 {
17012 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17013 }
17014 });
17015
17016 cx.assert_editor_state(
17017 &r#"
17018 use some::modified;
17019
17020
17021 fn main() {
17022 ˇ println!("hello there");
17023
17024 println!("around the");
17025 println!("world");
17026 }
17027 "#
17028 .unindent(),
17029 );
17030
17031 cx.update_editor(|editor, window, cx| {
17032 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17033 });
17034
17035 cx.assert_editor_state(
17036 &r#"
17037 use some::modified;
17038
17039 ˇ
17040 fn main() {
17041 println!("hello there");
17042
17043 println!("around the");
17044 println!("world");
17045 }
17046 "#
17047 .unindent(),
17048 );
17049
17050 cx.update_editor(|editor, window, cx| {
17051 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17052 });
17053
17054 cx.assert_editor_state(
17055 &r#"
17056 ˇuse some::modified;
17057
17058
17059 fn main() {
17060 println!("hello there");
17061
17062 println!("around the");
17063 println!("world");
17064 }
17065 "#
17066 .unindent(),
17067 );
17068
17069 cx.update_editor(|editor, window, cx| {
17070 for _ in 0..2 {
17071 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17072 }
17073 });
17074
17075 cx.assert_editor_state(
17076 &r#"
17077 use some::modified;
17078
17079
17080 fn main() {
17081 ˇ println!("hello there");
17082
17083 println!("around the");
17084 println!("world");
17085 }
17086 "#
17087 .unindent(),
17088 );
17089
17090 cx.update_editor(|editor, window, cx| {
17091 editor.fold(&Fold, window, cx);
17092 });
17093
17094 cx.update_editor(|editor, window, cx| {
17095 editor.go_to_next_hunk(&GoToHunk, window, cx);
17096 });
17097
17098 cx.assert_editor_state(
17099 &r#"
17100 ˇuse some::modified;
17101
17102
17103 fn main() {
17104 println!("hello there");
17105
17106 println!("around the");
17107 println!("world");
17108 }
17109 "#
17110 .unindent(),
17111 );
17112}
17113
17114#[test]
17115fn test_split_words() {
17116 fn split(text: &str) -> Vec<&str> {
17117 split_words(text).collect()
17118 }
17119
17120 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17121 assert_eq!(split("hello_world"), &["hello_", "world"]);
17122 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17123 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17124 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17125 assert_eq!(split("helloworld"), &["helloworld"]);
17126
17127 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17128}
17129
17130#[gpui::test]
17131async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17132 init_test(cx, |_| {});
17133
17134 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17135 let mut assert = |before, after| {
17136 let _state_context = cx.set_state(before);
17137 cx.run_until_parked();
17138 cx.update_editor(|editor, window, cx| {
17139 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17140 });
17141 cx.run_until_parked();
17142 cx.assert_editor_state(after);
17143 };
17144
17145 // Outside bracket jumps to outside of matching bracket
17146 assert("console.logˇ(var);", "console.log(var)ˇ;");
17147 assert("console.log(var)ˇ;", "console.logˇ(var);");
17148
17149 // Inside bracket jumps to inside of matching bracket
17150 assert("console.log(ˇvar);", "console.log(varˇ);");
17151 assert("console.log(varˇ);", "console.log(ˇvar);");
17152
17153 // When outside a bracket and inside, favor jumping to the inside bracket
17154 assert(
17155 "console.log('foo', [1, 2, 3]ˇ);",
17156 "console.log(ˇ'foo', [1, 2, 3]);",
17157 );
17158 assert(
17159 "console.log(ˇ'foo', [1, 2, 3]);",
17160 "console.log('foo', [1, 2, 3]ˇ);",
17161 );
17162
17163 // Bias forward if two options are equally likely
17164 assert(
17165 "let result = curried_fun()ˇ();",
17166 "let result = curried_fun()()ˇ;",
17167 );
17168
17169 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17170 assert(
17171 indoc! {"
17172 function test() {
17173 console.log('test')ˇ
17174 }"},
17175 indoc! {"
17176 function test() {
17177 console.logˇ('test')
17178 }"},
17179 );
17180}
17181
17182#[gpui::test]
17183async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17184 init_test(cx, |_| {});
17185
17186 let fs = FakeFs::new(cx.executor());
17187 fs.insert_tree(
17188 path!("/a"),
17189 json!({
17190 "main.rs": "fn main() { let a = 5; }",
17191 "other.rs": "// Test file",
17192 }),
17193 )
17194 .await;
17195 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17196
17197 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17198 language_registry.add(Arc::new(Language::new(
17199 LanguageConfig {
17200 name: "Rust".into(),
17201 matcher: LanguageMatcher {
17202 path_suffixes: vec!["rs".to_string()],
17203 ..Default::default()
17204 },
17205 brackets: BracketPairConfig {
17206 pairs: vec![BracketPair {
17207 start: "{".to_string(),
17208 end: "}".to_string(),
17209 close: true,
17210 surround: true,
17211 newline: true,
17212 }],
17213 disabled_scopes_by_bracket_ix: Vec::new(),
17214 },
17215 ..Default::default()
17216 },
17217 Some(tree_sitter_rust::LANGUAGE.into()),
17218 )));
17219 let mut fake_servers = language_registry.register_fake_lsp(
17220 "Rust",
17221 FakeLspAdapter {
17222 capabilities: lsp::ServerCapabilities {
17223 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17224 first_trigger_character: "{".to_string(),
17225 more_trigger_character: None,
17226 }),
17227 ..Default::default()
17228 },
17229 ..Default::default()
17230 },
17231 );
17232
17233 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17234
17235 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17236
17237 let worktree_id = workspace
17238 .update(cx, |workspace, _, cx| {
17239 workspace.project().update(cx, |project, cx| {
17240 project.worktrees(cx).next().unwrap().read(cx).id()
17241 })
17242 })
17243 .unwrap();
17244
17245 let buffer = project
17246 .update(cx, |project, cx| {
17247 project.open_local_buffer(path!("/a/main.rs"), cx)
17248 })
17249 .await
17250 .unwrap();
17251 let editor_handle = workspace
17252 .update(cx, |workspace, window, cx| {
17253 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17254 })
17255 .unwrap()
17256 .await
17257 .unwrap()
17258 .downcast::<Editor>()
17259 .unwrap();
17260
17261 cx.executor().start_waiting();
17262 let fake_server = fake_servers.next().await.unwrap();
17263
17264 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17265 |params, _| async move {
17266 assert_eq!(
17267 params.text_document_position.text_document.uri,
17268 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17269 );
17270 assert_eq!(
17271 params.text_document_position.position,
17272 lsp::Position::new(0, 21),
17273 );
17274
17275 Ok(Some(vec![lsp::TextEdit {
17276 new_text: "]".to_string(),
17277 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17278 }]))
17279 },
17280 );
17281
17282 editor_handle.update_in(cx, |editor, window, cx| {
17283 window.focus(&editor.focus_handle(cx));
17284 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17285 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17286 });
17287 editor.handle_input("{", window, cx);
17288 });
17289
17290 cx.executor().run_until_parked();
17291
17292 buffer.update(cx, |buffer, _| {
17293 assert_eq!(
17294 buffer.text(),
17295 "fn main() { let a = {5}; }",
17296 "No extra braces from on type formatting should appear in the buffer"
17297 )
17298 });
17299}
17300
17301#[gpui::test(iterations = 20, seeds(31))]
17302async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17303 init_test(cx, |_| {});
17304
17305 let mut cx = EditorLspTestContext::new_rust(
17306 lsp::ServerCapabilities {
17307 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17308 first_trigger_character: ".".to_string(),
17309 more_trigger_character: None,
17310 }),
17311 ..Default::default()
17312 },
17313 cx,
17314 )
17315 .await;
17316
17317 cx.update_buffer(|buffer, _| {
17318 // This causes autoindent to be async.
17319 buffer.set_sync_parse_timeout(Duration::ZERO)
17320 });
17321
17322 cx.set_state("fn c() {\n d()ˇ\n}\n");
17323 cx.simulate_keystroke("\n");
17324 cx.run_until_parked();
17325
17326 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17327 let mut request =
17328 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17329 let buffer_cloned = buffer_cloned.clone();
17330 async move {
17331 buffer_cloned.update(&mut cx, |buffer, _| {
17332 assert_eq!(
17333 buffer.text(),
17334 "fn c() {\n d()\n .\n}\n",
17335 "OnTypeFormatting should triggered after autoindent applied"
17336 )
17337 })?;
17338
17339 Ok(Some(vec![]))
17340 }
17341 });
17342
17343 cx.simulate_keystroke(".");
17344 cx.run_until_parked();
17345
17346 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17347 assert!(request.next().await.is_some());
17348 request.close();
17349 assert!(request.next().await.is_none());
17350}
17351
17352#[gpui::test]
17353async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17354 init_test(cx, |_| {});
17355
17356 let fs = FakeFs::new(cx.executor());
17357 fs.insert_tree(
17358 path!("/a"),
17359 json!({
17360 "main.rs": "fn main() { let a = 5; }",
17361 "other.rs": "// Test file",
17362 }),
17363 )
17364 .await;
17365
17366 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17367
17368 let server_restarts = Arc::new(AtomicUsize::new(0));
17369 let closure_restarts = Arc::clone(&server_restarts);
17370 let language_server_name = "test language server";
17371 let language_name: LanguageName = "Rust".into();
17372
17373 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17374 language_registry.add(Arc::new(Language::new(
17375 LanguageConfig {
17376 name: language_name.clone(),
17377 matcher: LanguageMatcher {
17378 path_suffixes: vec!["rs".to_string()],
17379 ..Default::default()
17380 },
17381 ..Default::default()
17382 },
17383 Some(tree_sitter_rust::LANGUAGE.into()),
17384 )));
17385 let mut fake_servers = language_registry.register_fake_lsp(
17386 "Rust",
17387 FakeLspAdapter {
17388 name: language_server_name,
17389 initialization_options: Some(json!({
17390 "testOptionValue": true
17391 })),
17392 initializer: Some(Box::new(move |fake_server| {
17393 let task_restarts = Arc::clone(&closure_restarts);
17394 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17395 task_restarts.fetch_add(1, atomic::Ordering::Release);
17396 futures::future::ready(Ok(()))
17397 });
17398 })),
17399 ..Default::default()
17400 },
17401 );
17402
17403 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17404 let _buffer = project
17405 .update(cx, |project, cx| {
17406 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17407 })
17408 .await
17409 .unwrap();
17410 let _fake_server = fake_servers.next().await.unwrap();
17411 update_test_language_settings(cx, |language_settings| {
17412 language_settings.languages.0.insert(
17413 language_name.clone().0,
17414 LanguageSettingsContent {
17415 tab_size: NonZeroU32::new(8),
17416 ..Default::default()
17417 },
17418 );
17419 });
17420 cx.executor().run_until_parked();
17421 assert_eq!(
17422 server_restarts.load(atomic::Ordering::Acquire),
17423 0,
17424 "Should not restart LSP server on an unrelated change"
17425 );
17426
17427 update_test_project_settings(cx, |project_settings| {
17428 project_settings.lsp.insert(
17429 "Some other server name".into(),
17430 LspSettings {
17431 binary: None,
17432 settings: None,
17433 initialization_options: Some(json!({
17434 "some other init value": false
17435 })),
17436 enable_lsp_tasks: false,
17437 fetch: None,
17438 },
17439 );
17440 });
17441 cx.executor().run_until_parked();
17442 assert_eq!(
17443 server_restarts.load(atomic::Ordering::Acquire),
17444 0,
17445 "Should not restart LSP server on an unrelated LSP settings change"
17446 );
17447
17448 update_test_project_settings(cx, |project_settings| {
17449 project_settings.lsp.insert(
17450 language_server_name.into(),
17451 LspSettings {
17452 binary: None,
17453 settings: None,
17454 initialization_options: Some(json!({
17455 "anotherInitValue": false
17456 })),
17457 enable_lsp_tasks: false,
17458 fetch: None,
17459 },
17460 );
17461 });
17462 cx.executor().run_until_parked();
17463 assert_eq!(
17464 server_restarts.load(atomic::Ordering::Acquire),
17465 1,
17466 "Should restart LSP server on a related LSP settings change"
17467 );
17468
17469 update_test_project_settings(cx, |project_settings| {
17470 project_settings.lsp.insert(
17471 language_server_name.into(),
17472 LspSettings {
17473 binary: None,
17474 settings: None,
17475 initialization_options: Some(json!({
17476 "anotherInitValue": false
17477 })),
17478 enable_lsp_tasks: false,
17479 fetch: None,
17480 },
17481 );
17482 });
17483 cx.executor().run_until_parked();
17484 assert_eq!(
17485 server_restarts.load(atomic::Ordering::Acquire),
17486 1,
17487 "Should not restart LSP server on a related LSP settings change that is the same"
17488 );
17489
17490 update_test_project_settings(cx, |project_settings| {
17491 project_settings.lsp.insert(
17492 language_server_name.into(),
17493 LspSettings {
17494 binary: None,
17495 settings: None,
17496 initialization_options: None,
17497 enable_lsp_tasks: false,
17498 fetch: None,
17499 },
17500 );
17501 });
17502 cx.executor().run_until_parked();
17503 assert_eq!(
17504 server_restarts.load(atomic::Ordering::Acquire),
17505 2,
17506 "Should restart LSP server on another related LSP settings change"
17507 );
17508}
17509
17510#[gpui::test]
17511async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17512 init_test(cx, |_| {});
17513
17514 let mut cx = EditorLspTestContext::new_rust(
17515 lsp::ServerCapabilities {
17516 completion_provider: Some(lsp::CompletionOptions {
17517 trigger_characters: Some(vec![".".to_string()]),
17518 resolve_provider: Some(true),
17519 ..Default::default()
17520 }),
17521 ..Default::default()
17522 },
17523 cx,
17524 )
17525 .await;
17526
17527 cx.set_state("fn main() { let a = 2ˇ; }");
17528 cx.simulate_keystroke(".");
17529 let completion_item = lsp::CompletionItem {
17530 label: "some".into(),
17531 kind: Some(lsp::CompletionItemKind::SNIPPET),
17532 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17533 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17534 kind: lsp::MarkupKind::Markdown,
17535 value: "```rust\nSome(2)\n```".to_string(),
17536 })),
17537 deprecated: Some(false),
17538 sort_text: Some("fffffff2".to_string()),
17539 filter_text: Some("some".to_string()),
17540 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17541 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17542 range: lsp::Range {
17543 start: lsp::Position {
17544 line: 0,
17545 character: 22,
17546 },
17547 end: lsp::Position {
17548 line: 0,
17549 character: 22,
17550 },
17551 },
17552 new_text: "Some(2)".to_string(),
17553 })),
17554 additional_text_edits: Some(vec![lsp::TextEdit {
17555 range: lsp::Range {
17556 start: lsp::Position {
17557 line: 0,
17558 character: 20,
17559 },
17560 end: lsp::Position {
17561 line: 0,
17562 character: 22,
17563 },
17564 },
17565 new_text: "".to_string(),
17566 }]),
17567 ..Default::default()
17568 };
17569
17570 let closure_completion_item = completion_item.clone();
17571 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17572 let task_completion_item = closure_completion_item.clone();
17573 async move {
17574 Ok(Some(lsp::CompletionResponse::Array(vec![
17575 task_completion_item,
17576 ])))
17577 }
17578 });
17579
17580 request.next().await;
17581
17582 cx.condition(|editor, _| editor.context_menu_visible())
17583 .await;
17584 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17585 editor
17586 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17587 .unwrap()
17588 });
17589 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17590
17591 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17592 let task_completion_item = completion_item.clone();
17593 async move { Ok(task_completion_item) }
17594 })
17595 .next()
17596 .await
17597 .unwrap();
17598 apply_additional_edits.await.unwrap();
17599 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17600}
17601
17602#[gpui::test]
17603async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17604 init_test(cx, |_| {});
17605
17606 let mut cx = EditorLspTestContext::new_rust(
17607 lsp::ServerCapabilities {
17608 completion_provider: Some(lsp::CompletionOptions {
17609 trigger_characters: Some(vec![".".to_string()]),
17610 resolve_provider: Some(true),
17611 ..Default::default()
17612 }),
17613 ..Default::default()
17614 },
17615 cx,
17616 )
17617 .await;
17618
17619 cx.set_state("fn main() { let a = 2ˇ; }");
17620 cx.simulate_keystroke(".");
17621
17622 let item1 = lsp::CompletionItem {
17623 label: "method id()".to_string(),
17624 filter_text: Some("id".to_string()),
17625 detail: None,
17626 documentation: None,
17627 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17628 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17629 new_text: ".id".to_string(),
17630 })),
17631 ..lsp::CompletionItem::default()
17632 };
17633
17634 let item2 = lsp::CompletionItem {
17635 label: "other".to_string(),
17636 filter_text: Some("other".to_string()),
17637 detail: None,
17638 documentation: None,
17639 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17640 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17641 new_text: ".other".to_string(),
17642 })),
17643 ..lsp::CompletionItem::default()
17644 };
17645
17646 let item1 = item1.clone();
17647 cx.set_request_handler::<lsp::request::Completion, _, _>({
17648 let item1 = item1.clone();
17649 move |_, _, _| {
17650 let item1 = item1.clone();
17651 let item2 = item2.clone();
17652 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17653 }
17654 })
17655 .next()
17656 .await;
17657
17658 cx.condition(|editor, _| editor.context_menu_visible())
17659 .await;
17660 cx.update_editor(|editor, _, _| {
17661 let context_menu = editor.context_menu.borrow_mut();
17662 let context_menu = context_menu
17663 .as_ref()
17664 .expect("Should have the context menu deployed");
17665 match context_menu {
17666 CodeContextMenu::Completions(completions_menu) => {
17667 let completions = completions_menu.completions.borrow_mut();
17668 assert_eq!(
17669 completions
17670 .iter()
17671 .map(|completion| &completion.label.text)
17672 .collect::<Vec<_>>(),
17673 vec!["method id()", "other"]
17674 )
17675 }
17676 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17677 }
17678 });
17679
17680 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17681 let item1 = item1.clone();
17682 move |_, item_to_resolve, _| {
17683 let item1 = item1.clone();
17684 async move {
17685 if item1 == item_to_resolve {
17686 Ok(lsp::CompletionItem {
17687 label: "method id()".to_string(),
17688 filter_text: Some("id".to_string()),
17689 detail: Some("Now resolved!".to_string()),
17690 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17691 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17692 range: lsp::Range::new(
17693 lsp::Position::new(0, 22),
17694 lsp::Position::new(0, 22),
17695 ),
17696 new_text: ".id".to_string(),
17697 })),
17698 ..lsp::CompletionItem::default()
17699 })
17700 } else {
17701 Ok(item_to_resolve)
17702 }
17703 }
17704 }
17705 })
17706 .next()
17707 .await
17708 .unwrap();
17709 cx.run_until_parked();
17710
17711 cx.update_editor(|editor, window, cx| {
17712 editor.context_menu_next(&Default::default(), window, cx);
17713 });
17714
17715 cx.update_editor(|editor, _, _| {
17716 let context_menu = editor.context_menu.borrow_mut();
17717 let context_menu = context_menu
17718 .as_ref()
17719 .expect("Should have the context menu deployed");
17720 match context_menu {
17721 CodeContextMenu::Completions(completions_menu) => {
17722 let completions = completions_menu.completions.borrow_mut();
17723 assert_eq!(
17724 completions
17725 .iter()
17726 .map(|completion| &completion.label.text)
17727 .collect::<Vec<_>>(),
17728 vec!["method id() Now resolved!", "other"],
17729 "Should update first completion label, but not second as the filter text did not match."
17730 );
17731 }
17732 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17733 }
17734 });
17735}
17736
17737#[gpui::test]
17738async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17739 init_test(cx, |_| {});
17740 let mut cx = EditorLspTestContext::new_rust(
17741 lsp::ServerCapabilities {
17742 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17743 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17744 completion_provider: Some(lsp::CompletionOptions {
17745 resolve_provider: Some(true),
17746 ..Default::default()
17747 }),
17748 ..Default::default()
17749 },
17750 cx,
17751 )
17752 .await;
17753 cx.set_state(indoc! {"
17754 struct TestStruct {
17755 field: i32
17756 }
17757
17758 fn mainˇ() {
17759 let unused_var = 42;
17760 let test_struct = TestStruct { field: 42 };
17761 }
17762 "});
17763 let symbol_range = cx.lsp_range(indoc! {"
17764 struct TestStruct {
17765 field: i32
17766 }
17767
17768 «fn main»() {
17769 let unused_var = 42;
17770 let test_struct = TestStruct { field: 42 };
17771 }
17772 "});
17773 let mut hover_requests =
17774 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17775 Ok(Some(lsp::Hover {
17776 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17777 kind: lsp::MarkupKind::Markdown,
17778 value: "Function documentation".to_string(),
17779 }),
17780 range: Some(symbol_range),
17781 }))
17782 });
17783
17784 // Case 1: Test that code action menu hide hover popover
17785 cx.dispatch_action(Hover);
17786 hover_requests.next().await;
17787 cx.condition(|editor, _| editor.hover_state.visible()).await;
17788 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17789 move |_, _, _| async move {
17790 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17791 lsp::CodeAction {
17792 title: "Remove unused variable".to_string(),
17793 kind: Some(CodeActionKind::QUICKFIX),
17794 edit: Some(lsp::WorkspaceEdit {
17795 changes: Some(
17796 [(
17797 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17798 vec![lsp::TextEdit {
17799 range: lsp::Range::new(
17800 lsp::Position::new(5, 4),
17801 lsp::Position::new(5, 27),
17802 ),
17803 new_text: "".to_string(),
17804 }],
17805 )]
17806 .into_iter()
17807 .collect(),
17808 ),
17809 ..Default::default()
17810 }),
17811 ..Default::default()
17812 },
17813 )]))
17814 },
17815 );
17816 cx.update_editor(|editor, window, cx| {
17817 editor.toggle_code_actions(
17818 &ToggleCodeActions {
17819 deployed_from: None,
17820 quick_launch: false,
17821 },
17822 window,
17823 cx,
17824 );
17825 });
17826 code_action_requests.next().await;
17827 cx.run_until_parked();
17828 cx.condition(|editor, _| editor.context_menu_visible())
17829 .await;
17830 cx.update_editor(|editor, _, _| {
17831 assert!(
17832 !editor.hover_state.visible(),
17833 "Hover popover should be hidden when code action menu is shown"
17834 );
17835 // Hide code actions
17836 editor.context_menu.take();
17837 });
17838
17839 // Case 2: Test that code completions hide hover popover
17840 cx.dispatch_action(Hover);
17841 hover_requests.next().await;
17842 cx.condition(|editor, _| editor.hover_state.visible()).await;
17843 let counter = Arc::new(AtomicUsize::new(0));
17844 let mut completion_requests =
17845 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17846 let counter = counter.clone();
17847 async move {
17848 counter.fetch_add(1, atomic::Ordering::Release);
17849 Ok(Some(lsp::CompletionResponse::Array(vec![
17850 lsp::CompletionItem {
17851 label: "main".into(),
17852 kind: Some(lsp::CompletionItemKind::FUNCTION),
17853 detail: Some("() -> ()".to_string()),
17854 ..Default::default()
17855 },
17856 lsp::CompletionItem {
17857 label: "TestStruct".into(),
17858 kind: Some(lsp::CompletionItemKind::STRUCT),
17859 detail: Some("struct TestStruct".to_string()),
17860 ..Default::default()
17861 },
17862 ])))
17863 }
17864 });
17865 cx.update_editor(|editor, window, cx| {
17866 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17867 });
17868 completion_requests.next().await;
17869 cx.condition(|editor, _| editor.context_menu_visible())
17870 .await;
17871 cx.update_editor(|editor, _, _| {
17872 assert!(
17873 !editor.hover_state.visible(),
17874 "Hover popover should be hidden when completion menu is shown"
17875 );
17876 });
17877}
17878
17879#[gpui::test]
17880async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17881 init_test(cx, |_| {});
17882
17883 let mut cx = EditorLspTestContext::new_rust(
17884 lsp::ServerCapabilities {
17885 completion_provider: Some(lsp::CompletionOptions {
17886 trigger_characters: Some(vec![".".to_string()]),
17887 resolve_provider: Some(true),
17888 ..Default::default()
17889 }),
17890 ..Default::default()
17891 },
17892 cx,
17893 )
17894 .await;
17895
17896 cx.set_state("fn main() { let a = 2ˇ; }");
17897 cx.simulate_keystroke(".");
17898
17899 let unresolved_item_1 = lsp::CompletionItem {
17900 label: "id".to_string(),
17901 filter_text: Some("id".to_string()),
17902 detail: None,
17903 documentation: None,
17904 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17905 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17906 new_text: ".id".to_string(),
17907 })),
17908 ..lsp::CompletionItem::default()
17909 };
17910 let resolved_item_1 = lsp::CompletionItem {
17911 additional_text_edits: Some(vec![lsp::TextEdit {
17912 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17913 new_text: "!!".to_string(),
17914 }]),
17915 ..unresolved_item_1.clone()
17916 };
17917 let unresolved_item_2 = lsp::CompletionItem {
17918 label: "other".to_string(),
17919 filter_text: Some("other".to_string()),
17920 detail: None,
17921 documentation: None,
17922 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17923 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17924 new_text: ".other".to_string(),
17925 })),
17926 ..lsp::CompletionItem::default()
17927 };
17928 let resolved_item_2 = lsp::CompletionItem {
17929 additional_text_edits: Some(vec![lsp::TextEdit {
17930 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17931 new_text: "??".to_string(),
17932 }]),
17933 ..unresolved_item_2.clone()
17934 };
17935
17936 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17937 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17938 cx.lsp
17939 .server
17940 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17941 let unresolved_item_1 = unresolved_item_1.clone();
17942 let resolved_item_1 = resolved_item_1.clone();
17943 let unresolved_item_2 = unresolved_item_2.clone();
17944 let resolved_item_2 = resolved_item_2.clone();
17945 let resolve_requests_1 = resolve_requests_1.clone();
17946 let resolve_requests_2 = resolve_requests_2.clone();
17947 move |unresolved_request, _| {
17948 let unresolved_item_1 = unresolved_item_1.clone();
17949 let resolved_item_1 = resolved_item_1.clone();
17950 let unresolved_item_2 = unresolved_item_2.clone();
17951 let resolved_item_2 = resolved_item_2.clone();
17952 let resolve_requests_1 = resolve_requests_1.clone();
17953 let resolve_requests_2 = resolve_requests_2.clone();
17954 async move {
17955 if unresolved_request == unresolved_item_1 {
17956 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17957 Ok(resolved_item_1.clone())
17958 } else if unresolved_request == unresolved_item_2 {
17959 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17960 Ok(resolved_item_2.clone())
17961 } else {
17962 panic!("Unexpected completion item {unresolved_request:?}")
17963 }
17964 }
17965 }
17966 })
17967 .detach();
17968
17969 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17970 let unresolved_item_1 = unresolved_item_1.clone();
17971 let unresolved_item_2 = unresolved_item_2.clone();
17972 async move {
17973 Ok(Some(lsp::CompletionResponse::Array(vec![
17974 unresolved_item_1,
17975 unresolved_item_2,
17976 ])))
17977 }
17978 })
17979 .next()
17980 .await;
17981
17982 cx.condition(|editor, _| editor.context_menu_visible())
17983 .await;
17984 cx.update_editor(|editor, _, _| {
17985 let context_menu = editor.context_menu.borrow_mut();
17986 let context_menu = context_menu
17987 .as_ref()
17988 .expect("Should have the context menu deployed");
17989 match context_menu {
17990 CodeContextMenu::Completions(completions_menu) => {
17991 let completions = completions_menu.completions.borrow_mut();
17992 assert_eq!(
17993 completions
17994 .iter()
17995 .map(|completion| &completion.label.text)
17996 .collect::<Vec<_>>(),
17997 vec!["id", "other"]
17998 )
17999 }
18000 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18001 }
18002 });
18003 cx.run_until_parked();
18004
18005 cx.update_editor(|editor, window, cx| {
18006 editor.context_menu_next(&ContextMenuNext, window, cx);
18007 });
18008 cx.run_until_parked();
18009 cx.update_editor(|editor, window, cx| {
18010 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18011 });
18012 cx.run_until_parked();
18013 cx.update_editor(|editor, window, cx| {
18014 editor.context_menu_next(&ContextMenuNext, window, cx);
18015 });
18016 cx.run_until_parked();
18017 cx.update_editor(|editor, window, cx| {
18018 editor
18019 .compose_completion(&ComposeCompletion::default(), window, cx)
18020 .expect("No task returned")
18021 })
18022 .await
18023 .expect("Completion failed");
18024 cx.run_until_parked();
18025
18026 cx.update_editor(|editor, _, cx| {
18027 assert_eq!(
18028 resolve_requests_1.load(atomic::Ordering::Acquire),
18029 1,
18030 "Should always resolve once despite multiple selections"
18031 );
18032 assert_eq!(
18033 resolve_requests_2.load(atomic::Ordering::Acquire),
18034 1,
18035 "Should always resolve once after multiple selections and applying the completion"
18036 );
18037 assert_eq!(
18038 editor.text(cx),
18039 "fn main() { let a = ??.other; }",
18040 "Should use resolved data when applying the completion"
18041 );
18042 });
18043}
18044
18045#[gpui::test]
18046async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18047 init_test(cx, |_| {});
18048
18049 let item_0 = lsp::CompletionItem {
18050 label: "abs".into(),
18051 insert_text: Some("abs".into()),
18052 data: Some(json!({ "very": "special"})),
18053 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18054 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18055 lsp::InsertReplaceEdit {
18056 new_text: "abs".to_string(),
18057 insert: lsp::Range::default(),
18058 replace: lsp::Range::default(),
18059 },
18060 )),
18061 ..lsp::CompletionItem::default()
18062 };
18063 let items = iter::once(item_0.clone())
18064 .chain((11..51).map(|i| lsp::CompletionItem {
18065 label: format!("item_{}", i),
18066 insert_text: Some(format!("item_{}", i)),
18067 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18068 ..lsp::CompletionItem::default()
18069 }))
18070 .collect::<Vec<_>>();
18071
18072 let default_commit_characters = vec!["?".to_string()];
18073 let default_data = json!({ "default": "data"});
18074 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18075 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18076 let default_edit_range = lsp::Range {
18077 start: lsp::Position {
18078 line: 0,
18079 character: 5,
18080 },
18081 end: lsp::Position {
18082 line: 0,
18083 character: 5,
18084 },
18085 };
18086
18087 let mut cx = EditorLspTestContext::new_rust(
18088 lsp::ServerCapabilities {
18089 completion_provider: Some(lsp::CompletionOptions {
18090 trigger_characters: Some(vec![".".to_string()]),
18091 resolve_provider: Some(true),
18092 ..Default::default()
18093 }),
18094 ..Default::default()
18095 },
18096 cx,
18097 )
18098 .await;
18099
18100 cx.set_state("fn main() { let a = 2ˇ; }");
18101 cx.simulate_keystroke(".");
18102
18103 let completion_data = default_data.clone();
18104 let completion_characters = default_commit_characters.clone();
18105 let completion_items = items.clone();
18106 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18107 let default_data = completion_data.clone();
18108 let default_commit_characters = completion_characters.clone();
18109 let items = completion_items.clone();
18110 async move {
18111 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18112 items,
18113 item_defaults: Some(lsp::CompletionListItemDefaults {
18114 data: Some(default_data.clone()),
18115 commit_characters: Some(default_commit_characters.clone()),
18116 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18117 default_edit_range,
18118 )),
18119 insert_text_format: Some(default_insert_text_format),
18120 insert_text_mode: Some(default_insert_text_mode),
18121 }),
18122 ..lsp::CompletionList::default()
18123 })))
18124 }
18125 })
18126 .next()
18127 .await;
18128
18129 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18130 cx.lsp
18131 .server
18132 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18133 let closure_resolved_items = resolved_items.clone();
18134 move |item_to_resolve, _| {
18135 let closure_resolved_items = closure_resolved_items.clone();
18136 async move {
18137 closure_resolved_items.lock().push(item_to_resolve.clone());
18138 Ok(item_to_resolve)
18139 }
18140 }
18141 })
18142 .detach();
18143
18144 cx.condition(|editor, _| editor.context_menu_visible())
18145 .await;
18146 cx.run_until_parked();
18147 cx.update_editor(|editor, _, _| {
18148 let menu = editor.context_menu.borrow_mut();
18149 match menu.as_ref().expect("should have the completions menu") {
18150 CodeContextMenu::Completions(completions_menu) => {
18151 assert_eq!(
18152 completions_menu
18153 .entries
18154 .borrow()
18155 .iter()
18156 .map(|mat| mat.string.clone())
18157 .collect::<Vec<String>>(),
18158 items
18159 .iter()
18160 .map(|completion| completion.label.clone())
18161 .collect::<Vec<String>>()
18162 );
18163 }
18164 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18165 }
18166 });
18167 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18168 // with 4 from the end.
18169 assert_eq!(
18170 *resolved_items.lock(),
18171 [&items[0..16], &items[items.len() - 4..items.len()]]
18172 .concat()
18173 .iter()
18174 .cloned()
18175 .map(|mut item| {
18176 if item.data.is_none() {
18177 item.data = Some(default_data.clone());
18178 }
18179 item
18180 })
18181 .collect::<Vec<lsp::CompletionItem>>(),
18182 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18183 );
18184 resolved_items.lock().clear();
18185
18186 cx.update_editor(|editor, window, cx| {
18187 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18188 });
18189 cx.run_until_parked();
18190 // Completions that have already been resolved are skipped.
18191 assert_eq!(
18192 *resolved_items.lock(),
18193 items[items.len() - 17..items.len() - 4]
18194 .iter()
18195 .cloned()
18196 .map(|mut item| {
18197 if item.data.is_none() {
18198 item.data = Some(default_data.clone());
18199 }
18200 item
18201 })
18202 .collect::<Vec<lsp::CompletionItem>>()
18203 );
18204 resolved_items.lock().clear();
18205}
18206
18207#[gpui::test]
18208async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18209 init_test(cx, |_| {});
18210
18211 let mut cx = EditorLspTestContext::new(
18212 Language::new(
18213 LanguageConfig {
18214 matcher: LanguageMatcher {
18215 path_suffixes: vec!["jsx".into()],
18216 ..Default::default()
18217 },
18218 overrides: [(
18219 "element".into(),
18220 LanguageConfigOverride {
18221 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18222 ..Default::default()
18223 },
18224 )]
18225 .into_iter()
18226 .collect(),
18227 ..Default::default()
18228 },
18229 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18230 )
18231 .with_override_query("(jsx_self_closing_element) @element")
18232 .unwrap(),
18233 lsp::ServerCapabilities {
18234 completion_provider: Some(lsp::CompletionOptions {
18235 trigger_characters: Some(vec![":".to_string()]),
18236 ..Default::default()
18237 }),
18238 ..Default::default()
18239 },
18240 cx,
18241 )
18242 .await;
18243
18244 cx.lsp
18245 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18246 Ok(Some(lsp::CompletionResponse::Array(vec![
18247 lsp::CompletionItem {
18248 label: "bg-blue".into(),
18249 ..Default::default()
18250 },
18251 lsp::CompletionItem {
18252 label: "bg-red".into(),
18253 ..Default::default()
18254 },
18255 lsp::CompletionItem {
18256 label: "bg-yellow".into(),
18257 ..Default::default()
18258 },
18259 ])))
18260 });
18261
18262 cx.set_state(r#"<p class="bgˇ" />"#);
18263
18264 // Trigger completion when typing a dash, because the dash is an extra
18265 // word character in the 'element' scope, which contains the cursor.
18266 cx.simulate_keystroke("-");
18267 cx.executor().run_until_parked();
18268 cx.update_editor(|editor, _, _| {
18269 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18270 {
18271 assert_eq!(
18272 completion_menu_entries(menu),
18273 &["bg-blue", "bg-red", "bg-yellow"]
18274 );
18275 } else {
18276 panic!("expected completion menu to be open");
18277 }
18278 });
18279
18280 cx.simulate_keystroke("l");
18281 cx.executor().run_until_parked();
18282 cx.update_editor(|editor, _, _| {
18283 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18284 {
18285 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18286 } else {
18287 panic!("expected completion menu to be open");
18288 }
18289 });
18290
18291 // When filtering completions, consider the character after the '-' to
18292 // be the start of a subword.
18293 cx.set_state(r#"<p class="yelˇ" />"#);
18294 cx.simulate_keystroke("l");
18295 cx.executor().run_until_parked();
18296 cx.update_editor(|editor, _, _| {
18297 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18298 {
18299 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18300 } else {
18301 panic!("expected completion menu to be open");
18302 }
18303 });
18304}
18305
18306fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18307 let entries = menu.entries.borrow();
18308 entries.iter().map(|mat| mat.string.clone()).collect()
18309}
18310
18311#[gpui::test]
18312async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18313 init_test(cx, |settings| {
18314 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18315 });
18316
18317 let fs = FakeFs::new(cx.executor());
18318 fs.insert_file(path!("/file.ts"), Default::default()).await;
18319
18320 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18321 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18322
18323 language_registry.add(Arc::new(Language::new(
18324 LanguageConfig {
18325 name: "TypeScript".into(),
18326 matcher: LanguageMatcher {
18327 path_suffixes: vec!["ts".to_string()],
18328 ..Default::default()
18329 },
18330 ..Default::default()
18331 },
18332 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18333 )));
18334 update_test_language_settings(cx, |settings| {
18335 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18336 });
18337
18338 let test_plugin = "test_plugin";
18339 let _ = language_registry.register_fake_lsp(
18340 "TypeScript",
18341 FakeLspAdapter {
18342 prettier_plugins: vec![test_plugin],
18343 ..Default::default()
18344 },
18345 );
18346
18347 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18348 let buffer = project
18349 .update(cx, |project, cx| {
18350 project.open_local_buffer(path!("/file.ts"), cx)
18351 })
18352 .await
18353 .unwrap();
18354
18355 let buffer_text = "one\ntwo\nthree\n";
18356 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18357 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18358 editor.update_in(cx, |editor, window, cx| {
18359 editor.set_text(buffer_text, window, cx)
18360 });
18361
18362 editor
18363 .update_in(cx, |editor, window, cx| {
18364 editor.perform_format(
18365 project.clone(),
18366 FormatTrigger::Manual,
18367 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18368 window,
18369 cx,
18370 )
18371 })
18372 .unwrap()
18373 .await;
18374 assert_eq!(
18375 editor.update(cx, |editor, cx| editor.text(cx)),
18376 buffer_text.to_string() + prettier_format_suffix,
18377 "Test prettier formatting was not applied to the original buffer text",
18378 );
18379
18380 update_test_language_settings(cx, |settings| {
18381 settings.defaults.formatter = Some(FormatterList::default())
18382 });
18383 let format = editor.update_in(cx, |editor, window, cx| {
18384 editor.perform_format(
18385 project.clone(),
18386 FormatTrigger::Manual,
18387 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18388 window,
18389 cx,
18390 )
18391 });
18392 format.await.unwrap();
18393 assert_eq!(
18394 editor.update(cx, |editor, cx| editor.text(cx)),
18395 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18396 "Autoformatting (via test prettier) was not applied to the original buffer text",
18397 );
18398}
18399
18400#[gpui::test]
18401async fn test_addition_reverts(cx: &mut TestAppContext) {
18402 init_test(cx, |_| {});
18403 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18404 let base_text = indoc! {r#"
18405 struct Row;
18406 struct Row1;
18407 struct Row2;
18408
18409 struct Row4;
18410 struct Row5;
18411 struct Row6;
18412
18413 struct Row8;
18414 struct Row9;
18415 struct Row10;"#};
18416
18417 // When addition hunks are not adjacent to carets, no hunk revert is performed
18418 assert_hunk_revert(
18419 indoc! {r#"struct Row;
18420 struct Row1;
18421 struct Row1.1;
18422 struct Row1.2;
18423 struct Row2;ˇ
18424
18425 struct Row4;
18426 struct Row5;
18427 struct Row6;
18428
18429 struct Row8;
18430 ˇstruct Row9;
18431 struct Row9.1;
18432 struct Row9.2;
18433 struct Row9.3;
18434 struct Row10;"#},
18435 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18436 indoc! {r#"struct Row;
18437 struct Row1;
18438 struct Row1.1;
18439 struct Row1.2;
18440 struct Row2;ˇ
18441
18442 struct Row4;
18443 struct Row5;
18444 struct Row6;
18445
18446 struct Row8;
18447 ˇstruct Row9;
18448 struct Row9.1;
18449 struct Row9.2;
18450 struct Row9.3;
18451 struct Row10;"#},
18452 base_text,
18453 &mut cx,
18454 );
18455 // Same for selections
18456 assert_hunk_revert(
18457 indoc! {r#"struct Row;
18458 struct Row1;
18459 struct Row2;
18460 struct Row2.1;
18461 struct Row2.2;
18462 «ˇ
18463 struct Row4;
18464 struct» Row5;
18465 «struct Row6;
18466 ˇ»
18467 struct Row9.1;
18468 struct Row9.2;
18469 struct Row9.3;
18470 struct Row8;
18471 struct Row9;
18472 struct Row10;"#},
18473 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18474 indoc! {r#"struct Row;
18475 struct Row1;
18476 struct Row2;
18477 struct Row2.1;
18478 struct Row2.2;
18479 «ˇ
18480 struct Row4;
18481 struct» Row5;
18482 «struct Row6;
18483 ˇ»
18484 struct Row9.1;
18485 struct Row9.2;
18486 struct Row9.3;
18487 struct Row8;
18488 struct Row9;
18489 struct Row10;"#},
18490 base_text,
18491 &mut cx,
18492 );
18493
18494 // When carets and selections intersect the addition hunks, those are reverted.
18495 // Adjacent carets got merged.
18496 assert_hunk_revert(
18497 indoc! {r#"struct Row;
18498 ˇ// something on the top
18499 struct Row1;
18500 struct Row2;
18501 struct Roˇw3.1;
18502 struct Row2.2;
18503 struct Row2.3;ˇ
18504
18505 struct Row4;
18506 struct ˇRow5.1;
18507 struct Row5.2;
18508 struct «Rowˇ»5.3;
18509 struct Row5;
18510 struct Row6;
18511 ˇ
18512 struct Row9.1;
18513 struct «Rowˇ»9.2;
18514 struct «ˇRow»9.3;
18515 struct Row8;
18516 struct Row9;
18517 «ˇ// something on bottom»
18518 struct Row10;"#},
18519 vec![
18520 DiffHunkStatusKind::Added,
18521 DiffHunkStatusKind::Added,
18522 DiffHunkStatusKind::Added,
18523 DiffHunkStatusKind::Added,
18524 DiffHunkStatusKind::Added,
18525 ],
18526 indoc! {r#"struct Row;
18527 ˇstruct Row1;
18528 struct Row2;
18529 ˇ
18530 struct Row4;
18531 ˇstruct Row5;
18532 struct Row6;
18533 ˇ
18534 ˇstruct Row8;
18535 struct Row9;
18536 ˇstruct Row10;"#},
18537 base_text,
18538 &mut cx,
18539 );
18540}
18541
18542#[gpui::test]
18543async fn test_modification_reverts(cx: &mut TestAppContext) {
18544 init_test(cx, |_| {});
18545 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18546 let base_text = indoc! {r#"
18547 struct Row;
18548 struct Row1;
18549 struct Row2;
18550
18551 struct Row4;
18552 struct Row5;
18553 struct Row6;
18554
18555 struct Row8;
18556 struct Row9;
18557 struct Row10;"#};
18558
18559 // Modification hunks behave the same as the addition ones.
18560 assert_hunk_revert(
18561 indoc! {r#"struct Row;
18562 struct Row1;
18563 struct Row33;
18564 ˇ
18565 struct Row4;
18566 struct Row5;
18567 struct Row6;
18568 ˇ
18569 struct Row99;
18570 struct Row9;
18571 struct Row10;"#},
18572 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18573 indoc! {r#"struct Row;
18574 struct Row1;
18575 struct Row33;
18576 ˇ
18577 struct Row4;
18578 struct Row5;
18579 struct Row6;
18580 ˇ
18581 struct Row99;
18582 struct Row9;
18583 struct Row10;"#},
18584 base_text,
18585 &mut cx,
18586 );
18587 assert_hunk_revert(
18588 indoc! {r#"struct Row;
18589 struct Row1;
18590 struct Row33;
18591 «ˇ
18592 struct Row4;
18593 struct» Row5;
18594 «struct Row6;
18595 ˇ»
18596 struct Row99;
18597 struct Row9;
18598 struct Row10;"#},
18599 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18600 indoc! {r#"struct Row;
18601 struct Row1;
18602 struct Row33;
18603 «ˇ
18604 struct Row4;
18605 struct» Row5;
18606 «struct Row6;
18607 ˇ»
18608 struct Row99;
18609 struct Row9;
18610 struct Row10;"#},
18611 base_text,
18612 &mut cx,
18613 );
18614
18615 assert_hunk_revert(
18616 indoc! {r#"ˇstruct Row1.1;
18617 struct Row1;
18618 «ˇstr»uct Row22;
18619
18620 struct ˇRow44;
18621 struct Row5;
18622 struct «Rˇ»ow66;ˇ
18623
18624 «struˇ»ct Row88;
18625 struct Row9;
18626 struct Row1011;ˇ"#},
18627 vec![
18628 DiffHunkStatusKind::Modified,
18629 DiffHunkStatusKind::Modified,
18630 DiffHunkStatusKind::Modified,
18631 DiffHunkStatusKind::Modified,
18632 DiffHunkStatusKind::Modified,
18633 DiffHunkStatusKind::Modified,
18634 ],
18635 indoc! {r#"struct Row;
18636 ˇstruct Row1;
18637 struct Row2;
18638 ˇ
18639 struct Row4;
18640 ˇstruct Row5;
18641 struct Row6;
18642 ˇ
18643 struct Row8;
18644 ˇstruct Row9;
18645 struct Row10;ˇ"#},
18646 base_text,
18647 &mut cx,
18648 );
18649}
18650
18651#[gpui::test]
18652async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18653 init_test(cx, |_| {});
18654 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18655 let base_text = indoc! {r#"
18656 one
18657
18658 two
18659 three
18660 "#};
18661
18662 cx.set_head_text(base_text);
18663 cx.set_state("\nˇ\n");
18664 cx.executor().run_until_parked();
18665 cx.update_editor(|editor, _window, cx| {
18666 editor.expand_selected_diff_hunks(cx);
18667 });
18668 cx.executor().run_until_parked();
18669 cx.update_editor(|editor, window, cx| {
18670 editor.backspace(&Default::default(), window, cx);
18671 });
18672 cx.run_until_parked();
18673 cx.assert_state_with_diff(
18674 indoc! {r#"
18675
18676 - two
18677 - threeˇ
18678 +
18679 "#}
18680 .to_string(),
18681 );
18682}
18683
18684#[gpui::test]
18685async fn test_deletion_reverts(cx: &mut TestAppContext) {
18686 init_test(cx, |_| {});
18687 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18688 let base_text = indoc! {r#"struct Row;
18689struct Row1;
18690struct Row2;
18691
18692struct Row4;
18693struct Row5;
18694struct Row6;
18695
18696struct Row8;
18697struct Row9;
18698struct Row10;"#};
18699
18700 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18701 assert_hunk_revert(
18702 indoc! {r#"struct Row;
18703 struct Row2;
18704
18705 ˇstruct Row4;
18706 struct Row5;
18707 struct Row6;
18708 ˇ
18709 struct Row8;
18710 struct Row10;"#},
18711 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18712 indoc! {r#"struct Row;
18713 struct Row2;
18714
18715 ˇstruct Row4;
18716 struct Row5;
18717 struct Row6;
18718 ˇ
18719 struct Row8;
18720 struct Row10;"#},
18721 base_text,
18722 &mut cx,
18723 );
18724 assert_hunk_revert(
18725 indoc! {r#"struct Row;
18726 struct Row2;
18727
18728 «ˇstruct Row4;
18729 struct» Row5;
18730 «struct Row6;
18731 ˇ»
18732 struct Row8;
18733 struct Row10;"#},
18734 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18735 indoc! {r#"struct Row;
18736 struct Row2;
18737
18738 «ˇstruct Row4;
18739 struct» Row5;
18740 «struct Row6;
18741 ˇ»
18742 struct Row8;
18743 struct Row10;"#},
18744 base_text,
18745 &mut cx,
18746 );
18747
18748 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18749 assert_hunk_revert(
18750 indoc! {r#"struct Row;
18751 ˇstruct Row2;
18752
18753 struct Row4;
18754 struct Row5;
18755 struct Row6;
18756
18757 struct Row8;ˇ
18758 struct Row10;"#},
18759 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18760 indoc! {r#"struct Row;
18761 struct Row1;
18762 ˇstruct Row2;
18763
18764 struct Row4;
18765 struct Row5;
18766 struct Row6;
18767
18768 struct Row8;ˇ
18769 struct Row9;
18770 struct Row10;"#},
18771 base_text,
18772 &mut cx,
18773 );
18774 assert_hunk_revert(
18775 indoc! {r#"struct Row;
18776 struct Row2«ˇ;
18777 struct Row4;
18778 struct» Row5;
18779 «struct Row6;
18780
18781 struct Row8;ˇ»
18782 struct Row10;"#},
18783 vec![
18784 DiffHunkStatusKind::Deleted,
18785 DiffHunkStatusKind::Deleted,
18786 DiffHunkStatusKind::Deleted,
18787 ],
18788 indoc! {r#"struct Row;
18789 struct Row1;
18790 struct Row2«ˇ;
18791
18792 struct Row4;
18793 struct» Row5;
18794 «struct Row6;
18795
18796 struct Row8;ˇ»
18797 struct Row9;
18798 struct Row10;"#},
18799 base_text,
18800 &mut cx,
18801 );
18802}
18803
18804#[gpui::test]
18805async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18806 init_test(cx, |_| {});
18807
18808 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18809 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18810 let base_text_3 =
18811 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18812
18813 let text_1 = edit_first_char_of_every_line(base_text_1);
18814 let text_2 = edit_first_char_of_every_line(base_text_2);
18815 let text_3 = edit_first_char_of_every_line(base_text_3);
18816
18817 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18818 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18819 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18820
18821 let multibuffer = cx.new(|cx| {
18822 let mut multibuffer = MultiBuffer::new(ReadWrite);
18823 multibuffer.push_excerpts(
18824 buffer_1.clone(),
18825 [
18826 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18827 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18828 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18829 ],
18830 cx,
18831 );
18832 multibuffer.push_excerpts(
18833 buffer_2.clone(),
18834 [
18835 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18836 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18837 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18838 ],
18839 cx,
18840 );
18841 multibuffer.push_excerpts(
18842 buffer_3.clone(),
18843 [
18844 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18845 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18846 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18847 ],
18848 cx,
18849 );
18850 multibuffer
18851 });
18852
18853 let fs = FakeFs::new(cx.executor());
18854 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18855 let (editor, cx) = cx
18856 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18857 editor.update_in(cx, |editor, _window, cx| {
18858 for (buffer, diff_base) in [
18859 (buffer_1.clone(), base_text_1),
18860 (buffer_2.clone(), base_text_2),
18861 (buffer_3.clone(), base_text_3),
18862 ] {
18863 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18864 editor
18865 .buffer
18866 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18867 }
18868 });
18869 cx.executor().run_until_parked();
18870
18871 editor.update_in(cx, |editor, window, cx| {
18872 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}");
18873 editor.select_all(&SelectAll, window, cx);
18874 editor.git_restore(&Default::default(), window, cx);
18875 });
18876 cx.executor().run_until_parked();
18877
18878 // When all ranges are selected, all buffer hunks are reverted.
18879 editor.update(cx, |editor, cx| {
18880 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");
18881 });
18882 buffer_1.update(cx, |buffer, _| {
18883 assert_eq!(buffer.text(), base_text_1);
18884 });
18885 buffer_2.update(cx, |buffer, _| {
18886 assert_eq!(buffer.text(), base_text_2);
18887 });
18888 buffer_3.update(cx, |buffer, _| {
18889 assert_eq!(buffer.text(), base_text_3);
18890 });
18891
18892 editor.update_in(cx, |editor, window, cx| {
18893 editor.undo(&Default::default(), window, cx);
18894 });
18895
18896 editor.update_in(cx, |editor, window, cx| {
18897 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18898 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18899 });
18900 editor.git_restore(&Default::default(), window, cx);
18901 });
18902
18903 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18904 // but not affect buffer_2 and its related excerpts.
18905 editor.update(cx, |editor, cx| {
18906 assert_eq!(
18907 editor.text(cx),
18908 "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}"
18909 );
18910 });
18911 buffer_1.update(cx, |buffer, _| {
18912 assert_eq!(buffer.text(), base_text_1);
18913 });
18914 buffer_2.update(cx, |buffer, _| {
18915 assert_eq!(
18916 buffer.text(),
18917 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18918 );
18919 });
18920 buffer_3.update(cx, |buffer, _| {
18921 assert_eq!(
18922 buffer.text(),
18923 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18924 );
18925 });
18926
18927 fn edit_first_char_of_every_line(text: &str) -> String {
18928 text.split('\n')
18929 .map(|line| format!("X{}", &line[1..]))
18930 .collect::<Vec<_>>()
18931 .join("\n")
18932 }
18933}
18934
18935#[gpui::test]
18936async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18937 init_test(cx, |_| {});
18938
18939 let cols = 4;
18940 let rows = 10;
18941 let sample_text_1 = sample_text(rows, cols, 'a');
18942 assert_eq!(
18943 sample_text_1,
18944 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18945 );
18946 let sample_text_2 = sample_text(rows, cols, 'l');
18947 assert_eq!(
18948 sample_text_2,
18949 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18950 );
18951 let sample_text_3 = sample_text(rows, cols, 'v');
18952 assert_eq!(
18953 sample_text_3,
18954 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18955 );
18956
18957 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18958 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18959 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18960
18961 let multi_buffer = cx.new(|cx| {
18962 let mut multibuffer = MultiBuffer::new(ReadWrite);
18963 multibuffer.push_excerpts(
18964 buffer_1.clone(),
18965 [
18966 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18967 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18968 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18969 ],
18970 cx,
18971 );
18972 multibuffer.push_excerpts(
18973 buffer_2.clone(),
18974 [
18975 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18976 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18977 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18978 ],
18979 cx,
18980 );
18981 multibuffer.push_excerpts(
18982 buffer_3.clone(),
18983 [
18984 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18985 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18986 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18987 ],
18988 cx,
18989 );
18990 multibuffer
18991 });
18992
18993 let fs = FakeFs::new(cx.executor());
18994 fs.insert_tree(
18995 "/a",
18996 json!({
18997 "main.rs": sample_text_1,
18998 "other.rs": sample_text_2,
18999 "lib.rs": sample_text_3,
19000 }),
19001 )
19002 .await;
19003 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19004 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19005 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19006 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19007 Editor::new(
19008 EditorMode::full(),
19009 multi_buffer,
19010 Some(project.clone()),
19011 window,
19012 cx,
19013 )
19014 });
19015 let multibuffer_item_id = workspace
19016 .update(cx, |workspace, window, cx| {
19017 assert!(
19018 workspace.active_item(cx).is_none(),
19019 "active item should be None before the first item is added"
19020 );
19021 workspace.add_item_to_active_pane(
19022 Box::new(multi_buffer_editor.clone()),
19023 None,
19024 true,
19025 window,
19026 cx,
19027 );
19028 let active_item = workspace
19029 .active_item(cx)
19030 .expect("should have an active item after adding the multi buffer");
19031 assert_eq!(
19032 active_item.buffer_kind(cx),
19033 ItemBufferKind::Multibuffer,
19034 "A multi buffer was expected to active after adding"
19035 );
19036 active_item.item_id()
19037 })
19038 .unwrap();
19039 cx.executor().run_until_parked();
19040
19041 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19042 editor.change_selections(
19043 SelectionEffects::scroll(Autoscroll::Next),
19044 window,
19045 cx,
19046 |s| s.select_ranges(Some(1..2)),
19047 );
19048 editor.open_excerpts(&OpenExcerpts, window, cx);
19049 });
19050 cx.executor().run_until_parked();
19051 let first_item_id = workspace
19052 .update(cx, |workspace, window, cx| {
19053 let active_item = workspace
19054 .active_item(cx)
19055 .expect("should have an active item after navigating into the 1st buffer");
19056 let first_item_id = active_item.item_id();
19057 assert_ne!(
19058 first_item_id, multibuffer_item_id,
19059 "Should navigate into the 1st buffer and activate it"
19060 );
19061 assert_eq!(
19062 active_item.buffer_kind(cx),
19063 ItemBufferKind::Singleton,
19064 "New active item should be a singleton buffer"
19065 );
19066 assert_eq!(
19067 active_item
19068 .act_as::<Editor>(cx)
19069 .expect("should have navigated into an editor for the 1st buffer")
19070 .read(cx)
19071 .text(cx),
19072 sample_text_1
19073 );
19074
19075 workspace
19076 .go_back(workspace.active_pane().downgrade(), window, cx)
19077 .detach_and_log_err(cx);
19078
19079 first_item_id
19080 })
19081 .unwrap();
19082 cx.executor().run_until_parked();
19083 workspace
19084 .update(cx, |workspace, _, cx| {
19085 let active_item = workspace
19086 .active_item(cx)
19087 .expect("should have an active item after navigating back");
19088 assert_eq!(
19089 active_item.item_id(),
19090 multibuffer_item_id,
19091 "Should navigate back to the multi buffer"
19092 );
19093 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19094 })
19095 .unwrap();
19096
19097 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19098 editor.change_selections(
19099 SelectionEffects::scroll(Autoscroll::Next),
19100 window,
19101 cx,
19102 |s| s.select_ranges(Some(39..40)),
19103 );
19104 editor.open_excerpts(&OpenExcerpts, window, cx);
19105 });
19106 cx.executor().run_until_parked();
19107 let second_item_id = workspace
19108 .update(cx, |workspace, window, cx| {
19109 let active_item = workspace
19110 .active_item(cx)
19111 .expect("should have an active item after navigating into the 2nd buffer");
19112 let second_item_id = active_item.item_id();
19113 assert_ne!(
19114 second_item_id, multibuffer_item_id,
19115 "Should navigate away from the multibuffer"
19116 );
19117 assert_ne!(
19118 second_item_id, first_item_id,
19119 "Should navigate into the 2nd buffer and activate it"
19120 );
19121 assert_eq!(
19122 active_item.buffer_kind(cx),
19123 ItemBufferKind::Singleton,
19124 "New active item should be a singleton buffer"
19125 );
19126 assert_eq!(
19127 active_item
19128 .act_as::<Editor>(cx)
19129 .expect("should have navigated into an editor")
19130 .read(cx)
19131 .text(cx),
19132 sample_text_2
19133 );
19134
19135 workspace
19136 .go_back(workspace.active_pane().downgrade(), window, cx)
19137 .detach_and_log_err(cx);
19138
19139 second_item_id
19140 })
19141 .unwrap();
19142 cx.executor().run_until_parked();
19143 workspace
19144 .update(cx, |workspace, _, cx| {
19145 let active_item = workspace
19146 .active_item(cx)
19147 .expect("should have an active item after navigating back from the 2nd buffer");
19148 assert_eq!(
19149 active_item.item_id(),
19150 multibuffer_item_id,
19151 "Should navigate back from the 2nd buffer to the multi buffer"
19152 );
19153 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19154 })
19155 .unwrap();
19156
19157 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19158 editor.change_selections(
19159 SelectionEffects::scroll(Autoscroll::Next),
19160 window,
19161 cx,
19162 |s| s.select_ranges(Some(70..70)),
19163 );
19164 editor.open_excerpts(&OpenExcerpts, window, cx);
19165 });
19166 cx.executor().run_until_parked();
19167 workspace
19168 .update(cx, |workspace, window, cx| {
19169 let active_item = workspace
19170 .active_item(cx)
19171 .expect("should have an active item after navigating into the 3rd buffer");
19172 let third_item_id = active_item.item_id();
19173 assert_ne!(
19174 third_item_id, multibuffer_item_id,
19175 "Should navigate into the 3rd buffer and activate it"
19176 );
19177 assert_ne!(third_item_id, first_item_id);
19178 assert_ne!(third_item_id, second_item_id);
19179 assert_eq!(
19180 active_item.buffer_kind(cx),
19181 ItemBufferKind::Singleton,
19182 "New active item should be a singleton buffer"
19183 );
19184 assert_eq!(
19185 active_item
19186 .act_as::<Editor>(cx)
19187 .expect("should have navigated into an editor")
19188 .read(cx)
19189 .text(cx),
19190 sample_text_3
19191 );
19192
19193 workspace
19194 .go_back(workspace.active_pane().downgrade(), window, cx)
19195 .detach_and_log_err(cx);
19196 })
19197 .unwrap();
19198 cx.executor().run_until_parked();
19199 workspace
19200 .update(cx, |workspace, _, cx| {
19201 let active_item = workspace
19202 .active_item(cx)
19203 .expect("should have an active item after navigating back from the 3rd buffer");
19204 assert_eq!(
19205 active_item.item_id(),
19206 multibuffer_item_id,
19207 "Should navigate back from the 3rd buffer to the multi buffer"
19208 );
19209 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19210 })
19211 .unwrap();
19212}
19213
19214#[gpui::test]
19215async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19216 init_test(cx, |_| {});
19217
19218 let mut cx = EditorTestContext::new(cx).await;
19219
19220 let diff_base = r#"
19221 use some::mod;
19222
19223 const A: u32 = 42;
19224
19225 fn main() {
19226 println!("hello");
19227
19228 println!("world");
19229 }
19230 "#
19231 .unindent();
19232
19233 cx.set_state(
19234 &r#"
19235 use some::modified;
19236
19237 ˇ
19238 fn main() {
19239 println!("hello there");
19240
19241 println!("around the");
19242 println!("world");
19243 }
19244 "#
19245 .unindent(),
19246 );
19247
19248 cx.set_head_text(&diff_base);
19249 executor.run_until_parked();
19250
19251 cx.update_editor(|editor, window, cx| {
19252 editor.go_to_next_hunk(&GoToHunk, window, cx);
19253 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19254 });
19255 executor.run_until_parked();
19256 cx.assert_state_with_diff(
19257 r#"
19258 use some::modified;
19259
19260
19261 fn main() {
19262 - println!("hello");
19263 + ˇ println!("hello there");
19264
19265 println!("around the");
19266 println!("world");
19267 }
19268 "#
19269 .unindent(),
19270 );
19271
19272 cx.update_editor(|editor, window, cx| {
19273 for _ in 0..2 {
19274 editor.go_to_next_hunk(&GoToHunk, window, cx);
19275 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19276 }
19277 });
19278 executor.run_until_parked();
19279 cx.assert_state_with_diff(
19280 r#"
19281 - use some::mod;
19282 + ˇuse some::modified;
19283
19284
19285 fn main() {
19286 - println!("hello");
19287 + println!("hello there");
19288
19289 + println!("around the");
19290 println!("world");
19291 }
19292 "#
19293 .unindent(),
19294 );
19295
19296 cx.update_editor(|editor, window, cx| {
19297 editor.go_to_next_hunk(&GoToHunk, window, cx);
19298 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19299 });
19300 executor.run_until_parked();
19301 cx.assert_state_with_diff(
19302 r#"
19303 - use some::mod;
19304 + use some::modified;
19305
19306 - const A: u32 = 42;
19307 ˇ
19308 fn main() {
19309 - println!("hello");
19310 + println!("hello there");
19311
19312 + println!("around the");
19313 println!("world");
19314 }
19315 "#
19316 .unindent(),
19317 );
19318
19319 cx.update_editor(|editor, window, cx| {
19320 editor.cancel(&Cancel, window, cx);
19321 });
19322
19323 cx.assert_state_with_diff(
19324 r#"
19325 use some::modified;
19326
19327 ˇ
19328 fn main() {
19329 println!("hello there");
19330
19331 println!("around the");
19332 println!("world");
19333 }
19334 "#
19335 .unindent(),
19336 );
19337}
19338
19339#[gpui::test]
19340async fn test_diff_base_change_with_expanded_diff_hunks(
19341 executor: BackgroundExecutor,
19342 cx: &mut TestAppContext,
19343) {
19344 init_test(cx, |_| {});
19345
19346 let mut cx = EditorTestContext::new(cx).await;
19347
19348 let diff_base = r#"
19349 use some::mod1;
19350 use some::mod2;
19351
19352 const A: u32 = 42;
19353 const B: u32 = 42;
19354 const C: u32 = 42;
19355
19356 fn main() {
19357 println!("hello");
19358
19359 println!("world");
19360 }
19361 "#
19362 .unindent();
19363
19364 cx.set_state(
19365 &r#"
19366 use some::mod2;
19367
19368 const A: u32 = 42;
19369 const C: u32 = 42;
19370
19371 fn main(ˇ) {
19372 //println!("hello");
19373
19374 println!("world");
19375 //
19376 //
19377 }
19378 "#
19379 .unindent(),
19380 );
19381
19382 cx.set_head_text(&diff_base);
19383 executor.run_until_parked();
19384
19385 cx.update_editor(|editor, window, cx| {
19386 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19387 });
19388 executor.run_until_parked();
19389 cx.assert_state_with_diff(
19390 r#"
19391 - use some::mod1;
19392 use some::mod2;
19393
19394 const A: u32 = 42;
19395 - const B: u32 = 42;
19396 const C: u32 = 42;
19397
19398 fn main(ˇ) {
19399 - println!("hello");
19400 + //println!("hello");
19401
19402 println!("world");
19403 + //
19404 + //
19405 }
19406 "#
19407 .unindent(),
19408 );
19409
19410 cx.set_head_text("new diff base!");
19411 executor.run_until_parked();
19412 cx.assert_state_with_diff(
19413 r#"
19414 - new diff base!
19415 + use some::mod2;
19416 +
19417 + const A: u32 = 42;
19418 + const C: u32 = 42;
19419 +
19420 + fn main(ˇ) {
19421 + //println!("hello");
19422 +
19423 + println!("world");
19424 + //
19425 + //
19426 + }
19427 "#
19428 .unindent(),
19429 );
19430}
19431
19432#[gpui::test]
19433async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19434 init_test(cx, |_| {});
19435
19436 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19437 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19438 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19439 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19440 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19441 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19442
19443 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19444 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19445 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19446
19447 let multi_buffer = cx.new(|cx| {
19448 let mut multibuffer = MultiBuffer::new(ReadWrite);
19449 multibuffer.push_excerpts(
19450 buffer_1.clone(),
19451 [
19452 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19453 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19454 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19455 ],
19456 cx,
19457 );
19458 multibuffer.push_excerpts(
19459 buffer_2.clone(),
19460 [
19461 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19462 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19463 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19464 ],
19465 cx,
19466 );
19467 multibuffer.push_excerpts(
19468 buffer_3.clone(),
19469 [
19470 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19471 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19472 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19473 ],
19474 cx,
19475 );
19476 multibuffer
19477 });
19478
19479 let editor =
19480 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19481 editor
19482 .update(cx, |editor, _window, cx| {
19483 for (buffer, diff_base) in [
19484 (buffer_1.clone(), file_1_old),
19485 (buffer_2.clone(), file_2_old),
19486 (buffer_3.clone(), file_3_old),
19487 ] {
19488 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19489 editor
19490 .buffer
19491 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19492 }
19493 })
19494 .unwrap();
19495
19496 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19497 cx.run_until_parked();
19498
19499 cx.assert_editor_state(
19500 &"
19501 ˇaaa
19502 ccc
19503 ddd
19504
19505 ggg
19506 hhh
19507
19508
19509 lll
19510 mmm
19511 NNN
19512
19513 qqq
19514 rrr
19515
19516 uuu
19517 111
19518 222
19519 333
19520
19521 666
19522 777
19523
19524 000
19525 !!!"
19526 .unindent(),
19527 );
19528
19529 cx.update_editor(|editor, window, cx| {
19530 editor.select_all(&SelectAll, window, cx);
19531 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19532 });
19533 cx.executor().run_until_parked();
19534
19535 cx.assert_state_with_diff(
19536 "
19537 «aaa
19538 - bbb
19539 ccc
19540 ddd
19541
19542 ggg
19543 hhh
19544
19545
19546 lll
19547 mmm
19548 - nnn
19549 + NNN
19550
19551 qqq
19552 rrr
19553
19554 uuu
19555 111
19556 222
19557 333
19558
19559 + 666
19560 777
19561
19562 000
19563 !!!ˇ»"
19564 .unindent(),
19565 );
19566}
19567
19568#[gpui::test]
19569async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19570 init_test(cx, |_| {});
19571
19572 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19573 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19574
19575 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19576 let multi_buffer = cx.new(|cx| {
19577 let mut multibuffer = MultiBuffer::new(ReadWrite);
19578 multibuffer.push_excerpts(
19579 buffer.clone(),
19580 [
19581 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19582 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19583 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19584 ],
19585 cx,
19586 );
19587 multibuffer
19588 });
19589
19590 let editor =
19591 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19592 editor
19593 .update(cx, |editor, _window, cx| {
19594 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19595 editor
19596 .buffer
19597 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19598 })
19599 .unwrap();
19600
19601 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19602 cx.run_until_parked();
19603
19604 cx.update_editor(|editor, window, cx| {
19605 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19606 });
19607 cx.executor().run_until_parked();
19608
19609 // When the start of a hunk coincides with the start of its excerpt,
19610 // the hunk is expanded. When the start of a hunk is earlier than
19611 // the start of its excerpt, the hunk is not expanded.
19612 cx.assert_state_with_diff(
19613 "
19614 ˇaaa
19615 - bbb
19616 + BBB
19617
19618 - ddd
19619 - eee
19620 + DDD
19621 + EEE
19622 fff
19623
19624 iii
19625 "
19626 .unindent(),
19627 );
19628}
19629
19630#[gpui::test]
19631async fn test_edits_around_expanded_insertion_hunks(
19632 executor: BackgroundExecutor,
19633 cx: &mut TestAppContext,
19634) {
19635 init_test(cx, |_| {});
19636
19637 let mut cx = EditorTestContext::new(cx).await;
19638
19639 let diff_base = r#"
19640 use some::mod1;
19641 use some::mod2;
19642
19643 const A: u32 = 42;
19644
19645 fn main() {
19646 println!("hello");
19647
19648 println!("world");
19649 }
19650 "#
19651 .unindent();
19652 executor.run_until_parked();
19653 cx.set_state(
19654 &r#"
19655 use some::mod1;
19656 use some::mod2;
19657
19658 const A: u32 = 42;
19659 const B: u32 = 42;
19660 const C: u32 = 42;
19661 ˇ
19662
19663 fn main() {
19664 println!("hello");
19665
19666 println!("world");
19667 }
19668 "#
19669 .unindent(),
19670 );
19671
19672 cx.set_head_text(&diff_base);
19673 executor.run_until_parked();
19674
19675 cx.update_editor(|editor, window, cx| {
19676 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19677 });
19678 executor.run_until_parked();
19679
19680 cx.assert_state_with_diff(
19681 r#"
19682 use some::mod1;
19683 use some::mod2;
19684
19685 const A: u32 = 42;
19686 + const B: u32 = 42;
19687 + const C: u32 = 42;
19688 + ˇ
19689
19690 fn main() {
19691 println!("hello");
19692
19693 println!("world");
19694 }
19695 "#
19696 .unindent(),
19697 );
19698
19699 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19700 executor.run_until_parked();
19701
19702 cx.assert_state_with_diff(
19703 r#"
19704 use some::mod1;
19705 use some::mod2;
19706
19707 const A: u32 = 42;
19708 + const B: u32 = 42;
19709 + const C: u32 = 42;
19710 + const D: u32 = 42;
19711 + ˇ
19712
19713 fn main() {
19714 println!("hello");
19715
19716 println!("world");
19717 }
19718 "#
19719 .unindent(),
19720 );
19721
19722 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19723 executor.run_until_parked();
19724
19725 cx.assert_state_with_diff(
19726 r#"
19727 use some::mod1;
19728 use some::mod2;
19729
19730 const A: u32 = 42;
19731 + const B: u32 = 42;
19732 + const C: u32 = 42;
19733 + const D: u32 = 42;
19734 + const E: u32 = 42;
19735 + ˇ
19736
19737 fn main() {
19738 println!("hello");
19739
19740 println!("world");
19741 }
19742 "#
19743 .unindent(),
19744 );
19745
19746 cx.update_editor(|editor, window, cx| {
19747 editor.delete_line(&DeleteLine, window, cx);
19748 });
19749 executor.run_until_parked();
19750
19751 cx.assert_state_with_diff(
19752 r#"
19753 use some::mod1;
19754 use some::mod2;
19755
19756 const A: u32 = 42;
19757 + const B: u32 = 42;
19758 + const C: u32 = 42;
19759 + const D: u32 = 42;
19760 + const E: u32 = 42;
19761 ˇ
19762 fn main() {
19763 println!("hello");
19764
19765 println!("world");
19766 }
19767 "#
19768 .unindent(),
19769 );
19770
19771 cx.update_editor(|editor, window, cx| {
19772 editor.move_up(&MoveUp, window, cx);
19773 editor.delete_line(&DeleteLine, window, cx);
19774 editor.move_up(&MoveUp, window, cx);
19775 editor.delete_line(&DeleteLine, window, cx);
19776 editor.move_up(&MoveUp, window, cx);
19777 editor.delete_line(&DeleteLine, window, cx);
19778 });
19779 executor.run_until_parked();
19780 cx.assert_state_with_diff(
19781 r#"
19782 use some::mod1;
19783 use some::mod2;
19784
19785 const A: u32 = 42;
19786 + const B: u32 = 42;
19787 ˇ
19788 fn main() {
19789 println!("hello");
19790
19791 println!("world");
19792 }
19793 "#
19794 .unindent(),
19795 );
19796
19797 cx.update_editor(|editor, window, cx| {
19798 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19799 editor.delete_line(&DeleteLine, window, cx);
19800 });
19801 executor.run_until_parked();
19802 cx.assert_state_with_diff(
19803 r#"
19804 ˇ
19805 fn main() {
19806 println!("hello");
19807
19808 println!("world");
19809 }
19810 "#
19811 .unindent(),
19812 );
19813}
19814
19815#[gpui::test]
19816async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19817 init_test(cx, |_| {});
19818
19819 let mut cx = EditorTestContext::new(cx).await;
19820 cx.set_head_text(indoc! { "
19821 one
19822 two
19823 three
19824 four
19825 five
19826 "
19827 });
19828 cx.set_state(indoc! { "
19829 one
19830 ˇthree
19831 five
19832 "});
19833 cx.run_until_parked();
19834 cx.update_editor(|editor, window, cx| {
19835 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19836 });
19837 cx.assert_state_with_diff(
19838 indoc! { "
19839 one
19840 - two
19841 ˇthree
19842 - four
19843 five
19844 "}
19845 .to_string(),
19846 );
19847 cx.update_editor(|editor, window, cx| {
19848 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19849 });
19850
19851 cx.assert_state_with_diff(
19852 indoc! { "
19853 one
19854 ˇthree
19855 five
19856 "}
19857 .to_string(),
19858 );
19859
19860 cx.set_state(indoc! { "
19861 one
19862 ˇTWO
19863 three
19864 four
19865 five
19866 "});
19867 cx.run_until_parked();
19868 cx.update_editor(|editor, window, cx| {
19869 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19870 });
19871
19872 cx.assert_state_with_diff(
19873 indoc! { "
19874 one
19875 - two
19876 + ˇTWO
19877 three
19878 four
19879 five
19880 "}
19881 .to_string(),
19882 );
19883 cx.update_editor(|editor, window, cx| {
19884 editor.move_up(&Default::default(), window, cx);
19885 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19886 });
19887 cx.assert_state_with_diff(
19888 indoc! { "
19889 one
19890 ˇTWO
19891 three
19892 four
19893 five
19894 "}
19895 .to_string(),
19896 );
19897}
19898
19899#[gpui::test]
19900async fn test_edits_around_expanded_deletion_hunks(
19901 executor: BackgroundExecutor,
19902 cx: &mut TestAppContext,
19903) {
19904 init_test(cx, |_| {});
19905
19906 let mut cx = EditorTestContext::new(cx).await;
19907
19908 let diff_base = r#"
19909 use some::mod1;
19910 use some::mod2;
19911
19912 const A: u32 = 42;
19913 const B: u32 = 42;
19914 const C: u32 = 42;
19915
19916
19917 fn main() {
19918 println!("hello");
19919
19920 println!("world");
19921 }
19922 "#
19923 .unindent();
19924 executor.run_until_parked();
19925 cx.set_state(
19926 &r#"
19927 use some::mod1;
19928 use some::mod2;
19929
19930 ˇconst B: u32 = 42;
19931 const C: u32 = 42;
19932
19933
19934 fn main() {
19935 println!("hello");
19936
19937 println!("world");
19938 }
19939 "#
19940 .unindent(),
19941 );
19942
19943 cx.set_head_text(&diff_base);
19944 executor.run_until_parked();
19945
19946 cx.update_editor(|editor, window, cx| {
19947 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19948 });
19949 executor.run_until_parked();
19950
19951 cx.assert_state_with_diff(
19952 r#"
19953 use some::mod1;
19954 use some::mod2;
19955
19956 - const A: u32 = 42;
19957 ˇconst B: u32 = 42;
19958 const C: u32 = 42;
19959
19960
19961 fn main() {
19962 println!("hello");
19963
19964 println!("world");
19965 }
19966 "#
19967 .unindent(),
19968 );
19969
19970 cx.update_editor(|editor, window, cx| {
19971 editor.delete_line(&DeleteLine, window, cx);
19972 });
19973 executor.run_until_parked();
19974 cx.assert_state_with_diff(
19975 r#"
19976 use some::mod1;
19977 use some::mod2;
19978
19979 - const A: u32 = 42;
19980 - const B: u32 = 42;
19981 ˇconst C: u32 = 42;
19982
19983
19984 fn main() {
19985 println!("hello");
19986
19987 println!("world");
19988 }
19989 "#
19990 .unindent(),
19991 );
19992
19993 cx.update_editor(|editor, window, cx| {
19994 editor.delete_line(&DeleteLine, window, cx);
19995 });
19996 executor.run_until_parked();
19997 cx.assert_state_with_diff(
19998 r#"
19999 use some::mod1;
20000 use some::mod2;
20001
20002 - const A: u32 = 42;
20003 - const B: u32 = 42;
20004 - const C: u32 = 42;
20005 ˇ
20006
20007 fn main() {
20008 println!("hello");
20009
20010 println!("world");
20011 }
20012 "#
20013 .unindent(),
20014 );
20015
20016 cx.update_editor(|editor, window, cx| {
20017 editor.handle_input("replacement", window, cx);
20018 });
20019 executor.run_until_parked();
20020 cx.assert_state_with_diff(
20021 r#"
20022 use some::mod1;
20023 use some::mod2;
20024
20025 - const A: u32 = 42;
20026 - const B: u32 = 42;
20027 - const C: u32 = 42;
20028 -
20029 + replacementˇ
20030
20031 fn main() {
20032 println!("hello");
20033
20034 println!("world");
20035 }
20036 "#
20037 .unindent(),
20038 );
20039}
20040
20041#[gpui::test]
20042async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20043 init_test(cx, |_| {});
20044
20045 let mut cx = EditorTestContext::new(cx).await;
20046
20047 let base_text = r#"
20048 one
20049 two
20050 three
20051 four
20052 five
20053 "#
20054 .unindent();
20055 executor.run_until_parked();
20056 cx.set_state(
20057 &r#"
20058 one
20059 two
20060 fˇour
20061 five
20062 "#
20063 .unindent(),
20064 );
20065
20066 cx.set_head_text(&base_text);
20067 executor.run_until_parked();
20068
20069 cx.update_editor(|editor, window, cx| {
20070 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20071 });
20072 executor.run_until_parked();
20073
20074 cx.assert_state_with_diff(
20075 r#"
20076 one
20077 two
20078 - three
20079 fˇour
20080 five
20081 "#
20082 .unindent(),
20083 );
20084
20085 cx.update_editor(|editor, window, cx| {
20086 editor.backspace(&Backspace, window, cx);
20087 editor.backspace(&Backspace, window, cx);
20088 });
20089 executor.run_until_parked();
20090 cx.assert_state_with_diff(
20091 r#"
20092 one
20093 two
20094 - threeˇ
20095 - four
20096 + our
20097 five
20098 "#
20099 .unindent(),
20100 );
20101}
20102
20103#[gpui::test]
20104async fn test_edit_after_expanded_modification_hunk(
20105 executor: BackgroundExecutor,
20106 cx: &mut TestAppContext,
20107) {
20108 init_test(cx, |_| {});
20109
20110 let mut cx = EditorTestContext::new(cx).await;
20111
20112 let diff_base = r#"
20113 use some::mod1;
20114 use some::mod2;
20115
20116 const A: u32 = 42;
20117 const B: u32 = 42;
20118 const C: u32 = 42;
20119 const D: u32 = 42;
20120
20121
20122 fn main() {
20123 println!("hello");
20124
20125 println!("world");
20126 }"#
20127 .unindent();
20128
20129 cx.set_state(
20130 &r#"
20131 use some::mod1;
20132 use some::mod2;
20133
20134 const A: u32 = 42;
20135 const B: u32 = 42;
20136 const C: u32 = 43ˇ
20137 const D: u32 = 42;
20138
20139
20140 fn main() {
20141 println!("hello");
20142
20143 println!("world");
20144 }"#
20145 .unindent(),
20146 );
20147
20148 cx.set_head_text(&diff_base);
20149 executor.run_until_parked();
20150 cx.update_editor(|editor, window, cx| {
20151 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20152 });
20153 executor.run_until_parked();
20154
20155 cx.assert_state_with_diff(
20156 r#"
20157 use some::mod1;
20158 use some::mod2;
20159
20160 const A: u32 = 42;
20161 const B: u32 = 42;
20162 - const C: u32 = 42;
20163 + const C: u32 = 43ˇ
20164 const D: u32 = 42;
20165
20166
20167 fn main() {
20168 println!("hello");
20169
20170 println!("world");
20171 }"#
20172 .unindent(),
20173 );
20174
20175 cx.update_editor(|editor, window, cx| {
20176 editor.handle_input("\nnew_line\n", window, cx);
20177 });
20178 executor.run_until_parked();
20179
20180 cx.assert_state_with_diff(
20181 r#"
20182 use some::mod1;
20183 use some::mod2;
20184
20185 const A: u32 = 42;
20186 const B: u32 = 42;
20187 - const C: u32 = 42;
20188 + const C: u32 = 43
20189 + new_line
20190 + ˇ
20191 const D: u32 = 42;
20192
20193
20194 fn main() {
20195 println!("hello");
20196
20197 println!("world");
20198 }"#
20199 .unindent(),
20200 );
20201}
20202
20203#[gpui::test]
20204async fn test_stage_and_unstage_added_file_hunk(
20205 executor: BackgroundExecutor,
20206 cx: &mut TestAppContext,
20207) {
20208 init_test(cx, |_| {});
20209
20210 let mut cx = EditorTestContext::new(cx).await;
20211 cx.update_editor(|editor, _, cx| {
20212 editor.set_expand_all_diff_hunks(cx);
20213 });
20214
20215 let working_copy = r#"
20216 ˇfn main() {
20217 println!("hello, world!");
20218 }
20219 "#
20220 .unindent();
20221
20222 cx.set_state(&working_copy);
20223 executor.run_until_parked();
20224
20225 cx.assert_state_with_diff(
20226 r#"
20227 + ˇfn main() {
20228 + println!("hello, world!");
20229 + }
20230 "#
20231 .unindent(),
20232 );
20233 cx.assert_index_text(None);
20234
20235 cx.update_editor(|editor, window, cx| {
20236 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20237 });
20238 executor.run_until_parked();
20239 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20240 cx.assert_state_with_diff(
20241 r#"
20242 + ˇfn main() {
20243 + println!("hello, world!");
20244 + }
20245 "#
20246 .unindent(),
20247 );
20248
20249 cx.update_editor(|editor, window, cx| {
20250 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20251 });
20252 executor.run_until_parked();
20253 cx.assert_index_text(None);
20254}
20255
20256async fn setup_indent_guides_editor(
20257 text: &str,
20258 cx: &mut TestAppContext,
20259) -> (BufferId, EditorTestContext) {
20260 init_test(cx, |_| {});
20261
20262 let mut cx = EditorTestContext::new(cx).await;
20263
20264 let buffer_id = cx.update_editor(|editor, window, cx| {
20265 editor.set_text(text, window, cx);
20266 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20267
20268 buffer_ids[0]
20269 });
20270
20271 (buffer_id, cx)
20272}
20273
20274fn assert_indent_guides(
20275 range: Range<u32>,
20276 expected: Vec<IndentGuide>,
20277 active_indices: Option<Vec<usize>>,
20278 cx: &mut EditorTestContext,
20279) {
20280 let indent_guides = cx.update_editor(|editor, window, cx| {
20281 let snapshot = editor.snapshot(window, cx).display_snapshot;
20282 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20283 editor,
20284 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20285 true,
20286 &snapshot,
20287 cx,
20288 );
20289
20290 indent_guides.sort_by(|a, b| {
20291 a.depth.cmp(&b.depth).then(
20292 a.start_row
20293 .cmp(&b.start_row)
20294 .then(a.end_row.cmp(&b.end_row)),
20295 )
20296 });
20297 indent_guides
20298 });
20299
20300 if let Some(expected) = active_indices {
20301 let active_indices = cx.update_editor(|editor, window, cx| {
20302 let snapshot = editor.snapshot(window, cx).display_snapshot;
20303 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20304 });
20305
20306 assert_eq!(
20307 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20308 expected,
20309 "Active indent guide indices do not match"
20310 );
20311 }
20312
20313 assert_eq!(indent_guides, expected, "Indent guides do not match");
20314}
20315
20316fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20317 IndentGuide {
20318 buffer_id,
20319 start_row: MultiBufferRow(start_row),
20320 end_row: MultiBufferRow(end_row),
20321 depth,
20322 tab_size: 4,
20323 settings: IndentGuideSettings {
20324 enabled: true,
20325 line_width: 1,
20326 active_line_width: 1,
20327 coloring: IndentGuideColoring::default(),
20328 background_coloring: IndentGuideBackgroundColoring::default(),
20329 },
20330 }
20331}
20332
20333#[gpui::test]
20334async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20335 let (buffer_id, mut cx) = setup_indent_guides_editor(
20336 &"
20337 fn main() {
20338 let a = 1;
20339 }"
20340 .unindent(),
20341 cx,
20342 )
20343 .await;
20344
20345 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20346}
20347
20348#[gpui::test]
20349async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20350 let (buffer_id, mut cx) = setup_indent_guides_editor(
20351 &"
20352 fn main() {
20353 let a = 1;
20354 let b = 2;
20355 }"
20356 .unindent(),
20357 cx,
20358 )
20359 .await;
20360
20361 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20362}
20363
20364#[gpui::test]
20365async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20366 let (buffer_id, mut cx) = setup_indent_guides_editor(
20367 &"
20368 fn main() {
20369 let a = 1;
20370 if a == 3 {
20371 let b = 2;
20372 } else {
20373 let c = 3;
20374 }
20375 }"
20376 .unindent(),
20377 cx,
20378 )
20379 .await;
20380
20381 assert_indent_guides(
20382 0..8,
20383 vec![
20384 indent_guide(buffer_id, 1, 6, 0),
20385 indent_guide(buffer_id, 3, 3, 1),
20386 indent_guide(buffer_id, 5, 5, 1),
20387 ],
20388 None,
20389 &mut cx,
20390 );
20391}
20392
20393#[gpui::test]
20394async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20395 let (buffer_id, mut cx) = setup_indent_guides_editor(
20396 &"
20397 fn main() {
20398 let a = 1;
20399 let b = 2;
20400 let c = 3;
20401 }"
20402 .unindent(),
20403 cx,
20404 )
20405 .await;
20406
20407 assert_indent_guides(
20408 0..5,
20409 vec![
20410 indent_guide(buffer_id, 1, 3, 0),
20411 indent_guide(buffer_id, 2, 2, 1),
20412 ],
20413 None,
20414 &mut cx,
20415 );
20416}
20417
20418#[gpui::test]
20419async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20420 let (buffer_id, mut cx) = setup_indent_guides_editor(
20421 &"
20422 fn main() {
20423 let a = 1;
20424
20425 let c = 3;
20426 }"
20427 .unindent(),
20428 cx,
20429 )
20430 .await;
20431
20432 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20433}
20434
20435#[gpui::test]
20436async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20437 let (buffer_id, mut cx) = setup_indent_guides_editor(
20438 &"
20439 fn main() {
20440 let a = 1;
20441
20442 let c = 3;
20443
20444 if a == 3 {
20445 let b = 2;
20446 } else {
20447 let c = 3;
20448 }
20449 }"
20450 .unindent(),
20451 cx,
20452 )
20453 .await;
20454
20455 assert_indent_guides(
20456 0..11,
20457 vec![
20458 indent_guide(buffer_id, 1, 9, 0),
20459 indent_guide(buffer_id, 6, 6, 1),
20460 indent_guide(buffer_id, 8, 8, 1),
20461 ],
20462 None,
20463 &mut cx,
20464 );
20465}
20466
20467#[gpui::test]
20468async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20469 let (buffer_id, mut cx) = setup_indent_guides_editor(
20470 &"
20471 fn main() {
20472 let a = 1;
20473
20474 let c = 3;
20475
20476 if a == 3 {
20477 let b = 2;
20478 } else {
20479 let c = 3;
20480 }
20481 }"
20482 .unindent(),
20483 cx,
20484 )
20485 .await;
20486
20487 assert_indent_guides(
20488 1..11,
20489 vec![
20490 indent_guide(buffer_id, 1, 9, 0),
20491 indent_guide(buffer_id, 6, 6, 1),
20492 indent_guide(buffer_id, 8, 8, 1),
20493 ],
20494 None,
20495 &mut cx,
20496 );
20497}
20498
20499#[gpui::test]
20500async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20501 let (buffer_id, mut cx) = setup_indent_guides_editor(
20502 &"
20503 fn main() {
20504 let a = 1;
20505
20506 let c = 3;
20507
20508 if a == 3 {
20509 let b = 2;
20510 } else {
20511 let c = 3;
20512 }
20513 }"
20514 .unindent(),
20515 cx,
20516 )
20517 .await;
20518
20519 assert_indent_guides(
20520 1..10,
20521 vec![
20522 indent_guide(buffer_id, 1, 9, 0),
20523 indent_guide(buffer_id, 6, 6, 1),
20524 indent_guide(buffer_id, 8, 8, 1),
20525 ],
20526 None,
20527 &mut cx,
20528 );
20529}
20530
20531#[gpui::test]
20532async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20533 let (buffer_id, mut cx) = setup_indent_guides_editor(
20534 &"
20535 fn main() {
20536 if a {
20537 b(
20538 c,
20539 d,
20540 )
20541 } else {
20542 e(
20543 f
20544 )
20545 }
20546 }"
20547 .unindent(),
20548 cx,
20549 )
20550 .await;
20551
20552 assert_indent_guides(
20553 0..11,
20554 vec![
20555 indent_guide(buffer_id, 1, 10, 0),
20556 indent_guide(buffer_id, 2, 5, 1),
20557 indent_guide(buffer_id, 7, 9, 1),
20558 indent_guide(buffer_id, 3, 4, 2),
20559 indent_guide(buffer_id, 8, 8, 2),
20560 ],
20561 None,
20562 &mut cx,
20563 );
20564
20565 cx.update_editor(|editor, window, cx| {
20566 editor.fold_at(MultiBufferRow(2), window, cx);
20567 assert_eq!(
20568 editor.display_text(cx),
20569 "
20570 fn main() {
20571 if a {
20572 b(⋯
20573 )
20574 } else {
20575 e(
20576 f
20577 )
20578 }
20579 }"
20580 .unindent()
20581 );
20582 });
20583
20584 assert_indent_guides(
20585 0..11,
20586 vec![
20587 indent_guide(buffer_id, 1, 10, 0),
20588 indent_guide(buffer_id, 2, 5, 1),
20589 indent_guide(buffer_id, 7, 9, 1),
20590 indent_guide(buffer_id, 8, 8, 2),
20591 ],
20592 None,
20593 &mut cx,
20594 );
20595}
20596
20597#[gpui::test]
20598async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20599 let (buffer_id, mut cx) = setup_indent_guides_editor(
20600 &"
20601 block1
20602 block2
20603 block3
20604 block4
20605 block2
20606 block1
20607 block1"
20608 .unindent(),
20609 cx,
20610 )
20611 .await;
20612
20613 assert_indent_guides(
20614 1..10,
20615 vec![
20616 indent_guide(buffer_id, 1, 4, 0),
20617 indent_guide(buffer_id, 2, 3, 1),
20618 indent_guide(buffer_id, 3, 3, 2),
20619 ],
20620 None,
20621 &mut cx,
20622 );
20623}
20624
20625#[gpui::test]
20626async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20627 let (buffer_id, mut cx) = setup_indent_guides_editor(
20628 &"
20629 block1
20630 block2
20631 block3
20632
20633 block1
20634 block1"
20635 .unindent(),
20636 cx,
20637 )
20638 .await;
20639
20640 assert_indent_guides(
20641 0..6,
20642 vec![
20643 indent_guide(buffer_id, 1, 2, 0),
20644 indent_guide(buffer_id, 2, 2, 1),
20645 ],
20646 None,
20647 &mut cx,
20648 );
20649}
20650
20651#[gpui::test]
20652async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20653 let (buffer_id, mut cx) = setup_indent_guides_editor(
20654 &"
20655 function component() {
20656 \treturn (
20657 \t\t\t
20658 \t\t<div>
20659 \t\t\t<abc></abc>
20660 \t\t</div>
20661 \t)
20662 }"
20663 .unindent(),
20664 cx,
20665 )
20666 .await;
20667
20668 assert_indent_guides(
20669 0..8,
20670 vec![
20671 indent_guide(buffer_id, 1, 6, 0),
20672 indent_guide(buffer_id, 2, 5, 1),
20673 indent_guide(buffer_id, 4, 4, 2),
20674 ],
20675 None,
20676 &mut cx,
20677 );
20678}
20679
20680#[gpui::test]
20681async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20682 let (buffer_id, mut cx) = setup_indent_guides_editor(
20683 &"
20684 function component() {
20685 \treturn (
20686 \t
20687 \t\t<div>
20688 \t\t\t<abc></abc>
20689 \t\t</div>
20690 \t)
20691 }"
20692 .unindent(),
20693 cx,
20694 )
20695 .await;
20696
20697 assert_indent_guides(
20698 0..8,
20699 vec![
20700 indent_guide(buffer_id, 1, 6, 0),
20701 indent_guide(buffer_id, 2, 5, 1),
20702 indent_guide(buffer_id, 4, 4, 2),
20703 ],
20704 None,
20705 &mut cx,
20706 );
20707}
20708
20709#[gpui::test]
20710async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20711 let (buffer_id, mut cx) = setup_indent_guides_editor(
20712 &"
20713 block1
20714
20715
20716
20717 block2
20718 "
20719 .unindent(),
20720 cx,
20721 )
20722 .await;
20723
20724 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20725}
20726
20727#[gpui::test]
20728async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20729 let (buffer_id, mut cx) = setup_indent_guides_editor(
20730 &"
20731 def a:
20732 \tb = 3
20733 \tif True:
20734 \t\tc = 4
20735 \t\td = 5
20736 \tprint(b)
20737 "
20738 .unindent(),
20739 cx,
20740 )
20741 .await;
20742
20743 assert_indent_guides(
20744 0..6,
20745 vec![
20746 indent_guide(buffer_id, 1, 5, 0),
20747 indent_guide(buffer_id, 3, 4, 1),
20748 ],
20749 None,
20750 &mut cx,
20751 );
20752}
20753
20754#[gpui::test]
20755async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20756 let (buffer_id, mut cx) = setup_indent_guides_editor(
20757 &"
20758 fn main() {
20759 let a = 1;
20760 }"
20761 .unindent(),
20762 cx,
20763 )
20764 .await;
20765
20766 cx.update_editor(|editor, window, cx| {
20767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20768 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20769 });
20770 });
20771
20772 assert_indent_guides(
20773 0..3,
20774 vec![indent_guide(buffer_id, 1, 1, 0)],
20775 Some(vec![0]),
20776 &mut cx,
20777 );
20778}
20779
20780#[gpui::test]
20781async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20782 let (buffer_id, mut cx) = setup_indent_guides_editor(
20783 &"
20784 fn main() {
20785 if 1 == 2 {
20786 let a = 1;
20787 }
20788 }"
20789 .unindent(),
20790 cx,
20791 )
20792 .await;
20793
20794 cx.update_editor(|editor, window, cx| {
20795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20796 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20797 });
20798 });
20799
20800 assert_indent_guides(
20801 0..4,
20802 vec![
20803 indent_guide(buffer_id, 1, 3, 0),
20804 indent_guide(buffer_id, 2, 2, 1),
20805 ],
20806 Some(vec![1]),
20807 &mut cx,
20808 );
20809
20810 cx.update_editor(|editor, window, cx| {
20811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20812 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20813 });
20814 });
20815
20816 assert_indent_guides(
20817 0..4,
20818 vec![
20819 indent_guide(buffer_id, 1, 3, 0),
20820 indent_guide(buffer_id, 2, 2, 1),
20821 ],
20822 Some(vec![1]),
20823 &mut cx,
20824 );
20825
20826 cx.update_editor(|editor, window, cx| {
20827 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20828 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20829 });
20830 });
20831
20832 assert_indent_guides(
20833 0..4,
20834 vec![
20835 indent_guide(buffer_id, 1, 3, 0),
20836 indent_guide(buffer_id, 2, 2, 1),
20837 ],
20838 Some(vec![0]),
20839 &mut cx,
20840 );
20841}
20842
20843#[gpui::test]
20844async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20845 let (buffer_id, mut cx) = setup_indent_guides_editor(
20846 &"
20847 fn main() {
20848 let a = 1;
20849
20850 let b = 2;
20851 }"
20852 .unindent(),
20853 cx,
20854 )
20855 .await;
20856
20857 cx.update_editor(|editor, window, cx| {
20858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20859 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20860 });
20861 });
20862
20863 assert_indent_guides(
20864 0..5,
20865 vec![indent_guide(buffer_id, 1, 3, 0)],
20866 Some(vec![0]),
20867 &mut cx,
20868 );
20869}
20870
20871#[gpui::test]
20872async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20873 let (buffer_id, mut cx) = setup_indent_guides_editor(
20874 &"
20875 def m:
20876 a = 1
20877 pass"
20878 .unindent(),
20879 cx,
20880 )
20881 .await;
20882
20883 cx.update_editor(|editor, window, cx| {
20884 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20885 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20886 });
20887 });
20888
20889 assert_indent_guides(
20890 0..3,
20891 vec![indent_guide(buffer_id, 1, 2, 0)],
20892 Some(vec![0]),
20893 &mut cx,
20894 );
20895}
20896
20897#[gpui::test]
20898async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20899 init_test(cx, |_| {});
20900 let mut cx = EditorTestContext::new(cx).await;
20901 let text = indoc! {
20902 "
20903 impl A {
20904 fn b() {
20905 0;
20906 3;
20907 5;
20908 6;
20909 7;
20910 }
20911 }
20912 "
20913 };
20914 let base_text = indoc! {
20915 "
20916 impl A {
20917 fn b() {
20918 0;
20919 1;
20920 2;
20921 3;
20922 4;
20923 }
20924 fn c() {
20925 5;
20926 6;
20927 7;
20928 }
20929 }
20930 "
20931 };
20932
20933 cx.update_editor(|editor, window, cx| {
20934 editor.set_text(text, window, cx);
20935
20936 editor.buffer().update(cx, |multibuffer, cx| {
20937 let buffer = multibuffer.as_singleton().unwrap();
20938 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20939
20940 multibuffer.set_all_diff_hunks_expanded(cx);
20941 multibuffer.add_diff(diff, cx);
20942
20943 buffer.read(cx).remote_id()
20944 })
20945 });
20946 cx.run_until_parked();
20947
20948 cx.assert_state_with_diff(
20949 indoc! { "
20950 impl A {
20951 fn b() {
20952 0;
20953 - 1;
20954 - 2;
20955 3;
20956 - 4;
20957 - }
20958 - fn c() {
20959 5;
20960 6;
20961 7;
20962 }
20963 }
20964 ˇ"
20965 }
20966 .to_string(),
20967 );
20968
20969 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20970 editor
20971 .snapshot(window, cx)
20972 .buffer_snapshot()
20973 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20974 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20975 .collect::<Vec<_>>()
20976 });
20977 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20978 assert_eq!(
20979 actual_guides,
20980 vec![
20981 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20982 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20983 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20984 ]
20985 );
20986}
20987
20988#[gpui::test]
20989async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20990 init_test(cx, |_| {});
20991 let mut cx = EditorTestContext::new(cx).await;
20992
20993 let diff_base = r#"
20994 a
20995 b
20996 c
20997 "#
20998 .unindent();
20999
21000 cx.set_state(
21001 &r#"
21002 ˇA
21003 b
21004 C
21005 "#
21006 .unindent(),
21007 );
21008 cx.set_head_text(&diff_base);
21009 cx.update_editor(|editor, window, cx| {
21010 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21011 });
21012 executor.run_until_parked();
21013
21014 let both_hunks_expanded = r#"
21015 - a
21016 + ˇA
21017 b
21018 - c
21019 + C
21020 "#
21021 .unindent();
21022
21023 cx.assert_state_with_diff(both_hunks_expanded.clone());
21024
21025 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21026 let snapshot = editor.snapshot(window, cx);
21027 let hunks = editor
21028 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21029 .collect::<Vec<_>>();
21030 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21031 let buffer_id = hunks[0].buffer_id;
21032 hunks
21033 .into_iter()
21034 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21035 .collect::<Vec<_>>()
21036 });
21037 assert_eq!(hunk_ranges.len(), 2);
21038
21039 cx.update_editor(|editor, _, cx| {
21040 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21041 });
21042 executor.run_until_parked();
21043
21044 let second_hunk_expanded = r#"
21045 ˇA
21046 b
21047 - c
21048 + C
21049 "#
21050 .unindent();
21051
21052 cx.assert_state_with_diff(second_hunk_expanded);
21053
21054 cx.update_editor(|editor, _, cx| {
21055 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21056 });
21057 executor.run_until_parked();
21058
21059 cx.assert_state_with_diff(both_hunks_expanded.clone());
21060
21061 cx.update_editor(|editor, _, cx| {
21062 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21063 });
21064 executor.run_until_parked();
21065
21066 let first_hunk_expanded = r#"
21067 - a
21068 + ˇA
21069 b
21070 C
21071 "#
21072 .unindent();
21073
21074 cx.assert_state_with_diff(first_hunk_expanded);
21075
21076 cx.update_editor(|editor, _, cx| {
21077 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21078 });
21079 executor.run_until_parked();
21080
21081 cx.assert_state_with_diff(both_hunks_expanded);
21082
21083 cx.set_state(
21084 &r#"
21085 ˇA
21086 b
21087 "#
21088 .unindent(),
21089 );
21090 cx.run_until_parked();
21091
21092 // TODO this cursor position seems bad
21093 cx.assert_state_with_diff(
21094 r#"
21095 - ˇa
21096 + A
21097 b
21098 "#
21099 .unindent(),
21100 );
21101
21102 cx.update_editor(|editor, window, cx| {
21103 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21104 });
21105
21106 cx.assert_state_with_diff(
21107 r#"
21108 - ˇa
21109 + A
21110 b
21111 - c
21112 "#
21113 .unindent(),
21114 );
21115
21116 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21117 let snapshot = editor.snapshot(window, cx);
21118 let hunks = editor
21119 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21120 .collect::<Vec<_>>();
21121 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21122 let buffer_id = hunks[0].buffer_id;
21123 hunks
21124 .into_iter()
21125 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21126 .collect::<Vec<_>>()
21127 });
21128 assert_eq!(hunk_ranges.len(), 2);
21129
21130 cx.update_editor(|editor, _, cx| {
21131 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21132 });
21133 executor.run_until_parked();
21134
21135 cx.assert_state_with_diff(
21136 r#"
21137 - ˇa
21138 + A
21139 b
21140 "#
21141 .unindent(),
21142 );
21143}
21144
21145#[gpui::test]
21146async fn test_toggle_deletion_hunk_at_start_of_file(
21147 executor: BackgroundExecutor,
21148 cx: &mut TestAppContext,
21149) {
21150 init_test(cx, |_| {});
21151 let mut cx = EditorTestContext::new(cx).await;
21152
21153 let diff_base = r#"
21154 a
21155 b
21156 c
21157 "#
21158 .unindent();
21159
21160 cx.set_state(
21161 &r#"
21162 ˇb
21163 c
21164 "#
21165 .unindent(),
21166 );
21167 cx.set_head_text(&diff_base);
21168 cx.update_editor(|editor, window, cx| {
21169 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21170 });
21171 executor.run_until_parked();
21172
21173 let hunk_expanded = r#"
21174 - a
21175 ˇb
21176 c
21177 "#
21178 .unindent();
21179
21180 cx.assert_state_with_diff(hunk_expanded.clone());
21181
21182 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21183 let snapshot = editor.snapshot(window, cx);
21184 let hunks = editor
21185 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21186 .collect::<Vec<_>>();
21187 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21188 let buffer_id = hunks[0].buffer_id;
21189 hunks
21190 .into_iter()
21191 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21192 .collect::<Vec<_>>()
21193 });
21194 assert_eq!(hunk_ranges.len(), 1);
21195
21196 cx.update_editor(|editor, _, cx| {
21197 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21198 });
21199 executor.run_until_parked();
21200
21201 let hunk_collapsed = r#"
21202 ˇb
21203 c
21204 "#
21205 .unindent();
21206
21207 cx.assert_state_with_diff(hunk_collapsed);
21208
21209 cx.update_editor(|editor, _, cx| {
21210 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21211 });
21212 executor.run_until_parked();
21213
21214 cx.assert_state_with_diff(hunk_expanded);
21215}
21216
21217#[gpui::test]
21218async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21219 init_test(cx, |_| {});
21220
21221 let fs = FakeFs::new(cx.executor());
21222 fs.insert_tree(
21223 path!("/test"),
21224 json!({
21225 ".git": {},
21226 "file-1": "ONE\n",
21227 "file-2": "TWO\n",
21228 "file-3": "THREE\n",
21229 }),
21230 )
21231 .await;
21232
21233 fs.set_head_for_repo(
21234 path!("/test/.git").as_ref(),
21235 &[
21236 ("file-1", "one\n".into()),
21237 ("file-2", "two\n".into()),
21238 ("file-3", "three\n".into()),
21239 ],
21240 "deadbeef",
21241 );
21242
21243 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21244 let mut buffers = vec![];
21245 for i in 1..=3 {
21246 let buffer = project
21247 .update(cx, |project, cx| {
21248 let path = format!(path!("/test/file-{}"), i);
21249 project.open_local_buffer(path, cx)
21250 })
21251 .await
21252 .unwrap();
21253 buffers.push(buffer);
21254 }
21255
21256 let multibuffer = cx.new(|cx| {
21257 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21258 multibuffer.set_all_diff_hunks_expanded(cx);
21259 for buffer in &buffers {
21260 let snapshot = buffer.read(cx).snapshot();
21261 multibuffer.set_excerpts_for_path(
21262 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21263 buffer.clone(),
21264 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21265 2,
21266 cx,
21267 );
21268 }
21269 multibuffer
21270 });
21271
21272 let editor = cx.add_window(|window, cx| {
21273 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21274 });
21275 cx.run_until_parked();
21276
21277 let snapshot = editor
21278 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21279 .unwrap();
21280 let hunks = snapshot
21281 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21282 .map(|hunk| match hunk {
21283 DisplayDiffHunk::Unfolded {
21284 display_row_range, ..
21285 } => display_row_range,
21286 DisplayDiffHunk::Folded { .. } => unreachable!(),
21287 })
21288 .collect::<Vec<_>>();
21289 assert_eq!(
21290 hunks,
21291 [
21292 DisplayRow(2)..DisplayRow(4),
21293 DisplayRow(7)..DisplayRow(9),
21294 DisplayRow(12)..DisplayRow(14),
21295 ]
21296 );
21297}
21298
21299#[gpui::test]
21300async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21301 init_test(cx, |_| {});
21302
21303 let mut cx = EditorTestContext::new(cx).await;
21304 cx.set_head_text(indoc! { "
21305 one
21306 two
21307 three
21308 four
21309 five
21310 "
21311 });
21312 cx.set_index_text(indoc! { "
21313 one
21314 two
21315 three
21316 four
21317 five
21318 "
21319 });
21320 cx.set_state(indoc! {"
21321 one
21322 TWO
21323 ˇTHREE
21324 FOUR
21325 five
21326 "});
21327 cx.run_until_parked();
21328 cx.update_editor(|editor, window, cx| {
21329 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21330 });
21331 cx.run_until_parked();
21332 cx.assert_index_text(Some(indoc! {"
21333 one
21334 TWO
21335 THREE
21336 FOUR
21337 five
21338 "}));
21339 cx.set_state(indoc! { "
21340 one
21341 TWO
21342 ˇTHREE-HUNDRED
21343 FOUR
21344 five
21345 "});
21346 cx.run_until_parked();
21347 cx.update_editor(|editor, window, cx| {
21348 let snapshot = editor.snapshot(window, cx);
21349 let hunks = editor
21350 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21351 .collect::<Vec<_>>();
21352 assert_eq!(hunks.len(), 1);
21353 assert_eq!(
21354 hunks[0].status(),
21355 DiffHunkStatus {
21356 kind: DiffHunkStatusKind::Modified,
21357 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21358 }
21359 );
21360
21361 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21362 });
21363 cx.run_until_parked();
21364 cx.assert_index_text(Some(indoc! {"
21365 one
21366 TWO
21367 THREE-HUNDRED
21368 FOUR
21369 five
21370 "}));
21371}
21372
21373#[gpui::test]
21374fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21375 init_test(cx, |_| {});
21376
21377 let editor = cx.add_window(|window, cx| {
21378 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21379 build_editor(buffer, window, cx)
21380 });
21381
21382 let render_args = Arc::new(Mutex::new(None));
21383 let snapshot = editor
21384 .update(cx, |editor, window, cx| {
21385 let snapshot = editor.buffer().read(cx).snapshot(cx);
21386 let range =
21387 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21388
21389 struct RenderArgs {
21390 row: MultiBufferRow,
21391 folded: bool,
21392 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21393 }
21394
21395 let crease = Crease::inline(
21396 range,
21397 FoldPlaceholder::test(),
21398 {
21399 let toggle_callback = render_args.clone();
21400 move |row, folded, callback, _window, _cx| {
21401 *toggle_callback.lock() = Some(RenderArgs {
21402 row,
21403 folded,
21404 callback,
21405 });
21406 div()
21407 }
21408 },
21409 |_row, _folded, _window, _cx| div(),
21410 );
21411
21412 editor.insert_creases(Some(crease), cx);
21413 let snapshot = editor.snapshot(window, cx);
21414 let _div =
21415 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21416 snapshot
21417 })
21418 .unwrap();
21419
21420 let render_args = render_args.lock().take().unwrap();
21421 assert_eq!(render_args.row, MultiBufferRow(1));
21422 assert!(!render_args.folded);
21423 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21424
21425 cx.update_window(*editor, |_, window, cx| {
21426 (render_args.callback)(true, window, cx)
21427 })
21428 .unwrap();
21429 let snapshot = editor
21430 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21431 .unwrap();
21432 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21433
21434 cx.update_window(*editor, |_, window, cx| {
21435 (render_args.callback)(false, window, cx)
21436 })
21437 .unwrap();
21438 let snapshot = editor
21439 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21440 .unwrap();
21441 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21442}
21443
21444#[gpui::test]
21445async fn test_input_text(cx: &mut TestAppContext) {
21446 init_test(cx, |_| {});
21447 let mut cx = EditorTestContext::new(cx).await;
21448
21449 cx.set_state(
21450 &r#"ˇone
21451 two
21452
21453 three
21454 fourˇ
21455 five
21456
21457 siˇx"#
21458 .unindent(),
21459 );
21460
21461 cx.dispatch_action(HandleInput(String::new()));
21462 cx.assert_editor_state(
21463 &r#"ˇone
21464 two
21465
21466 three
21467 fourˇ
21468 five
21469
21470 siˇx"#
21471 .unindent(),
21472 );
21473
21474 cx.dispatch_action(HandleInput("AAAA".to_string()));
21475 cx.assert_editor_state(
21476 &r#"AAAAˇone
21477 two
21478
21479 three
21480 fourAAAAˇ
21481 five
21482
21483 siAAAAˇx"#
21484 .unindent(),
21485 );
21486}
21487
21488#[gpui::test]
21489async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21490 init_test(cx, |_| {});
21491
21492 let mut cx = EditorTestContext::new(cx).await;
21493 cx.set_state(
21494 r#"let foo = 1;
21495let foo = 2;
21496let foo = 3;
21497let fooˇ = 4;
21498let foo = 5;
21499let foo = 6;
21500let foo = 7;
21501let foo = 8;
21502let foo = 9;
21503let foo = 10;
21504let foo = 11;
21505let foo = 12;
21506let foo = 13;
21507let foo = 14;
21508let foo = 15;"#,
21509 );
21510
21511 cx.update_editor(|e, window, cx| {
21512 assert_eq!(
21513 e.next_scroll_position,
21514 NextScrollCursorCenterTopBottom::Center,
21515 "Default next scroll direction is center",
21516 );
21517
21518 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21519 assert_eq!(
21520 e.next_scroll_position,
21521 NextScrollCursorCenterTopBottom::Top,
21522 "After center, next scroll direction should be top",
21523 );
21524
21525 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21526 assert_eq!(
21527 e.next_scroll_position,
21528 NextScrollCursorCenterTopBottom::Bottom,
21529 "After top, next scroll direction should be bottom",
21530 );
21531
21532 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21533 assert_eq!(
21534 e.next_scroll_position,
21535 NextScrollCursorCenterTopBottom::Center,
21536 "After bottom, scrolling should start over",
21537 );
21538
21539 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21540 assert_eq!(
21541 e.next_scroll_position,
21542 NextScrollCursorCenterTopBottom::Top,
21543 "Scrolling continues if retriggered fast enough"
21544 );
21545 });
21546
21547 cx.executor()
21548 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21549 cx.executor().run_until_parked();
21550 cx.update_editor(|e, _, _| {
21551 assert_eq!(
21552 e.next_scroll_position,
21553 NextScrollCursorCenterTopBottom::Center,
21554 "If scrolling is not triggered fast enough, it should reset"
21555 );
21556 });
21557}
21558
21559#[gpui::test]
21560async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21561 init_test(cx, |_| {});
21562 let mut cx = EditorLspTestContext::new_rust(
21563 lsp::ServerCapabilities {
21564 definition_provider: Some(lsp::OneOf::Left(true)),
21565 references_provider: Some(lsp::OneOf::Left(true)),
21566 ..lsp::ServerCapabilities::default()
21567 },
21568 cx,
21569 )
21570 .await;
21571
21572 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21573 let go_to_definition = cx
21574 .lsp
21575 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21576 move |params, _| async move {
21577 if empty_go_to_definition {
21578 Ok(None)
21579 } else {
21580 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21581 uri: params.text_document_position_params.text_document.uri,
21582 range: lsp::Range::new(
21583 lsp::Position::new(4, 3),
21584 lsp::Position::new(4, 6),
21585 ),
21586 })))
21587 }
21588 },
21589 );
21590 let references = cx
21591 .lsp
21592 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21593 Ok(Some(vec![lsp::Location {
21594 uri: params.text_document_position.text_document.uri,
21595 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21596 }]))
21597 });
21598 (go_to_definition, references)
21599 };
21600
21601 cx.set_state(
21602 &r#"fn one() {
21603 let mut a = ˇtwo();
21604 }
21605
21606 fn two() {}"#
21607 .unindent(),
21608 );
21609 set_up_lsp_handlers(false, &mut cx);
21610 let navigated = cx
21611 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21612 .await
21613 .expect("Failed to navigate to definition");
21614 assert_eq!(
21615 navigated,
21616 Navigated::Yes,
21617 "Should have navigated to definition from the GetDefinition response"
21618 );
21619 cx.assert_editor_state(
21620 &r#"fn one() {
21621 let mut a = two();
21622 }
21623
21624 fn «twoˇ»() {}"#
21625 .unindent(),
21626 );
21627
21628 let editors = cx.update_workspace(|workspace, _, cx| {
21629 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21630 });
21631 cx.update_editor(|_, _, test_editor_cx| {
21632 assert_eq!(
21633 editors.len(),
21634 1,
21635 "Initially, only one, test, editor should be open in the workspace"
21636 );
21637 assert_eq!(
21638 test_editor_cx.entity(),
21639 editors.last().expect("Asserted len is 1").clone()
21640 );
21641 });
21642
21643 set_up_lsp_handlers(true, &mut cx);
21644 let navigated = cx
21645 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21646 .await
21647 .expect("Failed to navigate to lookup references");
21648 assert_eq!(
21649 navigated,
21650 Navigated::Yes,
21651 "Should have navigated to references as a fallback after empty GoToDefinition response"
21652 );
21653 // We should not change the selections in the existing file,
21654 // if opening another milti buffer with the references
21655 cx.assert_editor_state(
21656 &r#"fn one() {
21657 let mut a = two();
21658 }
21659
21660 fn «twoˇ»() {}"#
21661 .unindent(),
21662 );
21663 let editors = cx.update_workspace(|workspace, _, cx| {
21664 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21665 });
21666 cx.update_editor(|_, _, test_editor_cx| {
21667 assert_eq!(
21668 editors.len(),
21669 2,
21670 "After falling back to references search, we open a new editor with the results"
21671 );
21672 let references_fallback_text = editors
21673 .into_iter()
21674 .find(|new_editor| *new_editor != test_editor_cx.entity())
21675 .expect("Should have one non-test editor now")
21676 .read(test_editor_cx)
21677 .text(test_editor_cx);
21678 assert_eq!(
21679 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21680 "Should use the range from the references response and not the GoToDefinition one"
21681 );
21682 });
21683}
21684
21685#[gpui::test]
21686async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21687 init_test(cx, |_| {});
21688 cx.update(|cx| {
21689 let mut editor_settings = EditorSettings::get_global(cx).clone();
21690 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21691 EditorSettings::override_global(editor_settings, cx);
21692 });
21693 let mut cx = EditorLspTestContext::new_rust(
21694 lsp::ServerCapabilities {
21695 definition_provider: Some(lsp::OneOf::Left(true)),
21696 references_provider: Some(lsp::OneOf::Left(true)),
21697 ..lsp::ServerCapabilities::default()
21698 },
21699 cx,
21700 )
21701 .await;
21702 let original_state = r#"fn one() {
21703 let mut a = ˇtwo();
21704 }
21705
21706 fn two() {}"#
21707 .unindent();
21708 cx.set_state(&original_state);
21709
21710 let mut go_to_definition = cx
21711 .lsp
21712 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21713 move |_, _| async move { Ok(None) },
21714 );
21715 let _references = cx
21716 .lsp
21717 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21718 panic!("Should not call for references with no go to definition fallback")
21719 });
21720
21721 let navigated = cx
21722 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21723 .await
21724 .expect("Failed to navigate to lookup references");
21725 go_to_definition
21726 .next()
21727 .await
21728 .expect("Should have called the go_to_definition handler");
21729
21730 assert_eq!(
21731 navigated,
21732 Navigated::No,
21733 "Should have navigated to references as a fallback after empty GoToDefinition response"
21734 );
21735 cx.assert_editor_state(&original_state);
21736 let editors = cx.update_workspace(|workspace, _, cx| {
21737 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21738 });
21739 cx.update_editor(|_, _, _| {
21740 assert_eq!(
21741 editors.len(),
21742 1,
21743 "After unsuccessful fallback, no other editor should have been opened"
21744 );
21745 });
21746}
21747
21748#[gpui::test]
21749async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21750 init_test(cx, |_| {});
21751 let mut cx = EditorLspTestContext::new_rust(
21752 lsp::ServerCapabilities {
21753 references_provider: Some(lsp::OneOf::Left(true)),
21754 ..lsp::ServerCapabilities::default()
21755 },
21756 cx,
21757 )
21758 .await;
21759
21760 cx.set_state(
21761 &r#"
21762 fn one() {
21763 let mut a = two();
21764 }
21765
21766 fn ˇtwo() {}"#
21767 .unindent(),
21768 );
21769 cx.lsp
21770 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21771 Ok(Some(vec![
21772 lsp::Location {
21773 uri: params.text_document_position.text_document.uri.clone(),
21774 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21775 },
21776 lsp::Location {
21777 uri: params.text_document_position.text_document.uri,
21778 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21779 },
21780 ]))
21781 });
21782 let navigated = cx
21783 .update_editor(|editor, window, cx| {
21784 editor.find_all_references(&FindAllReferences, window, cx)
21785 })
21786 .unwrap()
21787 .await
21788 .expect("Failed to navigate to references");
21789 assert_eq!(
21790 navigated,
21791 Navigated::Yes,
21792 "Should have navigated to references from the FindAllReferences response"
21793 );
21794 cx.assert_editor_state(
21795 &r#"fn one() {
21796 let mut a = two();
21797 }
21798
21799 fn ˇtwo() {}"#
21800 .unindent(),
21801 );
21802
21803 let editors = cx.update_workspace(|workspace, _, cx| {
21804 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21805 });
21806 cx.update_editor(|_, _, _| {
21807 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21808 });
21809
21810 cx.set_state(
21811 &r#"fn one() {
21812 let mut a = ˇtwo();
21813 }
21814
21815 fn two() {}"#
21816 .unindent(),
21817 );
21818 let navigated = cx
21819 .update_editor(|editor, window, cx| {
21820 editor.find_all_references(&FindAllReferences, window, cx)
21821 })
21822 .unwrap()
21823 .await
21824 .expect("Failed to navigate to references");
21825 assert_eq!(
21826 navigated,
21827 Navigated::Yes,
21828 "Should have navigated to references from the FindAllReferences response"
21829 );
21830 cx.assert_editor_state(
21831 &r#"fn one() {
21832 let mut a = ˇtwo();
21833 }
21834
21835 fn two() {}"#
21836 .unindent(),
21837 );
21838 let editors = cx.update_workspace(|workspace, _, cx| {
21839 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21840 });
21841 cx.update_editor(|_, _, _| {
21842 assert_eq!(
21843 editors.len(),
21844 2,
21845 "should have re-used the previous multibuffer"
21846 );
21847 });
21848
21849 cx.set_state(
21850 &r#"fn one() {
21851 let mut a = ˇtwo();
21852 }
21853 fn three() {}
21854 fn two() {}"#
21855 .unindent(),
21856 );
21857 cx.lsp
21858 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21859 Ok(Some(vec![
21860 lsp::Location {
21861 uri: params.text_document_position.text_document.uri.clone(),
21862 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21863 },
21864 lsp::Location {
21865 uri: params.text_document_position.text_document.uri,
21866 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21867 },
21868 ]))
21869 });
21870 let navigated = cx
21871 .update_editor(|editor, window, cx| {
21872 editor.find_all_references(&FindAllReferences, window, cx)
21873 })
21874 .unwrap()
21875 .await
21876 .expect("Failed to navigate to references");
21877 assert_eq!(
21878 navigated,
21879 Navigated::Yes,
21880 "Should have navigated to references from the FindAllReferences response"
21881 );
21882 cx.assert_editor_state(
21883 &r#"fn one() {
21884 let mut a = ˇtwo();
21885 }
21886 fn three() {}
21887 fn two() {}"#
21888 .unindent(),
21889 );
21890 let editors = cx.update_workspace(|workspace, _, cx| {
21891 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21892 });
21893 cx.update_editor(|_, _, _| {
21894 assert_eq!(
21895 editors.len(),
21896 3,
21897 "should have used a new multibuffer as offsets changed"
21898 );
21899 });
21900}
21901#[gpui::test]
21902async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21903 init_test(cx, |_| {});
21904
21905 let language = Arc::new(Language::new(
21906 LanguageConfig::default(),
21907 Some(tree_sitter_rust::LANGUAGE.into()),
21908 ));
21909
21910 let text = r#"
21911 #[cfg(test)]
21912 mod tests() {
21913 #[test]
21914 fn runnable_1() {
21915 let a = 1;
21916 }
21917
21918 #[test]
21919 fn runnable_2() {
21920 let a = 1;
21921 let b = 2;
21922 }
21923 }
21924 "#
21925 .unindent();
21926
21927 let fs = FakeFs::new(cx.executor());
21928 fs.insert_file("/file.rs", Default::default()).await;
21929
21930 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21931 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21932 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21933 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21934 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21935
21936 let editor = cx.new_window_entity(|window, cx| {
21937 Editor::new(
21938 EditorMode::full(),
21939 multi_buffer,
21940 Some(project.clone()),
21941 window,
21942 cx,
21943 )
21944 });
21945
21946 editor.update_in(cx, |editor, window, cx| {
21947 let snapshot = editor.buffer().read(cx).snapshot(cx);
21948 editor.tasks.insert(
21949 (buffer.read(cx).remote_id(), 3),
21950 RunnableTasks {
21951 templates: vec![],
21952 offset: snapshot.anchor_before(43),
21953 column: 0,
21954 extra_variables: HashMap::default(),
21955 context_range: BufferOffset(43)..BufferOffset(85),
21956 },
21957 );
21958 editor.tasks.insert(
21959 (buffer.read(cx).remote_id(), 8),
21960 RunnableTasks {
21961 templates: vec![],
21962 offset: snapshot.anchor_before(86),
21963 column: 0,
21964 extra_variables: HashMap::default(),
21965 context_range: BufferOffset(86)..BufferOffset(191),
21966 },
21967 );
21968
21969 // Test finding task when cursor is inside function body
21970 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21971 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21972 });
21973 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21974 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21975
21976 // Test finding task when cursor is on function name
21977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21978 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21979 });
21980 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21981 assert_eq!(row, 8, "Should find task when cursor is on function name");
21982 });
21983}
21984
21985#[gpui::test]
21986async fn test_folding_buffers(cx: &mut TestAppContext) {
21987 init_test(cx, |_| {});
21988
21989 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21990 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21991 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21992
21993 let fs = FakeFs::new(cx.executor());
21994 fs.insert_tree(
21995 path!("/a"),
21996 json!({
21997 "first.rs": sample_text_1,
21998 "second.rs": sample_text_2,
21999 "third.rs": sample_text_3,
22000 }),
22001 )
22002 .await;
22003 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22004 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22005 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22006 let worktree = project.update(cx, |project, cx| {
22007 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22008 assert_eq!(worktrees.len(), 1);
22009 worktrees.pop().unwrap()
22010 });
22011 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22012
22013 let buffer_1 = project
22014 .update(cx, |project, cx| {
22015 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22016 })
22017 .await
22018 .unwrap();
22019 let buffer_2 = project
22020 .update(cx, |project, cx| {
22021 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22022 })
22023 .await
22024 .unwrap();
22025 let buffer_3 = project
22026 .update(cx, |project, cx| {
22027 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22028 })
22029 .await
22030 .unwrap();
22031
22032 let multi_buffer = cx.new(|cx| {
22033 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22034 multi_buffer.push_excerpts(
22035 buffer_1.clone(),
22036 [
22037 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22038 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22039 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22040 ],
22041 cx,
22042 );
22043 multi_buffer.push_excerpts(
22044 buffer_2.clone(),
22045 [
22046 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22047 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22048 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22049 ],
22050 cx,
22051 );
22052 multi_buffer.push_excerpts(
22053 buffer_3.clone(),
22054 [
22055 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22056 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22057 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22058 ],
22059 cx,
22060 );
22061 multi_buffer
22062 });
22063 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22064 Editor::new(
22065 EditorMode::full(),
22066 multi_buffer.clone(),
22067 Some(project.clone()),
22068 window,
22069 cx,
22070 )
22071 });
22072
22073 assert_eq!(
22074 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22075 "\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",
22076 );
22077
22078 multi_buffer_editor.update(cx, |editor, cx| {
22079 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22080 });
22081 assert_eq!(
22082 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22083 "\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",
22084 "After folding the first buffer, its text should not be displayed"
22085 );
22086
22087 multi_buffer_editor.update(cx, |editor, cx| {
22088 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22089 });
22090 assert_eq!(
22091 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22092 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22093 "After folding the second buffer, its text should not be displayed"
22094 );
22095
22096 multi_buffer_editor.update(cx, |editor, cx| {
22097 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22098 });
22099 assert_eq!(
22100 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22101 "\n\n\n\n\n",
22102 "After folding the third buffer, its text should not be displayed"
22103 );
22104
22105 // Emulate selection inside the fold logic, that should work
22106 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22107 editor
22108 .snapshot(window, cx)
22109 .next_line_boundary(Point::new(0, 4));
22110 });
22111
22112 multi_buffer_editor.update(cx, |editor, cx| {
22113 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22114 });
22115 assert_eq!(
22116 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22117 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22118 "After unfolding the second buffer, its text should be displayed"
22119 );
22120
22121 // Typing inside of buffer 1 causes that buffer to be unfolded.
22122 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22123 assert_eq!(
22124 multi_buffer
22125 .read(cx)
22126 .snapshot(cx)
22127 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22128 .collect::<String>(),
22129 "bbbb"
22130 );
22131 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22132 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22133 });
22134 editor.handle_input("B", window, cx);
22135 });
22136
22137 assert_eq!(
22138 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22139 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22140 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22141 );
22142
22143 multi_buffer_editor.update(cx, |editor, cx| {
22144 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22145 });
22146 assert_eq!(
22147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22148 "\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",
22149 "After unfolding the all buffers, all original text should be displayed"
22150 );
22151}
22152
22153#[gpui::test]
22154async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22155 init_test(cx, |_| {});
22156
22157 let sample_text_1 = "1111\n2222\n3333".to_string();
22158 let sample_text_2 = "4444\n5555\n6666".to_string();
22159 let sample_text_3 = "7777\n8888\n9999".to_string();
22160
22161 let fs = FakeFs::new(cx.executor());
22162 fs.insert_tree(
22163 path!("/a"),
22164 json!({
22165 "first.rs": sample_text_1,
22166 "second.rs": sample_text_2,
22167 "third.rs": sample_text_3,
22168 }),
22169 )
22170 .await;
22171 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22172 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22173 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22174 let worktree = project.update(cx, |project, cx| {
22175 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22176 assert_eq!(worktrees.len(), 1);
22177 worktrees.pop().unwrap()
22178 });
22179 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22180
22181 let buffer_1 = project
22182 .update(cx, |project, cx| {
22183 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22184 })
22185 .await
22186 .unwrap();
22187 let buffer_2 = project
22188 .update(cx, |project, cx| {
22189 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22190 })
22191 .await
22192 .unwrap();
22193 let buffer_3 = project
22194 .update(cx, |project, cx| {
22195 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22196 })
22197 .await
22198 .unwrap();
22199
22200 let multi_buffer = cx.new(|cx| {
22201 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22202 multi_buffer.push_excerpts(
22203 buffer_1.clone(),
22204 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22205 cx,
22206 );
22207 multi_buffer.push_excerpts(
22208 buffer_2.clone(),
22209 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22210 cx,
22211 );
22212 multi_buffer.push_excerpts(
22213 buffer_3.clone(),
22214 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22215 cx,
22216 );
22217 multi_buffer
22218 });
22219
22220 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22221 Editor::new(
22222 EditorMode::full(),
22223 multi_buffer,
22224 Some(project.clone()),
22225 window,
22226 cx,
22227 )
22228 });
22229
22230 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22231 assert_eq!(
22232 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22233 full_text,
22234 );
22235
22236 multi_buffer_editor.update(cx, |editor, cx| {
22237 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22238 });
22239 assert_eq!(
22240 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22241 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22242 "After folding the first buffer, its text should not be displayed"
22243 );
22244
22245 multi_buffer_editor.update(cx, |editor, cx| {
22246 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22247 });
22248
22249 assert_eq!(
22250 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22251 "\n\n\n\n\n\n7777\n8888\n9999",
22252 "After folding the second buffer, its text should not be displayed"
22253 );
22254
22255 multi_buffer_editor.update(cx, |editor, cx| {
22256 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22257 });
22258 assert_eq!(
22259 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22260 "\n\n\n\n\n",
22261 "After folding the third buffer, its text should not be displayed"
22262 );
22263
22264 multi_buffer_editor.update(cx, |editor, cx| {
22265 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22266 });
22267 assert_eq!(
22268 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22269 "\n\n\n\n4444\n5555\n6666\n\n",
22270 "After unfolding the second buffer, its text should be displayed"
22271 );
22272
22273 multi_buffer_editor.update(cx, |editor, cx| {
22274 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22275 });
22276 assert_eq!(
22277 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22278 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22279 "After unfolding the first buffer, its text should be displayed"
22280 );
22281
22282 multi_buffer_editor.update(cx, |editor, cx| {
22283 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22284 });
22285 assert_eq!(
22286 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22287 full_text,
22288 "After unfolding all buffers, all original text should be displayed"
22289 );
22290}
22291
22292#[gpui::test]
22293async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22294 init_test(cx, |_| {});
22295
22296 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22297
22298 let fs = FakeFs::new(cx.executor());
22299 fs.insert_tree(
22300 path!("/a"),
22301 json!({
22302 "main.rs": sample_text,
22303 }),
22304 )
22305 .await;
22306 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22307 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22308 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22309 let worktree = project.update(cx, |project, cx| {
22310 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22311 assert_eq!(worktrees.len(), 1);
22312 worktrees.pop().unwrap()
22313 });
22314 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22315
22316 let buffer_1 = project
22317 .update(cx, |project, cx| {
22318 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22319 })
22320 .await
22321 .unwrap();
22322
22323 let multi_buffer = cx.new(|cx| {
22324 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22325 multi_buffer.push_excerpts(
22326 buffer_1.clone(),
22327 [ExcerptRange::new(
22328 Point::new(0, 0)
22329 ..Point::new(
22330 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22331 0,
22332 ),
22333 )],
22334 cx,
22335 );
22336 multi_buffer
22337 });
22338 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22339 Editor::new(
22340 EditorMode::full(),
22341 multi_buffer,
22342 Some(project.clone()),
22343 window,
22344 cx,
22345 )
22346 });
22347
22348 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22349 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22350 enum TestHighlight {}
22351 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22352 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22353 editor.highlight_text::<TestHighlight>(
22354 vec![highlight_range.clone()],
22355 HighlightStyle::color(Hsla::green()),
22356 cx,
22357 );
22358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22359 s.select_ranges(Some(highlight_range))
22360 });
22361 });
22362
22363 let full_text = format!("\n\n{sample_text}");
22364 assert_eq!(
22365 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22366 full_text,
22367 );
22368}
22369
22370#[gpui::test]
22371async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22372 init_test(cx, |_| {});
22373 cx.update(|cx| {
22374 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22375 "keymaps/default-linux.json",
22376 cx,
22377 )
22378 .unwrap();
22379 cx.bind_keys(default_key_bindings);
22380 });
22381
22382 let (editor, cx) = cx.add_window_view(|window, cx| {
22383 let multi_buffer = MultiBuffer::build_multi(
22384 [
22385 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22386 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22387 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22388 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22389 ],
22390 cx,
22391 );
22392 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22393
22394 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22395 // fold all but the second buffer, so that we test navigating between two
22396 // adjacent folded buffers, as well as folded buffers at the start and
22397 // end the multibuffer
22398 editor.fold_buffer(buffer_ids[0], cx);
22399 editor.fold_buffer(buffer_ids[2], cx);
22400 editor.fold_buffer(buffer_ids[3], cx);
22401
22402 editor
22403 });
22404 cx.simulate_resize(size(px(1000.), px(1000.)));
22405
22406 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22407 cx.assert_excerpts_with_selections(indoc! {"
22408 [EXCERPT]
22409 ˇ[FOLDED]
22410 [EXCERPT]
22411 a1
22412 b1
22413 [EXCERPT]
22414 [FOLDED]
22415 [EXCERPT]
22416 [FOLDED]
22417 "
22418 });
22419 cx.simulate_keystroke("down");
22420 cx.assert_excerpts_with_selections(indoc! {"
22421 [EXCERPT]
22422 [FOLDED]
22423 [EXCERPT]
22424 ˇa1
22425 b1
22426 [EXCERPT]
22427 [FOLDED]
22428 [EXCERPT]
22429 [FOLDED]
22430 "
22431 });
22432 cx.simulate_keystroke("down");
22433 cx.assert_excerpts_with_selections(indoc! {"
22434 [EXCERPT]
22435 [FOLDED]
22436 [EXCERPT]
22437 a1
22438 ˇb1
22439 [EXCERPT]
22440 [FOLDED]
22441 [EXCERPT]
22442 [FOLDED]
22443 "
22444 });
22445 cx.simulate_keystroke("down");
22446 cx.assert_excerpts_with_selections(indoc! {"
22447 [EXCERPT]
22448 [FOLDED]
22449 [EXCERPT]
22450 a1
22451 b1
22452 ˇ[EXCERPT]
22453 [FOLDED]
22454 [EXCERPT]
22455 [FOLDED]
22456 "
22457 });
22458 cx.simulate_keystroke("down");
22459 cx.assert_excerpts_with_selections(indoc! {"
22460 [EXCERPT]
22461 [FOLDED]
22462 [EXCERPT]
22463 a1
22464 b1
22465 [EXCERPT]
22466 ˇ[FOLDED]
22467 [EXCERPT]
22468 [FOLDED]
22469 "
22470 });
22471 for _ in 0..5 {
22472 cx.simulate_keystroke("down");
22473 cx.assert_excerpts_with_selections(indoc! {"
22474 [EXCERPT]
22475 [FOLDED]
22476 [EXCERPT]
22477 a1
22478 b1
22479 [EXCERPT]
22480 [FOLDED]
22481 [EXCERPT]
22482 ˇ[FOLDED]
22483 "
22484 });
22485 }
22486
22487 cx.simulate_keystroke("up");
22488 cx.assert_excerpts_with_selections(indoc! {"
22489 [EXCERPT]
22490 [FOLDED]
22491 [EXCERPT]
22492 a1
22493 b1
22494 [EXCERPT]
22495 ˇ[FOLDED]
22496 [EXCERPT]
22497 [FOLDED]
22498 "
22499 });
22500 cx.simulate_keystroke("up");
22501 cx.assert_excerpts_with_selections(indoc! {"
22502 [EXCERPT]
22503 [FOLDED]
22504 [EXCERPT]
22505 a1
22506 b1
22507 ˇ[EXCERPT]
22508 [FOLDED]
22509 [EXCERPT]
22510 [FOLDED]
22511 "
22512 });
22513 cx.simulate_keystroke("up");
22514 cx.assert_excerpts_with_selections(indoc! {"
22515 [EXCERPT]
22516 [FOLDED]
22517 [EXCERPT]
22518 a1
22519 ˇb1
22520 [EXCERPT]
22521 [FOLDED]
22522 [EXCERPT]
22523 [FOLDED]
22524 "
22525 });
22526 cx.simulate_keystroke("up");
22527 cx.assert_excerpts_with_selections(indoc! {"
22528 [EXCERPT]
22529 [FOLDED]
22530 [EXCERPT]
22531 ˇa1
22532 b1
22533 [EXCERPT]
22534 [FOLDED]
22535 [EXCERPT]
22536 [FOLDED]
22537 "
22538 });
22539 for _ in 0..5 {
22540 cx.simulate_keystroke("up");
22541 cx.assert_excerpts_with_selections(indoc! {"
22542 [EXCERPT]
22543 ˇ[FOLDED]
22544 [EXCERPT]
22545 a1
22546 b1
22547 [EXCERPT]
22548 [FOLDED]
22549 [EXCERPT]
22550 [FOLDED]
22551 "
22552 });
22553 }
22554}
22555
22556#[gpui::test]
22557async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22558 init_test(cx, |_| {});
22559
22560 // Simple insertion
22561 assert_highlighted_edits(
22562 "Hello, world!",
22563 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22564 true,
22565 cx,
22566 |highlighted_edits, cx| {
22567 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22568 assert_eq!(highlighted_edits.highlights.len(), 1);
22569 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22570 assert_eq!(
22571 highlighted_edits.highlights[0].1.background_color,
22572 Some(cx.theme().status().created_background)
22573 );
22574 },
22575 )
22576 .await;
22577
22578 // Replacement
22579 assert_highlighted_edits(
22580 "This is a test.",
22581 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22582 false,
22583 cx,
22584 |highlighted_edits, cx| {
22585 assert_eq!(highlighted_edits.text, "That is a test.");
22586 assert_eq!(highlighted_edits.highlights.len(), 1);
22587 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22588 assert_eq!(
22589 highlighted_edits.highlights[0].1.background_color,
22590 Some(cx.theme().status().created_background)
22591 );
22592 },
22593 )
22594 .await;
22595
22596 // Multiple edits
22597 assert_highlighted_edits(
22598 "Hello, world!",
22599 vec![
22600 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22601 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22602 ],
22603 false,
22604 cx,
22605 |highlighted_edits, cx| {
22606 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22607 assert_eq!(highlighted_edits.highlights.len(), 2);
22608 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22609 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22610 assert_eq!(
22611 highlighted_edits.highlights[0].1.background_color,
22612 Some(cx.theme().status().created_background)
22613 );
22614 assert_eq!(
22615 highlighted_edits.highlights[1].1.background_color,
22616 Some(cx.theme().status().created_background)
22617 );
22618 },
22619 )
22620 .await;
22621
22622 // Multiple lines with edits
22623 assert_highlighted_edits(
22624 "First line\nSecond line\nThird line\nFourth line",
22625 vec![
22626 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22627 (
22628 Point::new(2, 0)..Point::new(2, 10),
22629 "New third line".to_string(),
22630 ),
22631 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22632 ],
22633 false,
22634 cx,
22635 |highlighted_edits, cx| {
22636 assert_eq!(
22637 highlighted_edits.text,
22638 "Second modified\nNew third line\nFourth updated line"
22639 );
22640 assert_eq!(highlighted_edits.highlights.len(), 3);
22641 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22642 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22643 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22644 for highlight in &highlighted_edits.highlights {
22645 assert_eq!(
22646 highlight.1.background_color,
22647 Some(cx.theme().status().created_background)
22648 );
22649 }
22650 },
22651 )
22652 .await;
22653}
22654
22655#[gpui::test]
22656async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22657 init_test(cx, |_| {});
22658
22659 // Deletion
22660 assert_highlighted_edits(
22661 "Hello, world!",
22662 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22663 true,
22664 cx,
22665 |highlighted_edits, cx| {
22666 assert_eq!(highlighted_edits.text, "Hello, world!");
22667 assert_eq!(highlighted_edits.highlights.len(), 1);
22668 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22669 assert_eq!(
22670 highlighted_edits.highlights[0].1.background_color,
22671 Some(cx.theme().status().deleted_background)
22672 );
22673 },
22674 )
22675 .await;
22676
22677 // Insertion
22678 assert_highlighted_edits(
22679 "Hello, world!",
22680 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22681 true,
22682 cx,
22683 |highlighted_edits, cx| {
22684 assert_eq!(highlighted_edits.highlights.len(), 1);
22685 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22686 assert_eq!(
22687 highlighted_edits.highlights[0].1.background_color,
22688 Some(cx.theme().status().created_background)
22689 );
22690 },
22691 )
22692 .await;
22693}
22694
22695async fn assert_highlighted_edits(
22696 text: &str,
22697 edits: Vec<(Range<Point>, String)>,
22698 include_deletions: bool,
22699 cx: &mut TestAppContext,
22700 assertion_fn: impl Fn(HighlightedText, &App),
22701) {
22702 let window = cx.add_window(|window, cx| {
22703 let buffer = MultiBuffer::build_simple(text, cx);
22704 Editor::new(EditorMode::full(), buffer, None, window, cx)
22705 });
22706 let cx = &mut VisualTestContext::from_window(*window, cx);
22707
22708 let (buffer, snapshot) = window
22709 .update(cx, |editor, _window, cx| {
22710 (
22711 editor.buffer().clone(),
22712 editor.buffer().read(cx).snapshot(cx),
22713 )
22714 })
22715 .unwrap();
22716
22717 let edits = edits
22718 .into_iter()
22719 .map(|(range, edit)| {
22720 (
22721 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22722 edit,
22723 )
22724 })
22725 .collect::<Vec<_>>();
22726
22727 let text_anchor_edits = edits
22728 .clone()
22729 .into_iter()
22730 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22731 .collect::<Vec<_>>();
22732
22733 let edit_preview = window
22734 .update(cx, |_, _window, cx| {
22735 buffer
22736 .read(cx)
22737 .as_singleton()
22738 .unwrap()
22739 .read(cx)
22740 .preview_edits(text_anchor_edits.into(), cx)
22741 })
22742 .unwrap()
22743 .await;
22744
22745 cx.update(|_window, cx| {
22746 let highlighted_edits = edit_prediction_edit_text(
22747 snapshot.as_singleton().unwrap().2,
22748 &edits,
22749 &edit_preview,
22750 include_deletions,
22751 cx,
22752 );
22753 assertion_fn(highlighted_edits, cx)
22754 });
22755}
22756
22757#[track_caller]
22758fn assert_breakpoint(
22759 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22760 path: &Arc<Path>,
22761 expected: Vec<(u32, Breakpoint)>,
22762) {
22763 if expected.is_empty() {
22764 assert!(!breakpoints.contains_key(path), "{}", path.display());
22765 } else {
22766 let mut breakpoint = breakpoints
22767 .get(path)
22768 .unwrap()
22769 .iter()
22770 .map(|breakpoint| {
22771 (
22772 breakpoint.row,
22773 Breakpoint {
22774 message: breakpoint.message.clone(),
22775 state: breakpoint.state,
22776 condition: breakpoint.condition.clone(),
22777 hit_condition: breakpoint.hit_condition.clone(),
22778 },
22779 )
22780 })
22781 .collect::<Vec<_>>();
22782
22783 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22784
22785 assert_eq!(expected, breakpoint);
22786 }
22787}
22788
22789fn add_log_breakpoint_at_cursor(
22790 editor: &mut Editor,
22791 log_message: &str,
22792 window: &mut Window,
22793 cx: &mut Context<Editor>,
22794) {
22795 let (anchor, bp) = editor
22796 .breakpoints_at_cursors(window, cx)
22797 .first()
22798 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22799 .unwrap_or_else(|| {
22800 let snapshot = editor.snapshot(window, cx);
22801 let cursor_position: Point =
22802 editor.selections.newest(&snapshot.display_snapshot).head();
22803
22804 let breakpoint_position = snapshot
22805 .buffer_snapshot()
22806 .anchor_before(Point::new(cursor_position.row, 0));
22807
22808 (breakpoint_position, Breakpoint::new_log(log_message))
22809 });
22810
22811 editor.edit_breakpoint_at_anchor(
22812 anchor,
22813 bp,
22814 BreakpointEditAction::EditLogMessage(log_message.into()),
22815 cx,
22816 );
22817}
22818
22819#[gpui::test]
22820async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22821 init_test(cx, |_| {});
22822
22823 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22824 let fs = FakeFs::new(cx.executor());
22825 fs.insert_tree(
22826 path!("/a"),
22827 json!({
22828 "main.rs": sample_text,
22829 }),
22830 )
22831 .await;
22832 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22833 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22834 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22835
22836 let fs = FakeFs::new(cx.executor());
22837 fs.insert_tree(
22838 path!("/a"),
22839 json!({
22840 "main.rs": sample_text,
22841 }),
22842 )
22843 .await;
22844 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22845 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22846 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22847 let worktree_id = workspace
22848 .update(cx, |workspace, _window, cx| {
22849 workspace.project().update(cx, |project, cx| {
22850 project.worktrees(cx).next().unwrap().read(cx).id()
22851 })
22852 })
22853 .unwrap();
22854
22855 let buffer = project
22856 .update(cx, |project, cx| {
22857 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22858 })
22859 .await
22860 .unwrap();
22861
22862 let (editor, cx) = cx.add_window_view(|window, cx| {
22863 Editor::new(
22864 EditorMode::full(),
22865 MultiBuffer::build_from_buffer(buffer, cx),
22866 Some(project.clone()),
22867 window,
22868 cx,
22869 )
22870 });
22871
22872 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22873 let abs_path = project.read_with(cx, |project, cx| {
22874 project
22875 .absolute_path(&project_path, cx)
22876 .map(Arc::from)
22877 .unwrap()
22878 });
22879
22880 // assert we can add breakpoint on the first line
22881 editor.update_in(cx, |editor, window, cx| {
22882 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22883 editor.move_to_end(&MoveToEnd, window, cx);
22884 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22885 });
22886
22887 let breakpoints = editor.update(cx, |editor, cx| {
22888 editor
22889 .breakpoint_store()
22890 .as_ref()
22891 .unwrap()
22892 .read(cx)
22893 .all_source_breakpoints(cx)
22894 });
22895
22896 assert_eq!(1, breakpoints.len());
22897 assert_breakpoint(
22898 &breakpoints,
22899 &abs_path,
22900 vec![
22901 (0, Breakpoint::new_standard()),
22902 (3, Breakpoint::new_standard()),
22903 ],
22904 );
22905
22906 editor.update_in(cx, |editor, window, cx| {
22907 editor.move_to_beginning(&MoveToBeginning, window, cx);
22908 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22909 });
22910
22911 let breakpoints = editor.update(cx, |editor, cx| {
22912 editor
22913 .breakpoint_store()
22914 .as_ref()
22915 .unwrap()
22916 .read(cx)
22917 .all_source_breakpoints(cx)
22918 });
22919
22920 assert_eq!(1, breakpoints.len());
22921 assert_breakpoint(
22922 &breakpoints,
22923 &abs_path,
22924 vec![(3, Breakpoint::new_standard())],
22925 );
22926
22927 editor.update_in(cx, |editor, window, cx| {
22928 editor.move_to_end(&MoveToEnd, window, cx);
22929 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22930 });
22931
22932 let breakpoints = editor.update(cx, |editor, cx| {
22933 editor
22934 .breakpoint_store()
22935 .as_ref()
22936 .unwrap()
22937 .read(cx)
22938 .all_source_breakpoints(cx)
22939 });
22940
22941 assert_eq!(0, breakpoints.len());
22942 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22943}
22944
22945#[gpui::test]
22946async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22947 init_test(cx, |_| {});
22948
22949 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22950
22951 let fs = FakeFs::new(cx.executor());
22952 fs.insert_tree(
22953 path!("/a"),
22954 json!({
22955 "main.rs": sample_text,
22956 }),
22957 )
22958 .await;
22959 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22960 let (workspace, cx) =
22961 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22962
22963 let worktree_id = workspace.update(cx, |workspace, cx| {
22964 workspace.project().update(cx, |project, cx| {
22965 project.worktrees(cx).next().unwrap().read(cx).id()
22966 })
22967 });
22968
22969 let buffer = project
22970 .update(cx, |project, cx| {
22971 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22972 })
22973 .await
22974 .unwrap();
22975
22976 let (editor, cx) = cx.add_window_view(|window, cx| {
22977 Editor::new(
22978 EditorMode::full(),
22979 MultiBuffer::build_from_buffer(buffer, cx),
22980 Some(project.clone()),
22981 window,
22982 cx,
22983 )
22984 });
22985
22986 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22987 let abs_path = project.read_with(cx, |project, cx| {
22988 project
22989 .absolute_path(&project_path, cx)
22990 .map(Arc::from)
22991 .unwrap()
22992 });
22993
22994 editor.update_in(cx, |editor, window, cx| {
22995 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22996 });
22997
22998 let breakpoints = editor.update(cx, |editor, cx| {
22999 editor
23000 .breakpoint_store()
23001 .as_ref()
23002 .unwrap()
23003 .read(cx)
23004 .all_source_breakpoints(cx)
23005 });
23006
23007 assert_breakpoint(
23008 &breakpoints,
23009 &abs_path,
23010 vec![(0, Breakpoint::new_log("hello world"))],
23011 );
23012
23013 // Removing a log message from a log breakpoint should remove it
23014 editor.update_in(cx, |editor, window, cx| {
23015 add_log_breakpoint_at_cursor(editor, "", window, cx);
23016 });
23017
23018 let breakpoints = editor.update(cx, |editor, cx| {
23019 editor
23020 .breakpoint_store()
23021 .as_ref()
23022 .unwrap()
23023 .read(cx)
23024 .all_source_breakpoints(cx)
23025 });
23026
23027 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23028
23029 editor.update_in(cx, |editor, window, cx| {
23030 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23031 editor.move_to_end(&MoveToEnd, window, cx);
23032 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23033 // Not adding a log message to a standard breakpoint shouldn't remove it
23034 add_log_breakpoint_at_cursor(editor, "", window, cx);
23035 });
23036
23037 let breakpoints = editor.update(cx, |editor, cx| {
23038 editor
23039 .breakpoint_store()
23040 .as_ref()
23041 .unwrap()
23042 .read(cx)
23043 .all_source_breakpoints(cx)
23044 });
23045
23046 assert_breakpoint(
23047 &breakpoints,
23048 &abs_path,
23049 vec![
23050 (0, Breakpoint::new_standard()),
23051 (3, Breakpoint::new_standard()),
23052 ],
23053 );
23054
23055 editor.update_in(cx, |editor, window, cx| {
23056 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23057 });
23058
23059 let breakpoints = editor.update(cx, |editor, cx| {
23060 editor
23061 .breakpoint_store()
23062 .as_ref()
23063 .unwrap()
23064 .read(cx)
23065 .all_source_breakpoints(cx)
23066 });
23067
23068 assert_breakpoint(
23069 &breakpoints,
23070 &abs_path,
23071 vec![
23072 (0, Breakpoint::new_standard()),
23073 (3, Breakpoint::new_log("hello world")),
23074 ],
23075 );
23076
23077 editor.update_in(cx, |editor, window, cx| {
23078 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23079 });
23080
23081 let breakpoints = editor.update(cx, |editor, cx| {
23082 editor
23083 .breakpoint_store()
23084 .as_ref()
23085 .unwrap()
23086 .read(cx)
23087 .all_source_breakpoints(cx)
23088 });
23089
23090 assert_breakpoint(
23091 &breakpoints,
23092 &abs_path,
23093 vec![
23094 (0, Breakpoint::new_standard()),
23095 (3, Breakpoint::new_log("hello Earth!!")),
23096 ],
23097 );
23098}
23099
23100/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23101/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23102/// or when breakpoints were placed out of order. This tests for a regression too
23103#[gpui::test]
23104async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23105 init_test(cx, |_| {});
23106
23107 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23108 let fs = FakeFs::new(cx.executor());
23109 fs.insert_tree(
23110 path!("/a"),
23111 json!({
23112 "main.rs": sample_text,
23113 }),
23114 )
23115 .await;
23116 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23117 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23118 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23119
23120 let fs = FakeFs::new(cx.executor());
23121 fs.insert_tree(
23122 path!("/a"),
23123 json!({
23124 "main.rs": sample_text,
23125 }),
23126 )
23127 .await;
23128 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23129 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23130 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23131 let worktree_id = workspace
23132 .update(cx, |workspace, _window, cx| {
23133 workspace.project().update(cx, |project, cx| {
23134 project.worktrees(cx).next().unwrap().read(cx).id()
23135 })
23136 })
23137 .unwrap();
23138
23139 let buffer = project
23140 .update(cx, |project, cx| {
23141 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23142 })
23143 .await
23144 .unwrap();
23145
23146 let (editor, cx) = cx.add_window_view(|window, cx| {
23147 Editor::new(
23148 EditorMode::full(),
23149 MultiBuffer::build_from_buffer(buffer, cx),
23150 Some(project.clone()),
23151 window,
23152 cx,
23153 )
23154 });
23155
23156 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23157 let abs_path = project.read_with(cx, |project, cx| {
23158 project
23159 .absolute_path(&project_path, cx)
23160 .map(Arc::from)
23161 .unwrap()
23162 });
23163
23164 // assert we can add breakpoint on the first line
23165 editor.update_in(cx, |editor, window, cx| {
23166 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23167 editor.move_to_end(&MoveToEnd, window, cx);
23168 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23169 editor.move_up(&MoveUp, window, cx);
23170 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23171 });
23172
23173 let breakpoints = editor.update(cx, |editor, cx| {
23174 editor
23175 .breakpoint_store()
23176 .as_ref()
23177 .unwrap()
23178 .read(cx)
23179 .all_source_breakpoints(cx)
23180 });
23181
23182 assert_eq!(1, breakpoints.len());
23183 assert_breakpoint(
23184 &breakpoints,
23185 &abs_path,
23186 vec![
23187 (0, Breakpoint::new_standard()),
23188 (2, Breakpoint::new_standard()),
23189 (3, Breakpoint::new_standard()),
23190 ],
23191 );
23192
23193 editor.update_in(cx, |editor, window, cx| {
23194 editor.move_to_beginning(&MoveToBeginning, window, cx);
23195 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23196 editor.move_to_end(&MoveToEnd, window, cx);
23197 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23198 // Disabling a breakpoint that doesn't exist should do nothing
23199 editor.move_up(&MoveUp, window, cx);
23200 editor.move_up(&MoveUp, window, cx);
23201 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23202 });
23203
23204 let breakpoints = editor.update(cx, |editor, cx| {
23205 editor
23206 .breakpoint_store()
23207 .as_ref()
23208 .unwrap()
23209 .read(cx)
23210 .all_source_breakpoints(cx)
23211 });
23212
23213 let disable_breakpoint = {
23214 let mut bp = Breakpoint::new_standard();
23215 bp.state = BreakpointState::Disabled;
23216 bp
23217 };
23218
23219 assert_eq!(1, breakpoints.len());
23220 assert_breakpoint(
23221 &breakpoints,
23222 &abs_path,
23223 vec![
23224 (0, disable_breakpoint.clone()),
23225 (2, Breakpoint::new_standard()),
23226 (3, disable_breakpoint.clone()),
23227 ],
23228 );
23229
23230 editor.update_in(cx, |editor, window, cx| {
23231 editor.move_to_beginning(&MoveToBeginning, window, cx);
23232 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23233 editor.move_to_end(&MoveToEnd, window, cx);
23234 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23235 editor.move_up(&MoveUp, window, cx);
23236 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23237 });
23238
23239 let breakpoints = editor.update(cx, |editor, cx| {
23240 editor
23241 .breakpoint_store()
23242 .as_ref()
23243 .unwrap()
23244 .read(cx)
23245 .all_source_breakpoints(cx)
23246 });
23247
23248 assert_eq!(1, breakpoints.len());
23249 assert_breakpoint(
23250 &breakpoints,
23251 &abs_path,
23252 vec![
23253 (0, Breakpoint::new_standard()),
23254 (2, disable_breakpoint),
23255 (3, Breakpoint::new_standard()),
23256 ],
23257 );
23258}
23259
23260#[gpui::test]
23261async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23262 init_test(cx, |_| {});
23263 let capabilities = lsp::ServerCapabilities {
23264 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23265 prepare_provider: Some(true),
23266 work_done_progress_options: Default::default(),
23267 })),
23268 ..Default::default()
23269 };
23270 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23271
23272 cx.set_state(indoc! {"
23273 struct Fˇoo {}
23274 "});
23275
23276 cx.update_editor(|editor, _, cx| {
23277 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23278 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23279 editor.highlight_background::<DocumentHighlightRead>(
23280 &[highlight_range],
23281 |theme| theme.colors().editor_document_highlight_read_background,
23282 cx,
23283 );
23284 });
23285
23286 let mut prepare_rename_handler = cx
23287 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23288 move |_, _, _| async move {
23289 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23290 start: lsp::Position {
23291 line: 0,
23292 character: 7,
23293 },
23294 end: lsp::Position {
23295 line: 0,
23296 character: 10,
23297 },
23298 })))
23299 },
23300 );
23301 let prepare_rename_task = cx
23302 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23303 .expect("Prepare rename was not started");
23304 prepare_rename_handler.next().await.unwrap();
23305 prepare_rename_task.await.expect("Prepare rename failed");
23306
23307 let mut rename_handler =
23308 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23309 let edit = lsp::TextEdit {
23310 range: lsp::Range {
23311 start: lsp::Position {
23312 line: 0,
23313 character: 7,
23314 },
23315 end: lsp::Position {
23316 line: 0,
23317 character: 10,
23318 },
23319 },
23320 new_text: "FooRenamed".to_string(),
23321 };
23322 Ok(Some(lsp::WorkspaceEdit::new(
23323 // Specify the same edit twice
23324 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23325 )))
23326 });
23327 let rename_task = cx
23328 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23329 .expect("Confirm rename was not started");
23330 rename_handler.next().await.unwrap();
23331 rename_task.await.expect("Confirm rename failed");
23332 cx.run_until_parked();
23333
23334 // Despite two edits, only one is actually applied as those are identical
23335 cx.assert_editor_state(indoc! {"
23336 struct FooRenamedˇ {}
23337 "});
23338}
23339
23340#[gpui::test]
23341async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23342 init_test(cx, |_| {});
23343 // These capabilities indicate that the server does not support prepare rename.
23344 let capabilities = lsp::ServerCapabilities {
23345 rename_provider: Some(lsp::OneOf::Left(true)),
23346 ..Default::default()
23347 };
23348 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23349
23350 cx.set_state(indoc! {"
23351 struct Fˇoo {}
23352 "});
23353
23354 cx.update_editor(|editor, _window, cx| {
23355 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23356 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23357 editor.highlight_background::<DocumentHighlightRead>(
23358 &[highlight_range],
23359 |theme| theme.colors().editor_document_highlight_read_background,
23360 cx,
23361 );
23362 });
23363
23364 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23365 .expect("Prepare rename was not started")
23366 .await
23367 .expect("Prepare rename failed");
23368
23369 let mut rename_handler =
23370 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23371 let edit = lsp::TextEdit {
23372 range: lsp::Range {
23373 start: lsp::Position {
23374 line: 0,
23375 character: 7,
23376 },
23377 end: lsp::Position {
23378 line: 0,
23379 character: 10,
23380 },
23381 },
23382 new_text: "FooRenamed".to_string(),
23383 };
23384 Ok(Some(lsp::WorkspaceEdit::new(
23385 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23386 )))
23387 });
23388 let rename_task = cx
23389 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23390 .expect("Confirm rename was not started");
23391 rename_handler.next().await.unwrap();
23392 rename_task.await.expect("Confirm rename failed");
23393 cx.run_until_parked();
23394
23395 // Correct range is renamed, as `surrounding_word` is used to find it.
23396 cx.assert_editor_state(indoc! {"
23397 struct FooRenamedˇ {}
23398 "});
23399}
23400
23401#[gpui::test]
23402async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23403 init_test(cx, |_| {});
23404 let mut cx = EditorTestContext::new(cx).await;
23405
23406 let language = Arc::new(
23407 Language::new(
23408 LanguageConfig::default(),
23409 Some(tree_sitter_html::LANGUAGE.into()),
23410 )
23411 .with_brackets_query(
23412 r#"
23413 ("<" @open "/>" @close)
23414 ("</" @open ">" @close)
23415 ("<" @open ">" @close)
23416 ("\"" @open "\"" @close)
23417 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23418 "#,
23419 )
23420 .unwrap(),
23421 );
23422 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23423
23424 cx.set_state(indoc! {"
23425 <span>ˇ</span>
23426 "});
23427 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23428 cx.assert_editor_state(indoc! {"
23429 <span>
23430 ˇ
23431 </span>
23432 "});
23433
23434 cx.set_state(indoc! {"
23435 <span><span></span>ˇ</span>
23436 "});
23437 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23438 cx.assert_editor_state(indoc! {"
23439 <span><span></span>
23440 ˇ</span>
23441 "});
23442
23443 cx.set_state(indoc! {"
23444 <span>ˇ
23445 </span>
23446 "});
23447 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23448 cx.assert_editor_state(indoc! {"
23449 <span>
23450 ˇ
23451 </span>
23452 "});
23453}
23454
23455#[gpui::test(iterations = 10)]
23456async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23457 init_test(cx, |_| {});
23458
23459 let fs = FakeFs::new(cx.executor());
23460 fs.insert_tree(
23461 path!("/dir"),
23462 json!({
23463 "a.ts": "a",
23464 }),
23465 )
23466 .await;
23467
23468 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23469 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23470 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23471
23472 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23473 language_registry.add(Arc::new(Language::new(
23474 LanguageConfig {
23475 name: "TypeScript".into(),
23476 matcher: LanguageMatcher {
23477 path_suffixes: vec!["ts".to_string()],
23478 ..Default::default()
23479 },
23480 ..Default::default()
23481 },
23482 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23483 )));
23484 let mut fake_language_servers = language_registry.register_fake_lsp(
23485 "TypeScript",
23486 FakeLspAdapter {
23487 capabilities: lsp::ServerCapabilities {
23488 code_lens_provider: Some(lsp::CodeLensOptions {
23489 resolve_provider: Some(true),
23490 }),
23491 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23492 commands: vec!["_the/command".to_string()],
23493 ..lsp::ExecuteCommandOptions::default()
23494 }),
23495 ..lsp::ServerCapabilities::default()
23496 },
23497 ..FakeLspAdapter::default()
23498 },
23499 );
23500
23501 let editor = workspace
23502 .update(cx, |workspace, window, cx| {
23503 workspace.open_abs_path(
23504 PathBuf::from(path!("/dir/a.ts")),
23505 OpenOptions::default(),
23506 window,
23507 cx,
23508 )
23509 })
23510 .unwrap()
23511 .await
23512 .unwrap()
23513 .downcast::<Editor>()
23514 .unwrap();
23515 cx.executor().run_until_parked();
23516
23517 let fake_server = fake_language_servers.next().await.unwrap();
23518
23519 let buffer = editor.update(cx, |editor, cx| {
23520 editor
23521 .buffer()
23522 .read(cx)
23523 .as_singleton()
23524 .expect("have opened a single file by path")
23525 });
23526
23527 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23528 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23529 drop(buffer_snapshot);
23530 let actions = cx
23531 .update_window(*workspace, |_, window, cx| {
23532 project.code_actions(&buffer, anchor..anchor, window, cx)
23533 })
23534 .unwrap();
23535
23536 fake_server
23537 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23538 Ok(Some(vec![
23539 lsp::CodeLens {
23540 range: lsp::Range::default(),
23541 command: Some(lsp::Command {
23542 title: "Code lens command".to_owned(),
23543 command: "_the/command".to_owned(),
23544 arguments: None,
23545 }),
23546 data: None,
23547 },
23548 lsp::CodeLens {
23549 range: lsp::Range::default(),
23550 command: Some(lsp::Command {
23551 title: "Command not in capabilities".to_owned(),
23552 command: "not in capabilities".to_owned(),
23553 arguments: None,
23554 }),
23555 data: None,
23556 },
23557 lsp::CodeLens {
23558 range: lsp::Range {
23559 start: lsp::Position {
23560 line: 1,
23561 character: 1,
23562 },
23563 end: lsp::Position {
23564 line: 1,
23565 character: 1,
23566 },
23567 },
23568 command: Some(lsp::Command {
23569 title: "Command not in range".to_owned(),
23570 command: "_the/command".to_owned(),
23571 arguments: None,
23572 }),
23573 data: None,
23574 },
23575 ]))
23576 })
23577 .next()
23578 .await;
23579
23580 let actions = actions.await.unwrap();
23581 assert_eq!(
23582 actions.len(),
23583 1,
23584 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23585 );
23586 let action = actions[0].clone();
23587 let apply = project.update(cx, |project, cx| {
23588 project.apply_code_action(buffer.clone(), action, true, cx)
23589 });
23590
23591 // Resolving the code action does not populate its edits. In absence of
23592 // edits, we must execute the given command.
23593 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23594 |mut lens, _| async move {
23595 let lens_command = lens.command.as_mut().expect("should have a command");
23596 assert_eq!(lens_command.title, "Code lens command");
23597 lens_command.arguments = Some(vec![json!("the-argument")]);
23598 Ok(lens)
23599 },
23600 );
23601
23602 // While executing the command, the language server sends the editor
23603 // a `workspaceEdit` request.
23604 fake_server
23605 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23606 let fake = fake_server.clone();
23607 move |params, _| {
23608 assert_eq!(params.command, "_the/command");
23609 let fake = fake.clone();
23610 async move {
23611 fake.server
23612 .request::<lsp::request::ApplyWorkspaceEdit>(
23613 lsp::ApplyWorkspaceEditParams {
23614 label: None,
23615 edit: lsp::WorkspaceEdit {
23616 changes: Some(
23617 [(
23618 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23619 vec![lsp::TextEdit {
23620 range: lsp::Range::new(
23621 lsp::Position::new(0, 0),
23622 lsp::Position::new(0, 0),
23623 ),
23624 new_text: "X".into(),
23625 }],
23626 )]
23627 .into_iter()
23628 .collect(),
23629 ),
23630 ..lsp::WorkspaceEdit::default()
23631 },
23632 },
23633 )
23634 .await
23635 .into_response()
23636 .unwrap();
23637 Ok(Some(json!(null)))
23638 }
23639 }
23640 })
23641 .next()
23642 .await;
23643
23644 // Applying the code lens command returns a project transaction containing the edits
23645 // sent by the language server in its `workspaceEdit` request.
23646 let transaction = apply.await.unwrap();
23647 assert!(transaction.0.contains_key(&buffer));
23648 buffer.update(cx, |buffer, cx| {
23649 assert_eq!(buffer.text(), "Xa");
23650 buffer.undo(cx);
23651 assert_eq!(buffer.text(), "a");
23652 });
23653
23654 let actions_after_edits = cx
23655 .update_window(*workspace, |_, window, cx| {
23656 project.code_actions(&buffer, anchor..anchor, window, cx)
23657 })
23658 .unwrap()
23659 .await
23660 .unwrap();
23661 assert_eq!(
23662 actions, actions_after_edits,
23663 "For the same selection, same code lens actions should be returned"
23664 );
23665
23666 let _responses =
23667 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23668 panic!("No more code lens requests are expected");
23669 });
23670 editor.update_in(cx, |editor, window, cx| {
23671 editor.select_all(&SelectAll, window, cx);
23672 });
23673 cx.executor().run_until_parked();
23674 let new_actions = cx
23675 .update_window(*workspace, |_, window, cx| {
23676 project.code_actions(&buffer, anchor..anchor, window, cx)
23677 })
23678 .unwrap()
23679 .await
23680 .unwrap();
23681 assert_eq!(
23682 actions, new_actions,
23683 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23684 );
23685}
23686
23687#[gpui::test]
23688async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23689 init_test(cx, |_| {});
23690
23691 let fs = FakeFs::new(cx.executor());
23692 let main_text = r#"fn main() {
23693println!("1");
23694println!("2");
23695println!("3");
23696println!("4");
23697println!("5");
23698}"#;
23699 let lib_text = "mod foo {}";
23700 fs.insert_tree(
23701 path!("/a"),
23702 json!({
23703 "lib.rs": lib_text,
23704 "main.rs": main_text,
23705 }),
23706 )
23707 .await;
23708
23709 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23710 let (workspace, cx) =
23711 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23712 let worktree_id = workspace.update(cx, |workspace, cx| {
23713 workspace.project().update(cx, |project, cx| {
23714 project.worktrees(cx).next().unwrap().read(cx).id()
23715 })
23716 });
23717
23718 let expected_ranges = vec![
23719 Point::new(0, 0)..Point::new(0, 0),
23720 Point::new(1, 0)..Point::new(1, 1),
23721 Point::new(2, 0)..Point::new(2, 2),
23722 Point::new(3, 0)..Point::new(3, 3),
23723 ];
23724
23725 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23726 let editor_1 = workspace
23727 .update_in(cx, |workspace, window, cx| {
23728 workspace.open_path(
23729 (worktree_id, rel_path("main.rs")),
23730 Some(pane_1.downgrade()),
23731 true,
23732 window,
23733 cx,
23734 )
23735 })
23736 .unwrap()
23737 .await
23738 .downcast::<Editor>()
23739 .unwrap();
23740 pane_1.update(cx, |pane, cx| {
23741 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23742 open_editor.update(cx, |editor, cx| {
23743 assert_eq!(
23744 editor.display_text(cx),
23745 main_text,
23746 "Original main.rs text on initial open",
23747 );
23748 assert_eq!(
23749 editor
23750 .selections
23751 .all::<Point>(&editor.display_snapshot(cx))
23752 .into_iter()
23753 .map(|s| s.range())
23754 .collect::<Vec<_>>(),
23755 vec![Point::zero()..Point::zero()],
23756 "Default selections on initial open",
23757 );
23758 })
23759 });
23760 editor_1.update_in(cx, |editor, window, cx| {
23761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23762 s.select_ranges(expected_ranges.clone());
23763 });
23764 });
23765
23766 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23767 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23768 });
23769 let editor_2 = workspace
23770 .update_in(cx, |workspace, window, cx| {
23771 workspace.open_path(
23772 (worktree_id, rel_path("main.rs")),
23773 Some(pane_2.downgrade()),
23774 true,
23775 window,
23776 cx,
23777 )
23778 })
23779 .unwrap()
23780 .await
23781 .downcast::<Editor>()
23782 .unwrap();
23783 pane_2.update(cx, |pane, cx| {
23784 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23785 open_editor.update(cx, |editor, cx| {
23786 assert_eq!(
23787 editor.display_text(cx),
23788 main_text,
23789 "Original main.rs text on initial open in another panel",
23790 );
23791 assert_eq!(
23792 editor
23793 .selections
23794 .all::<Point>(&editor.display_snapshot(cx))
23795 .into_iter()
23796 .map(|s| s.range())
23797 .collect::<Vec<_>>(),
23798 vec![Point::zero()..Point::zero()],
23799 "Default selections on initial open in another panel",
23800 );
23801 })
23802 });
23803
23804 editor_2.update_in(cx, |editor, window, cx| {
23805 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23806 });
23807
23808 let _other_editor_1 = workspace
23809 .update_in(cx, |workspace, window, cx| {
23810 workspace.open_path(
23811 (worktree_id, rel_path("lib.rs")),
23812 Some(pane_1.downgrade()),
23813 true,
23814 window,
23815 cx,
23816 )
23817 })
23818 .unwrap()
23819 .await
23820 .downcast::<Editor>()
23821 .unwrap();
23822 pane_1
23823 .update_in(cx, |pane, window, cx| {
23824 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23825 })
23826 .await
23827 .unwrap();
23828 drop(editor_1);
23829 pane_1.update(cx, |pane, cx| {
23830 pane.active_item()
23831 .unwrap()
23832 .downcast::<Editor>()
23833 .unwrap()
23834 .update(cx, |editor, cx| {
23835 assert_eq!(
23836 editor.display_text(cx),
23837 lib_text,
23838 "Other file should be open and active",
23839 );
23840 });
23841 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23842 });
23843
23844 let _other_editor_2 = workspace
23845 .update_in(cx, |workspace, window, cx| {
23846 workspace.open_path(
23847 (worktree_id, rel_path("lib.rs")),
23848 Some(pane_2.downgrade()),
23849 true,
23850 window,
23851 cx,
23852 )
23853 })
23854 .unwrap()
23855 .await
23856 .downcast::<Editor>()
23857 .unwrap();
23858 pane_2
23859 .update_in(cx, |pane, window, cx| {
23860 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23861 })
23862 .await
23863 .unwrap();
23864 drop(editor_2);
23865 pane_2.update(cx, |pane, cx| {
23866 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23867 open_editor.update(cx, |editor, cx| {
23868 assert_eq!(
23869 editor.display_text(cx),
23870 lib_text,
23871 "Other file should be open and active in another panel too",
23872 );
23873 });
23874 assert_eq!(
23875 pane.items().count(),
23876 1,
23877 "No other editors should be open in another pane",
23878 );
23879 });
23880
23881 let _editor_1_reopened = workspace
23882 .update_in(cx, |workspace, window, cx| {
23883 workspace.open_path(
23884 (worktree_id, rel_path("main.rs")),
23885 Some(pane_1.downgrade()),
23886 true,
23887 window,
23888 cx,
23889 )
23890 })
23891 .unwrap()
23892 .await
23893 .downcast::<Editor>()
23894 .unwrap();
23895 let _editor_2_reopened = workspace
23896 .update_in(cx, |workspace, window, cx| {
23897 workspace.open_path(
23898 (worktree_id, rel_path("main.rs")),
23899 Some(pane_2.downgrade()),
23900 true,
23901 window,
23902 cx,
23903 )
23904 })
23905 .unwrap()
23906 .await
23907 .downcast::<Editor>()
23908 .unwrap();
23909 pane_1.update(cx, |pane, cx| {
23910 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23911 open_editor.update(cx, |editor, cx| {
23912 assert_eq!(
23913 editor.display_text(cx),
23914 main_text,
23915 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23916 );
23917 assert_eq!(
23918 editor
23919 .selections
23920 .all::<Point>(&editor.display_snapshot(cx))
23921 .into_iter()
23922 .map(|s| s.range())
23923 .collect::<Vec<_>>(),
23924 expected_ranges,
23925 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23926 );
23927 })
23928 });
23929 pane_2.update(cx, |pane, cx| {
23930 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23931 open_editor.update(cx, |editor, cx| {
23932 assert_eq!(
23933 editor.display_text(cx),
23934 r#"fn main() {
23935⋯rintln!("1");
23936⋯intln!("2");
23937⋯ntln!("3");
23938println!("4");
23939println!("5");
23940}"#,
23941 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23942 );
23943 assert_eq!(
23944 editor
23945 .selections
23946 .all::<Point>(&editor.display_snapshot(cx))
23947 .into_iter()
23948 .map(|s| s.range())
23949 .collect::<Vec<_>>(),
23950 vec![Point::zero()..Point::zero()],
23951 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23952 );
23953 })
23954 });
23955}
23956
23957#[gpui::test]
23958async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23959 init_test(cx, |_| {});
23960
23961 let fs = FakeFs::new(cx.executor());
23962 let main_text = r#"fn main() {
23963println!("1");
23964println!("2");
23965println!("3");
23966println!("4");
23967println!("5");
23968}"#;
23969 let lib_text = "mod foo {}";
23970 fs.insert_tree(
23971 path!("/a"),
23972 json!({
23973 "lib.rs": lib_text,
23974 "main.rs": main_text,
23975 }),
23976 )
23977 .await;
23978
23979 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23980 let (workspace, cx) =
23981 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23982 let worktree_id = workspace.update(cx, |workspace, cx| {
23983 workspace.project().update(cx, |project, cx| {
23984 project.worktrees(cx).next().unwrap().read(cx).id()
23985 })
23986 });
23987
23988 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23989 let editor = workspace
23990 .update_in(cx, |workspace, window, cx| {
23991 workspace.open_path(
23992 (worktree_id, rel_path("main.rs")),
23993 Some(pane.downgrade()),
23994 true,
23995 window,
23996 cx,
23997 )
23998 })
23999 .unwrap()
24000 .await
24001 .downcast::<Editor>()
24002 .unwrap();
24003 pane.update(cx, |pane, cx| {
24004 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24005 open_editor.update(cx, |editor, cx| {
24006 assert_eq!(
24007 editor.display_text(cx),
24008 main_text,
24009 "Original main.rs text on initial open",
24010 );
24011 })
24012 });
24013 editor.update_in(cx, |editor, window, cx| {
24014 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24015 });
24016
24017 cx.update_global(|store: &mut SettingsStore, cx| {
24018 store.update_user_settings(cx, |s| {
24019 s.workspace.restore_on_file_reopen = Some(false);
24020 });
24021 });
24022 editor.update_in(cx, |editor, window, cx| {
24023 editor.fold_ranges(
24024 vec![
24025 Point::new(1, 0)..Point::new(1, 1),
24026 Point::new(2, 0)..Point::new(2, 2),
24027 Point::new(3, 0)..Point::new(3, 3),
24028 ],
24029 false,
24030 window,
24031 cx,
24032 );
24033 });
24034 pane.update_in(cx, |pane, window, cx| {
24035 pane.close_all_items(&CloseAllItems::default(), window, cx)
24036 })
24037 .await
24038 .unwrap();
24039 pane.update(cx, |pane, _| {
24040 assert!(pane.active_item().is_none());
24041 });
24042 cx.update_global(|store: &mut SettingsStore, cx| {
24043 store.update_user_settings(cx, |s| {
24044 s.workspace.restore_on_file_reopen = Some(true);
24045 });
24046 });
24047
24048 let _editor_reopened = workspace
24049 .update_in(cx, |workspace, window, cx| {
24050 workspace.open_path(
24051 (worktree_id, rel_path("main.rs")),
24052 Some(pane.downgrade()),
24053 true,
24054 window,
24055 cx,
24056 )
24057 })
24058 .unwrap()
24059 .await
24060 .downcast::<Editor>()
24061 .unwrap();
24062 pane.update(cx, |pane, cx| {
24063 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24064 open_editor.update(cx, |editor, cx| {
24065 assert_eq!(
24066 editor.display_text(cx),
24067 main_text,
24068 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24069 );
24070 })
24071 });
24072}
24073
24074#[gpui::test]
24075async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24076 struct EmptyModalView {
24077 focus_handle: gpui::FocusHandle,
24078 }
24079 impl EventEmitter<DismissEvent> for EmptyModalView {}
24080 impl Render for EmptyModalView {
24081 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24082 div()
24083 }
24084 }
24085 impl Focusable for EmptyModalView {
24086 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24087 self.focus_handle.clone()
24088 }
24089 }
24090 impl workspace::ModalView for EmptyModalView {}
24091 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24092 EmptyModalView {
24093 focus_handle: cx.focus_handle(),
24094 }
24095 }
24096
24097 init_test(cx, |_| {});
24098
24099 let fs = FakeFs::new(cx.executor());
24100 let project = Project::test(fs, [], cx).await;
24101 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24102 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24103 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24104 let editor = cx.new_window_entity(|window, cx| {
24105 Editor::new(
24106 EditorMode::full(),
24107 buffer,
24108 Some(project.clone()),
24109 window,
24110 cx,
24111 )
24112 });
24113 workspace
24114 .update(cx, |workspace, window, cx| {
24115 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24116 })
24117 .unwrap();
24118 editor.update_in(cx, |editor, window, cx| {
24119 editor.open_context_menu(&OpenContextMenu, window, cx);
24120 assert!(editor.mouse_context_menu.is_some());
24121 });
24122 workspace
24123 .update(cx, |workspace, window, cx| {
24124 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24125 })
24126 .unwrap();
24127 cx.read(|cx| {
24128 assert!(editor.read(cx).mouse_context_menu.is_none());
24129 });
24130}
24131
24132fn set_linked_edit_ranges(
24133 opening: (Point, Point),
24134 closing: (Point, Point),
24135 editor: &mut Editor,
24136 cx: &mut Context<Editor>,
24137) {
24138 let Some((buffer, _)) = editor
24139 .buffer
24140 .read(cx)
24141 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24142 else {
24143 panic!("Failed to get buffer for selection position");
24144 };
24145 let buffer = buffer.read(cx);
24146 let buffer_id = buffer.remote_id();
24147 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24148 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24149 let mut linked_ranges = HashMap::default();
24150 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24151 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24152}
24153
24154#[gpui::test]
24155async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24156 init_test(cx, |_| {});
24157
24158 let fs = FakeFs::new(cx.executor());
24159 fs.insert_file(path!("/file.html"), Default::default())
24160 .await;
24161
24162 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24163
24164 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24165 let html_language = Arc::new(Language::new(
24166 LanguageConfig {
24167 name: "HTML".into(),
24168 matcher: LanguageMatcher {
24169 path_suffixes: vec!["html".to_string()],
24170 ..LanguageMatcher::default()
24171 },
24172 brackets: BracketPairConfig {
24173 pairs: vec![BracketPair {
24174 start: "<".into(),
24175 end: ">".into(),
24176 close: true,
24177 ..Default::default()
24178 }],
24179 ..Default::default()
24180 },
24181 ..Default::default()
24182 },
24183 Some(tree_sitter_html::LANGUAGE.into()),
24184 ));
24185 language_registry.add(html_language);
24186 let mut fake_servers = language_registry.register_fake_lsp(
24187 "HTML",
24188 FakeLspAdapter {
24189 capabilities: lsp::ServerCapabilities {
24190 completion_provider: Some(lsp::CompletionOptions {
24191 resolve_provider: Some(true),
24192 ..Default::default()
24193 }),
24194 ..Default::default()
24195 },
24196 ..Default::default()
24197 },
24198 );
24199
24200 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24201 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24202
24203 let worktree_id = workspace
24204 .update(cx, |workspace, _window, cx| {
24205 workspace.project().update(cx, |project, cx| {
24206 project.worktrees(cx).next().unwrap().read(cx).id()
24207 })
24208 })
24209 .unwrap();
24210 project
24211 .update(cx, |project, cx| {
24212 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24213 })
24214 .await
24215 .unwrap();
24216 let editor = workspace
24217 .update(cx, |workspace, window, cx| {
24218 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24219 })
24220 .unwrap()
24221 .await
24222 .unwrap()
24223 .downcast::<Editor>()
24224 .unwrap();
24225
24226 let fake_server = fake_servers.next().await.unwrap();
24227 editor.update_in(cx, |editor, window, cx| {
24228 editor.set_text("<ad></ad>", window, cx);
24229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24230 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24231 });
24232 set_linked_edit_ranges(
24233 (Point::new(0, 1), Point::new(0, 3)),
24234 (Point::new(0, 6), Point::new(0, 8)),
24235 editor,
24236 cx,
24237 );
24238 });
24239 let mut completion_handle =
24240 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24241 Ok(Some(lsp::CompletionResponse::Array(vec![
24242 lsp::CompletionItem {
24243 label: "head".to_string(),
24244 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24245 lsp::InsertReplaceEdit {
24246 new_text: "head".to_string(),
24247 insert: lsp::Range::new(
24248 lsp::Position::new(0, 1),
24249 lsp::Position::new(0, 3),
24250 ),
24251 replace: lsp::Range::new(
24252 lsp::Position::new(0, 1),
24253 lsp::Position::new(0, 3),
24254 ),
24255 },
24256 )),
24257 ..Default::default()
24258 },
24259 ])))
24260 });
24261 editor.update_in(cx, |editor, window, cx| {
24262 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24263 });
24264 cx.run_until_parked();
24265 completion_handle.next().await.unwrap();
24266 editor.update(cx, |editor, _| {
24267 assert!(
24268 editor.context_menu_visible(),
24269 "Completion menu should be visible"
24270 );
24271 });
24272 editor.update_in(cx, |editor, window, cx| {
24273 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24274 });
24275 cx.executor().run_until_parked();
24276 editor.update(cx, |editor, cx| {
24277 assert_eq!(editor.text(cx), "<head></head>");
24278 });
24279}
24280
24281#[gpui::test]
24282async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24283 init_test(cx, |_| {});
24284
24285 let mut cx = EditorTestContext::new(cx).await;
24286 let language = Arc::new(Language::new(
24287 LanguageConfig {
24288 name: "TSX".into(),
24289 matcher: LanguageMatcher {
24290 path_suffixes: vec!["tsx".to_string()],
24291 ..LanguageMatcher::default()
24292 },
24293 brackets: BracketPairConfig {
24294 pairs: vec![BracketPair {
24295 start: "<".into(),
24296 end: ">".into(),
24297 close: true,
24298 ..Default::default()
24299 }],
24300 ..Default::default()
24301 },
24302 linked_edit_characters: HashSet::from_iter(['.']),
24303 ..Default::default()
24304 },
24305 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24306 ));
24307 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24308
24309 // Test typing > does not extend linked pair
24310 cx.set_state("<divˇ<div></div>");
24311 cx.update_editor(|editor, _, cx| {
24312 set_linked_edit_ranges(
24313 (Point::new(0, 1), Point::new(0, 4)),
24314 (Point::new(0, 11), Point::new(0, 14)),
24315 editor,
24316 cx,
24317 );
24318 });
24319 cx.update_editor(|editor, window, cx| {
24320 editor.handle_input(">", window, cx);
24321 });
24322 cx.assert_editor_state("<div>ˇ<div></div>");
24323
24324 // Test typing . do extend linked pair
24325 cx.set_state("<Animatedˇ></Animated>");
24326 cx.update_editor(|editor, _, cx| {
24327 set_linked_edit_ranges(
24328 (Point::new(0, 1), Point::new(0, 9)),
24329 (Point::new(0, 12), Point::new(0, 20)),
24330 editor,
24331 cx,
24332 );
24333 });
24334 cx.update_editor(|editor, window, cx| {
24335 editor.handle_input(".", window, cx);
24336 });
24337 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24338 cx.update_editor(|editor, _, cx| {
24339 set_linked_edit_ranges(
24340 (Point::new(0, 1), Point::new(0, 10)),
24341 (Point::new(0, 13), Point::new(0, 21)),
24342 editor,
24343 cx,
24344 );
24345 });
24346 cx.update_editor(|editor, window, cx| {
24347 editor.handle_input("V", window, cx);
24348 });
24349 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24350}
24351
24352#[gpui::test]
24353async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24354 init_test(cx, |_| {});
24355
24356 let fs = FakeFs::new(cx.executor());
24357 fs.insert_tree(
24358 path!("/root"),
24359 json!({
24360 "a": {
24361 "main.rs": "fn main() {}",
24362 },
24363 "foo": {
24364 "bar": {
24365 "external_file.rs": "pub mod external {}",
24366 }
24367 }
24368 }),
24369 )
24370 .await;
24371
24372 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24373 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24374 language_registry.add(rust_lang());
24375 let _fake_servers = language_registry.register_fake_lsp(
24376 "Rust",
24377 FakeLspAdapter {
24378 ..FakeLspAdapter::default()
24379 },
24380 );
24381 let (workspace, cx) =
24382 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24383 let worktree_id = workspace.update(cx, |workspace, cx| {
24384 workspace.project().update(cx, |project, cx| {
24385 project.worktrees(cx).next().unwrap().read(cx).id()
24386 })
24387 });
24388
24389 let assert_language_servers_count =
24390 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24391 project.update(cx, |project, cx| {
24392 let current = project
24393 .lsp_store()
24394 .read(cx)
24395 .as_local()
24396 .unwrap()
24397 .language_servers
24398 .len();
24399 assert_eq!(expected, current, "{context}");
24400 });
24401 };
24402
24403 assert_language_servers_count(
24404 0,
24405 "No servers should be running before any file is open",
24406 cx,
24407 );
24408 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24409 let main_editor = workspace
24410 .update_in(cx, |workspace, window, cx| {
24411 workspace.open_path(
24412 (worktree_id, rel_path("main.rs")),
24413 Some(pane.downgrade()),
24414 true,
24415 window,
24416 cx,
24417 )
24418 })
24419 .unwrap()
24420 .await
24421 .downcast::<Editor>()
24422 .unwrap();
24423 pane.update(cx, |pane, cx| {
24424 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24425 open_editor.update(cx, |editor, cx| {
24426 assert_eq!(
24427 editor.display_text(cx),
24428 "fn main() {}",
24429 "Original main.rs text on initial open",
24430 );
24431 });
24432 assert_eq!(open_editor, main_editor);
24433 });
24434 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24435
24436 let external_editor = workspace
24437 .update_in(cx, |workspace, window, cx| {
24438 workspace.open_abs_path(
24439 PathBuf::from("/root/foo/bar/external_file.rs"),
24440 OpenOptions::default(),
24441 window,
24442 cx,
24443 )
24444 })
24445 .await
24446 .expect("opening external file")
24447 .downcast::<Editor>()
24448 .expect("downcasted external file's open element to editor");
24449 pane.update(cx, |pane, cx| {
24450 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24451 open_editor.update(cx, |editor, cx| {
24452 assert_eq!(
24453 editor.display_text(cx),
24454 "pub mod external {}",
24455 "External file is open now",
24456 );
24457 });
24458 assert_eq!(open_editor, external_editor);
24459 });
24460 assert_language_servers_count(
24461 1,
24462 "Second, external, *.rs file should join the existing server",
24463 cx,
24464 );
24465
24466 pane.update_in(cx, |pane, window, cx| {
24467 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24468 })
24469 .await
24470 .unwrap();
24471 pane.update_in(cx, |pane, window, cx| {
24472 pane.navigate_backward(&Default::default(), window, cx);
24473 });
24474 cx.run_until_parked();
24475 pane.update(cx, |pane, cx| {
24476 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24477 open_editor.update(cx, |editor, cx| {
24478 assert_eq!(
24479 editor.display_text(cx),
24480 "pub mod external {}",
24481 "External file is open now",
24482 );
24483 });
24484 });
24485 assert_language_servers_count(
24486 1,
24487 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24488 cx,
24489 );
24490
24491 cx.update(|_, cx| {
24492 workspace::reload(cx);
24493 });
24494 assert_language_servers_count(
24495 1,
24496 "After reloading the worktree with local and external files opened, only one project should be started",
24497 cx,
24498 );
24499}
24500
24501#[gpui::test]
24502async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24503 init_test(cx, |_| {});
24504
24505 let mut cx = EditorTestContext::new(cx).await;
24506 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24507 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24508
24509 // test cursor move to start of each line on tab
24510 // for `if`, `elif`, `else`, `while`, `with` and `for`
24511 cx.set_state(indoc! {"
24512 def main():
24513 ˇ for item in items:
24514 ˇ while item.active:
24515 ˇ if item.value > 10:
24516 ˇ continue
24517 ˇ elif item.value < 0:
24518 ˇ break
24519 ˇ else:
24520 ˇ with item.context() as ctx:
24521 ˇ yield count
24522 ˇ else:
24523 ˇ log('while else')
24524 ˇ else:
24525 ˇ log('for else')
24526 "});
24527 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24528 cx.assert_editor_state(indoc! {"
24529 def main():
24530 ˇfor item in items:
24531 ˇwhile item.active:
24532 ˇif item.value > 10:
24533 ˇcontinue
24534 ˇelif item.value < 0:
24535 ˇbreak
24536 ˇelse:
24537 ˇwith item.context() as ctx:
24538 ˇyield count
24539 ˇelse:
24540 ˇlog('while else')
24541 ˇelse:
24542 ˇlog('for else')
24543 "});
24544 // test relative indent is preserved when tab
24545 // for `if`, `elif`, `else`, `while`, `with` and `for`
24546 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24547 cx.assert_editor_state(indoc! {"
24548 def main():
24549 ˇfor item in items:
24550 ˇwhile item.active:
24551 ˇif item.value > 10:
24552 ˇcontinue
24553 ˇelif item.value < 0:
24554 ˇbreak
24555 ˇelse:
24556 ˇwith item.context() as ctx:
24557 ˇyield count
24558 ˇelse:
24559 ˇlog('while else')
24560 ˇelse:
24561 ˇlog('for else')
24562 "});
24563
24564 // test cursor move to start of each line on tab
24565 // for `try`, `except`, `else`, `finally`, `match` and `def`
24566 cx.set_state(indoc! {"
24567 def main():
24568 ˇ try:
24569 ˇ fetch()
24570 ˇ except ValueError:
24571 ˇ handle_error()
24572 ˇ else:
24573 ˇ match value:
24574 ˇ case _:
24575 ˇ finally:
24576 ˇ def status():
24577 ˇ return 0
24578 "});
24579 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24580 cx.assert_editor_state(indoc! {"
24581 def main():
24582 ˇtry:
24583 ˇfetch()
24584 ˇexcept ValueError:
24585 ˇhandle_error()
24586 ˇelse:
24587 ˇmatch value:
24588 ˇcase _:
24589 ˇfinally:
24590 ˇdef status():
24591 ˇreturn 0
24592 "});
24593 // test relative indent is preserved when tab
24594 // for `try`, `except`, `else`, `finally`, `match` and `def`
24595 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24596 cx.assert_editor_state(indoc! {"
24597 def main():
24598 ˇtry:
24599 ˇfetch()
24600 ˇexcept ValueError:
24601 ˇhandle_error()
24602 ˇelse:
24603 ˇmatch value:
24604 ˇcase _:
24605 ˇfinally:
24606 ˇdef status():
24607 ˇreturn 0
24608 "});
24609}
24610
24611#[gpui::test]
24612async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24613 init_test(cx, |_| {});
24614
24615 let mut cx = EditorTestContext::new(cx).await;
24616 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24617 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24618
24619 // test `else` auto outdents when typed inside `if` block
24620 cx.set_state(indoc! {"
24621 def main():
24622 if i == 2:
24623 return
24624 ˇ
24625 "});
24626 cx.update_editor(|editor, window, cx| {
24627 editor.handle_input("else:", window, cx);
24628 });
24629 cx.assert_editor_state(indoc! {"
24630 def main():
24631 if i == 2:
24632 return
24633 else:ˇ
24634 "});
24635
24636 // test `except` auto outdents when typed inside `try` block
24637 cx.set_state(indoc! {"
24638 def main():
24639 try:
24640 i = 2
24641 ˇ
24642 "});
24643 cx.update_editor(|editor, window, cx| {
24644 editor.handle_input("except:", window, cx);
24645 });
24646 cx.assert_editor_state(indoc! {"
24647 def main():
24648 try:
24649 i = 2
24650 except:ˇ
24651 "});
24652
24653 // test `else` auto outdents when typed inside `except` block
24654 cx.set_state(indoc! {"
24655 def main():
24656 try:
24657 i = 2
24658 except:
24659 j = 2
24660 ˇ
24661 "});
24662 cx.update_editor(|editor, window, cx| {
24663 editor.handle_input("else:", window, cx);
24664 });
24665 cx.assert_editor_state(indoc! {"
24666 def main():
24667 try:
24668 i = 2
24669 except:
24670 j = 2
24671 else:ˇ
24672 "});
24673
24674 // test `finally` auto outdents when typed inside `else` block
24675 cx.set_state(indoc! {"
24676 def main():
24677 try:
24678 i = 2
24679 except:
24680 j = 2
24681 else:
24682 k = 2
24683 ˇ
24684 "});
24685 cx.update_editor(|editor, window, cx| {
24686 editor.handle_input("finally:", window, cx);
24687 });
24688 cx.assert_editor_state(indoc! {"
24689 def main():
24690 try:
24691 i = 2
24692 except:
24693 j = 2
24694 else:
24695 k = 2
24696 finally:ˇ
24697 "});
24698
24699 // test `else` does not outdents when typed inside `except` block right after for block
24700 cx.set_state(indoc! {"
24701 def main():
24702 try:
24703 i = 2
24704 except:
24705 for i in range(n):
24706 pass
24707 ˇ
24708 "});
24709 cx.update_editor(|editor, window, cx| {
24710 editor.handle_input("else:", window, cx);
24711 });
24712 cx.assert_editor_state(indoc! {"
24713 def main():
24714 try:
24715 i = 2
24716 except:
24717 for i in range(n):
24718 pass
24719 else:ˇ
24720 "});
24721
24722 // test `finally` auto outdents when typed inside `else` block right after for block
24723 cx.set_state(indoc! {"
24724 def main():
24725 try:
24726 i = 2
24727 except:
24728 j = 2
24729 else:
24730 for i in range(n):
24731 pass
24732 ˇ
24733 "});
24734 cx.update_editor(|editor, window, cx| {
24735 editor.handle_input("finally:", window, cx);
24736 });
24737 cx.assert_editor_state(indoc! {"
24738 def main():
24739 try:
24740 i = 2
24741 except:
24742 j = 2
24743 else:
24744 for i in range(n):
24745 pass
24746 finally:ˇ
24747 "});
24748
24749 // test `except` outdents to inner "try" block
24750 cx.set_state(indoc! {"
24751 def main():
24752 try:
24753 i = 2
24754 if i == 2:
24755 try:
24756 i = 3
24757 ˇ
24758 "});
24759 cx.update_editor(|editor, window, cx| {
24760 editor.handle_input("except:", window, cx);
24761 });
24762 cx.assert_editor_state(indoc! {"
24763 def main():
24764 try:
24765 i = 2
24766 if i == 2:
24767 try:
24768 i = 3
24769 except:ˇ
24770 "});
24771
24772 // test `except` outdents to outer "try" block
24773 cx.set_state(indoc! {"
24774 def main():
24775 try:
24776 i = 2
24777 if i == 2:
24778 try:
24779 i = 3
24780 ˇ
24781 "});
24782 cx.update_editor(|editor, window, cx| {
24783 editor.handle_input("except:", window, cx);
24784 });
24785 cx.assert_editor_state(indoc! {"
24786 def main():
24787 try:
24788 i = 2
24789 if i == 2:
24790 try:
24791 i = 3
24792 except:ˇ
24793 "});
24794
24795 // test `else` stays at correct indent when typed after `for` block
24796 cx.set_state(indoc! {"
24797 def main():
24798 for i in range(10):
24799 if i == 3:
24800 break
24801 ˇ
24802 "});
24803 cx.update_editor(|editor, window, cx| {
24804 editor.handle_input("else:", window, cx);
24805 });
24806 cx.assert_editor_state(indoc! {"
24807 def main():
24808 for i in range(10):
24809 if i == 3:
24810 break
24811 else:ˇ
24812 "});
24813
24814 // test does not outdent on typing after line with square brackets
24815 cx.set_state(indoc! {"
24816 def f() -> list[str]:
24817 ˇ
24818 "});
24819 cx.update_editor(|editor, window, cx| {
24820 editor.handle_input("a", window, cx);
24821 });
24822 cx.assert_editor_state(indoc! {"
24823 def f() -> list[str]:
24824 aˇ
24825 "});
24826
24827 // test does not outdent on typing : after case keyword
24828 cx.set_state(indoc! {"
24829 match 1:
24830 caseˇ
24831 "});
24832 cx.update_editor(|editor, window, cx| {
24833 editor.handle_input(":", window, cx);
24834 });
24835 cx.assert_editor_state(indoc! {"
24836 match 1:
24837 case:ˇ
24838 "});
24839}
24840
24841#[gpui::test]
24842async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24843 init_test(cx, |_| {});
24844 update_test_language_settings(cx, |settings| {
24845 settings.defaults.extend_comment_on_newline = Some(false);
24846 });
24847 let mut cx = EditorTestContext::new(cx).await;
24848 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24849 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24850
24851 // test correct indent after newline on comment
24852 cx.set_state(indoc! {"
24853 # COMMENT:ˇ
24854 "});
24855 cx.update_editor(|editor, window, cx| {
24856 editor.newline(&Newline, window, cx);
24857 });
24858 cx.assert_editor_state(indoc! {"
24859 # COMMENT:
24860 ˇ
24861 "});
24862
24863 // test correct indent after newline in brackets
24864 cx.set_state(indoc! {"
24865 {ˇ}
24866 "});
24867 cx.update_editor(|editor, window, cx| {
24868 editor.newline(&Newline, window, cx);
24869 });
24870 cx.run_until_parked();
24871 cx.assert_editor_state(indoc! {"
24872 {
24873 ˇ
24874 }
24875 "});
24876
24877 cx.set_state(indoc! {"
24878 (ˇ)
24879 "});
24880 cx.update_editor(|editor, window, cx| {
24881 editor.newline(&Newline, window, cx);
24882 });
24883 cx.run_until_parked();
24884 cx.assert_editor_state(indoc! {"
24885 (
24886 ˇ
24887 )
24888 "});
24889
24890 // do not indent after empty lists or dictionaries
24891 cx.set_state(indoc! {"
24892 a = []ˇ
24893 "});
24894 cx.update_editor(|editor, window, cx| {
24895 editor.newline(&Newline, window, cx);
24896 });
24897 cx.run_until_parked();
24898 cx.assert_editor_state(indoc! {"
24899 a = []
24900 ˇ
24901 "});
24902}
24903
24904#[gpui::test]
24905async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24906 init_test(cx, |_| {});
24907
24908 let mut cx = EditorTestContext::new(cx).await;
24909 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24910 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24911
24912 // test cursor move to start of each line on tab
24913 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24914 cx.set_state(indoc! {"
24915 function main() {
24916 ˇ for item in $items; do
24917 ˇ while [ -n \"$item\" ]; do
24918 ˇ if [ \"$value\" -gt 10 ]; then
24919 ˇ continue
24920 ˇ elif [ \"$value\" -lt 0 ]; then
24921 ˇ break
24922 ˇ else
24923 ˇ echo \"$item\"
24924 ˇ fi
24925 ˇ done
24926 ˇ done
24927 ˇ}
24928 "});
24929 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24930 cx.assert_editor_state(indoc! {"
24931 function main() {
24932 ˇfor item in $items; do
24933 ˇwhile [ -n \"$item\" ]; do
24934 ˇif [ \"$value\" -gt 10 ]; then
24935 ˇcontinue
24936 ˇelif [ \"$value\" -lt 0 ]; then
24937 ˇbreak
24938 ˇelse
24939 ˇecho \"$item\"
24940 ˇfi
24941 ˇdone
24942 ˇdone
24943 ˇ}
24944 "});
24945 // test relative indent is preserved when tab
24946 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24947 cx.assert_editor_state(indoc! {"
24948 function main() {
24949 ˇfor item in $items; do
24950 ˇwhile [ -n \"$item\" ]; do
24951 ˇif [ \"$value\" -gt 10 ]; then
24952 ˇcontinue
24953 ˇelif [ \"$value\" -lt 0 ]; then
24954 ˇbreak
24955 ˇelse
24956 ˇecho \"$item\"
24957 ˇfi
24958 ˇdone
24959 ˇdone
24960 ˇ}
24961 "});
24962
24963 // test cursor move to start of each line on tab
24964 // for `case` statement with patterns
24965 cx.set_state(indoc! {"
24966 function handle() {
24967 ˇ case \"$1\" in
24968 ˇ start)
24969 ˇ echo \"a\"
24970 ˇ ;;
24971 ˇ stop)
24972 ˇ echo \"b\"
24973 ˇ ;;
24974 ˇ *)
24975 ˇ echo \"c\"
24976 ˇ ;;
24977 ˇ esac
24978 ˇ}
24979 "});
24980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24981 cx.assert_editor_state(indoc! {"
24982 function handle() {
24983 ˇcase \"$1\" in
24984 ˇstart)
24985 ˇecho \"a\"
24986 ˇ;;
24987 ˇstop)
24988 ˇecho \"b\"
24989 ˇ;;
24990 ˇ*)
24991 ˇecho \"c\"
24992 ˇ;;
24993 ˇesac
24994 ˇ}
24995 "});
24996}
24997
24998#[gpui::test]
24999async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25000 init_test(cx, |_| {});
25001
25002 let mut cx = EditorTestContext::new(cx).await;
25003 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25004 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25005
25006 // test indents on comment insert
25007 cx.set_state(indoc! {"
25008 function main() {
25009 ˇ for item in $items; do
25010 ˇ while [ -n \"$item\" ]; do
25011 ˇ if [ \"$value\" -gt 10 ]; then
25012 ˇ continue
25013 ˇ elif [ \"$value\" -lt 0 ]; then
25014 ˇ break
25015 ˇ else
25016 ˇ echo \"$item\"
25017 ˇ fi
25018 ˇ done
25019 ˇ done
25020 ˇ}
25021 "});
25022 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25023 cx.assert_editor_state(indoc! {"
25024 function main() {
25025 #ˇ for item in $items; do
25026 #ˇ while [ -n \"$item\" ]; do
25027 #ˇ if [ \"$value\" -gt 10 ]; then
25028 #ˇ continue
25029 #ˇ elif [ \"$value\" -lt 0 ]; then
25030 #ˇ break
25031 #ˇ else
25032 #ˇ echo \"$item\"
25033 #ˇ fi
25034 #ˇ done
25035 #ˇ done
25036 #ˇ}
25037 "});
25038}
25039
25040#[gpui::test]
25041async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25042 init_test(cx, |_| {});
25043
25044 let mut cx = EditorTestContext::new(cx).await;
25045 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25046 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25047
25048 // test `else` auto outdents when typed inside `if` block
25049 cx.set_state(indoc! {"
25050 if [ \"$1\" = \"test\" ]; then
25051 echo \"foo bar\"
25052 ˇ
25053 "});
25054 cx.update_editor(|editor, window, cx| {
25055 editor.handle_input("else", window, cx);
25056 });
25057 cx.assert_editor_state(indoc! {"
25058 if [ \"$1\" = \"test\" ]; then
25059 echo \"foo bar\"
25060 elseˇ
25061 "});
25062
25063 // test `elif` auto outdents when typed inside `if` block
25064 cx.set_state(indoc! {"
25065 if [ \"$1\" = \"test\" ]; then
25066 echo \"foo bar\"
25067 ˇ
25068 "});
25069 cx.update_editor(|editor, window, cx| {
25070 editor.handle_input("elif", window, cx);
25071 });
25072 cx.assert_editor_state(indoc! {"
25073 if [ \"$1\" = \"test\" ]; then
25074 echo \"foo bar\"
25075 elifˇ
25076 "});
25077
25078 // test `fi` auto outdents when typed inside `else` block
25079 cx.set_state(indoc! {"
25080 if [ \"$1\" = \"test\" ]; then
25081 echo \"foo bar\"
25082 else
25083 echo \"bar baz\"
25084 ˇ
25085 "});
25086 cx.update_editor(|editor, window, cx| {
25087 editor.handle_input("fi", window, cx);
25088 });
25089 cx.assert_editor_state(indoc! {"
25090 if [ \"$1\" = \"test\" ]; then
25091 echo \"foo bar\"
25092 else
25093 echo \"bar baz\"
25094 fiˇ
25095 "});
25096
25097 // test `done` auto outdents when typed inside `while` block
25098 cx.set_state(indoc! {"
25099 while read line; do
25100 echo \"$line\"
25101 ˇ
25102 "});
25103 cx.update_editor(|editor, window, cx| {
25104 editor.handle_input("done", window, cx);
25105 });
25106 cx.assert_editor_state(indoc! {"
25107 while read line; do
25108 echo \"$line\"
25109 doneˇ
25110 "});
25111
25112 // test `done` auto outdents when typed inside `for` block
25113 cx.set_state(indoc! {"
25114 for file in *.txt; do
25115 cat \"$file\"
25116 ˇ
25117 "});
25118 cx.update_editor(|editor, window, cx| {
25119 editor.handle_input("done", window, cx);
25120 });
25121 cx.assert_editor_state(indoc! {"
25122 for file in *.txt; do
25123 cat \"$file\"
25124 doneˇ
25125 "});
25126
25127 // test `esac` auto outdents when typed inside `case` block
25128 cx.set_state(indoc! {"
25129 case \"$1\" in
25130 start)
25131 echo \"foo bar\"
25132 ;;
25133 stop)
25134 echo \"bar baz\"
25135 ;;
25136 ˇ
25137 "});
25138 cx.update_editor(|editor, window, cx| {
25139 editor.handle_input("esac", window, cx);
25140 });
25141 cx.assert_editor_state(indoc! {"
25142 case \"$1\" in
25143 start)
25144 echo \"foo bar\"
25145 ;;
25146 stop)
25147 echo \"bar baz\"
25148 ;;
25149 esacˇ
25150 "});
25151
25152 // test `*)` auto outdents when typed inside `case` block
25153 cx.set_state(indoc! {"
25154 case \"$1\" in
25155 start)
25156 echo \"foo bar\"
25157 ;;
25158 ˇ
25159 "});
25160 cx.update_editor(|editor, window, cx| {
25161 editor.handle_input("*)", window, cx);
25162 });
25163 cx.assert_editor_state(indoc! {"
25164 case \"$1\" in
25165 start)
25166 echo \"foo bar\"
25167 ;;
25168 *)ˇ
25169 "});
25170
25171 // test `fi` outdents to correct level with nested if blocks
25172 cx.set_state(indoc! {"
25173 if [ \"$1\" = \"test\" ]; then
25174 echo \"outer if\"
25175 if [ \"$2\" = \"debug\" ]; then
25176 echo \"inner if\"
25177 ˇ
25178 "});
25179 cx.update_editor(|editor, window, cx| {
25180 editor.handle_input("fi", window, cx);
25181 });
25182 cx.assert_editor_state(indoc! {"
25183 if [ \"$1\" = \"test\" ]; then
25184 echo \"outer if\"
25185 if [ \"$2\" = \"debug\" ]; then
25186 echo \"inner if\"
25187 fiˇ
25188 "});
25189}
25190
25191#[gpui::test]
25192async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25193 init_test(cx, |_| {});
25194 update_test_language_settings(cx, |settings| {
25195 settings.defaults.extend_comment_on_newline = Some(false);
25196 });
25197 let mut cx = EditorTestContext::new(cx).await;
25198 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25200
25201 // test correct indent after newline on comment
25202 cx.set_state(indoc! {"
25203 # COMMENT:ˇ
25204 "});
25205 cx.update_editor(|editor, window, cx| {
25206 editor.newline(&Newline, window, cx);
25207 });
25208 cx.assert_editor_state(indoc! {"
25209 # COMMENT:
25210 ˇ
25211 "});
25212
25213 // test correct indent after newline after `then`
25214 cx.set_state(indoc! {"
25215
25216 if [ \"$1\" = \"test\" ]; thenˇ
25217 "});
25218 cx.update_editor(|editor, window, cx| {
25219 editor.newline(&Newline, window, cx);
25220 });
25221 cx.run_until_parked();
25222 cx.assert_editor_state(indoc! {"
25223
25224 if [ \"$1\" = \"test\" ]; then
25225 ˇ
25226 "});
25227
25228 // test correct indent after newline after `else`
25229 cx.set_state(indoc! {"
25230 if [ \"$1\" = \"test\" ]; then
25231 elseˇ
25232 "});
25233 cx.update_editor(|editor, window, cx| {
25234 editor.newline(&Newline, window, cx);
25235 });
25236 cx.run_until_parked();
25237 cx.assert_editor_state(indoc! {"
25238 if [ \"$1\" = \"test\" ]; then
25239 else
25240 ˇ
25241 "});
25242
25243 // test correct indent after newline after `elif`
25244 cx.set_state(indoc! {"
25245 if [ \"$1\" = \"test\" ]; then
25246 elifˇ
25247 "});
25248 cx.update_editor(|editor, window, cx| {
25249 editor.newline(&Newline, window, cx);
25250 });
25251 cx.run_until_parked();
25252 cx.assert_editor_state(indoc! {"
25253 if [ \"$1\" = \"test\" ]; then
25254 elif
25255 ˇ
25256 "});
25257
25258 // test correct indent after newline after `do`
25259 cx.set_state(indoc! {"
25260 for file in *.txt; doˇ
25261 "});
25262 cx.update_editor(|editor, window, cx| {
25263 editor.newline(&Newline, window, cx);
25264 });
25265 cx.run_until_parked();
25266 cx.assert_editor_state(indoc! {"
25267 for file in *.txt; do
25268 ˇ
25269 "});
25270
25271 // test correct indent after newline after case pattern
25272 cx.set_state(indoc! {"
25273 case \"$1\" in
25274 start)ˇ
25275 "});
25276 cx.update_editor(|editor, window, cx| {
25277 editor.newline(&Newline, window, cx);
25278 });
25279 cx.run_until_parked();
25280 cx.assert_editor_state(indoc! {"
25281 case \"$1\" in
25282 start)
25283 ˇ
25284 "});
25285
25286 // test correct indent after newline after case pattern
25287 cx.set_state(indoc! {"
25288 case \"$1\" in
25289 start)
25290 ;;
25291 *)ˇ
25292 "});
25293 cx.update_editor(|editor, window, cx| {
25294 editor.newline(&Newline, window, cx);
25295 });
25296 cx.run_until_parked();
25297 cx.assert_editor_state(indoc! {"
25298 case \"$1\" in
25299 start)
25300 ;;
25301 *)
25302 ˇ
25303 "});
25304
25305 // test correct indent after newline after function opening brace
25306 cx.set_state(indoc! {"
25307 function test() {ˇ}
25308 "});
25309 cx.update_editor(|editor, window, cx| {
25310 editor.newline(&Newline, window, cx);
25311 });
25312 cx.run_until_parked();
25313 cx.assert_editor_state(indoc! {"
25314 function test() {
25315 ˇ
25316 }
25317 "});
25318
25319 // test no extra indent after semicolon on same line
25320 cx.set_state(indoc! {"
25321 echo \"test\";ˇ
25322 "});
25323 cx.update_editor(|editor, window, cx| {
25324 editor.newline(&Newline, window, cx);
25325 });
25326 cx.run_until_parked();
25327 cx.assert_editor_state(indoc! {"
25328 echo \"test\";
25329 ˇ
25330 "});
25331}
25332
25333fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25334 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25335 point..point
25336}
25337
25338#[track_caller]
25339fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25340 let (text, ranges) = marked_text_ranges(marked_text, true);
25341 assert_eq!(editor.text(cx), text);
25342 assert_eq!(
25343 editor.selections.ranges(&editor.display_snapshot(cx)),
25344 ranges,
25345 "Assert selections are {}",
25346 marked_text
25347 );
25348}
25349
25350pub fn handle_signature_help_request(
25351 cx: &mut EditorLspTestContext,
25352 mocked_response: lsp::SignatureHelp,
25353) -> impl Future<Output = ()> + use<> {
25354 let mut request =
25355 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25356 let mocked_response = mocked_response.clone();
25357 async move { Ok(Some(mocked_response)) }
25358 });
25359
25360 async move {
25361 request.next().await;
25362 }
25363}
25364
25365#[track_caller]
25366pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25367 cx.update_editor(|editor, _, _| {
25368 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25369 let entries = menu.entries.borrow();
25370 let entries = entries
25371 .iter()
25372 .map(|entry| entry.string.as_str())
25373 .collect::<Vec<_>>();
25374 assert_eq!(entries, expected);
25375 } else {
25376 panic!("Expected completions menu");
25377 }
25378 });
25379}
25380
25381/// Handle completion request passing a marked string specifying where the completion
25382/// should be triggered from using '|' character, what range should be replaced, and what completions
25383/// should be returned using '<' and '>' to delimit the range.
25384///
25385/// Also see `handle_completion_request_with_insert_and_replace`.
25386#[track_caller]
25387pub fn handle_completion_request(
25388 marked_string: &str,
25389 completions: Vec<&'static str>,
25390 is_incomplete: bool,
25391 counter: Arc<AtomicUsize>,
25392 cx: &mut EditorLspTestContext,
25393) -> impl Future<Output = ()> {
25394 let complete_from_marker: TextRangeMarker = '|'.into();
25395 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25396 let (_, mut marked_ranges) = marked_text_ranges_by(
25397 marked_string,
25398 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25399 );
25400
25401 let complete_from_position =
25402 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25403 let replace_range =
25404 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25405
25406 let mut request =
25407 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25408 let completions = completions.clone();
25409 counter.fetch_add(1, atomic::Ordering::Release);
25410 async move {
25411 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25412 assert_eq!(
25413 params.text_document_position.position,
25414 complete_from_position
25415 );
25416 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25417 is_incomplete,
25418 item_defaults: None,
25419 items: completions
25420 .iter()
25421 .map(|completion_text| lsp::CompletionItem {
25422 label: completion_text.to_string(),
25423 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25424 range: replace_range,
25425 new_text: completion_text.to_string(),
25426 })),
25427 ..Default::default()
25428 })
25429 .collect(),
25430 })))
25431 }
25432 });
25433
25434 async move {
25435 request.next().await;
25436 }
25437}
25438
25439/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25440/// given instead, which also contains an `insert` range.
25441///
25442/// This function uses markers to define ranges:
25443/// - `|` marks the cursor position
25444/// - `<>` marks the replace range
25445/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25446pub fn handle_completion_request_with_insert_and_replace(
25447 cx: &mut EditorLspTestContext,
25448 marked_string: &str,
25449 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25450 counter: Arc<AtomicUsize>,
25451) -> impl Future<Output = ()> {
25452 let complete_from_marker: TextRangeMarker = '|'.into();
25453 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25454 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25455
25456 let (_, mut marked_ranges) = marked_text_ranges_by(
25457 marked_string,
25458 vec![
25459 complete_from_marker.clone(),
25460 replace_range_marker.clone(),
25461 insert_range_marker.clone(),
25462 ],
25463 );
25464
25465 let complete_from_position =
25466 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25467 let replace_range =
25468 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25469
25470 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25471 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25472 _ => lsp::Range {
25473 start: replace_range.start,
25474 end: complete_from_position,
25475 },
25476 };
25477
25478 let mut request =
25479 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25480 let completions = completions.clone();
25481 counter.fetch_add(1, atomic::Ordering::Release);
25482 async move {
25483 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25484 assert_eq!(
25485 params.text_document_position.position, complete_from_position,
25486 "marker `|` position doesn't match",
25487 );
25488 Ok(Some(lsp::CompletionResponse::Array(
25489 completions
25490 .iter()
25491 .map(|(label, new_text)| lsp::CompletionItem {
25492 label: label.to_string(),
25493 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25494 lsp::InsertReplaceEdit {
25495 insert: insert_range,
25496 replace: replace_range,
25497 new_text: new_text.to_string(),
25498 },
25499 )),
25500 ..Default::default()
25501 })
25502 .collect(),
25503 )))
25504 }
25505 });
25506
25507 async move {
25508 request.next().await;
25509 }
25510}
25511
25512fn handle_resolve_completion_request(
25513 cx: &mut EditorLspTestContext,
25514 edits: Option<Vec<(&'static str, &'static str)>>,
25515) -> impl Future<Output = ()> {
25516 let edits = edits.map(|edits| {
25517 edits
25518 .iter()
25519 .map(|(marked_string, new_text)| {
25520 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25521 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25522 lsp::TextEdit::new(replace_range, new_text.to_string())
25523 })
25524 .collect::<Vec<_>>()
25525 });
25526
25527 let mut request =
25528 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25529 let edits = edits.clone();
25530 async move {
25531 Ok(lsp::CompletionItem {
25532 additional_text_edits: edits,
25533 ..Default::default()
25534 })
25535 }
25536 });
25537
25538 async move {
25539 request.next().await;
25540 }
25541}
25542
25543pub(crate) fn update_test_language_settings(
25544 cx: &mut TestAppContext,
25545 f: impl Fn(&mut AllLanguageSettingsContent),
25546) {
25547 cx.update(|cx| {
25548 SettingsStore::update_global(cx, |store, cx| {
25549 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25550 });
25551 });
25552}
25553
25554pub(crate) fn update_test_project_settings(
25555 cx: &mut TestAppContext,
25556 f: impl Fn(&mut ProjectSettingsContent),
25557) {
25558 cx.update(|cx| {
25559 SettingsStore::update_global(cx, |store, cx| {
25560 store.update_user_settings(cx, |settings| f(&mut settings.project));
25561 });
25562 });
25563}
25564
25565pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25566 cx.update(|cx| {
25567 assets::Assets.load_test_fonts(cx);
25568 let store = SettingsStore::test(cx);
25569 cx.set_global(store);
25570 theme::init(theme::LoadThemes::JustBase, cx);
25571 release_channel::init(SemanticVersion::default(), cx);
25572 client::init_settings(cx);
25573 language::init(cx);
25574 Project::init_settings(cx);
25575 workspace::init_settings(cx);
25576 crate::init(cx);
25577 });
25578 zlog::init_test();
25579 update_test_language_settings(cx, f);
25580}
25581
25582#[track_caller]
25583fn assert_hunk_revert(
25584 not_reverted_text_with_selections: &str,
25585 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25586 expected_reverted_text_with_selections: &str,
25587 base_text: &str,
25588 cx: &mut EditorLspTestContext,
25589) {
25590 cx.set_state(not_reverted_text_with_selections);
25591 cx.set_head_text(base_text);
25592 cx.executor().run_until_parked();
25593
25594 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25595 let snapshot = editor.snapshot(window, cx);
25596 let reverted_hunk_statuses = snapshot
25597 .buffer_snapshot()
25598 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25599 .map(|hunk| hunk.status().kind)
25600 .collect::<Vec<_>>();
25601
25602 editor.git_restore(&Default::default(), window, cx);
25603 reverted_hunk_statuses
25604 });
25605 cx.executor().run_until_parked();
25606 cx.assert_editor_state(expected_reverted_text_with_selections);
25607 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25608}
25609
25610#[gpui::test(iterations = 10)]
25611async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25612 init_test(cx, |_| {});
25613
25614 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25615 let counter = diagnostic_requests.clone();
25616
25617 let fs = FakeFs::new(cx.executor());
25618 fs.insert_tree(
25619 path!("/a"),
25620 json!({
25621 "first.rs": "fn main() { let a = 5; }",
25622 "second.rs": "// Test file",
25623 }),
25624 )
25625 .await;
25626
25627 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25628 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25629 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25630
25631 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25632 language_registry.add(rust_lang());
25633 let mut fake_servers = language_registry.register_fake_lsp(
25634 "Rust",
25635 FakeLspAdapter {
25636 capabilities: lsp::ServerCapabilities {
25637 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25638 lsp::DiagnosticOptions {
25639 identifier: None,
25640 inter_file_dependencies: true,
25641 workspace_diagnostics: true,
25642 work_done_progress_options: Default::default(),
25643 },
25644 )),
25645 ..Default::default()
25646 },
25647 ..Default::default()
25648 },
25649 );
25650
25651 let editor = workspace
25652 .update(cx, |workspace, window, cx| {
25653 workspace.open_abs_path(
25654 PathBuf::from(path!("/a/first.rs")),
25655 OpenOptions::default(),
25656 window,
25657 cx,
25658 )
25659 })
25660 .unwrap()
25661 .await
25662 .unwrap()
25663 .downcast::<Editor>()
25664 .unwrap();
25665 let fake_server = fake_servers.next().await.unwrap();
25666 let server_id = fake_server.server.server_id();
25667 let mut first_request = fake_server
25668 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25669 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25670 let result_id = Some(new_result_id.to_string());
25671 assert_eq!(
25672 params.text_document.uri,
25673 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25674 );
25675 async move {
25676 Ok(lsp::DocumentDiagnosticReportResult::Report(
25677 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25678 related_documents: None,
25679 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25680 items: Vec::new(),
25681 result_id,
25682 },
25683 }),
25684 ))
25685 }
25686 });
25687
25688 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25689 project.update(cx, |project, cx| {
25690 let buffer_id = editor
25691 .read(cx)
25692 .buffer()
25693 .read(cx)
25694 .as_singleton()
25695 .expect("created a singleton buffer")
25696 .read(cx)
25697 .remote_id();
25698 let buffer_result_id = project
25699 .lsp_store()
25700 .read(cx)
25701 .result_id(server_id, buffer_id, cx);
25702 assert_eq!(expected, buffer_result_id);
25703 });
25704 };
25705
25706 ensure_result_id(None, cx);
25707 cx.executor().advance_clock(Duration::from_millis(60));
25708 cx.executor().run_until_parked();
25709 assert_eq!(
25710 diagnostic_requests.load(atomic::Ordering::Acquire),
25711 1,
25712 "Opening file should trigger diagnostic request"
25713 );
25714 first_request
25715 .next()
25716 .await
25717 .expect("should have sent the first diagnostics pull request");
25718 ensure_result_id(Some("1".to_string()), cx);
25719
25720 // Editing should trigger diagnostics
25721 editor.update_in(cx, |editor, window, cx| {
25722 editor.handle_input("2", window, cx)
25723 });
25724 cx.executor().advance_clock(Duration::from_millis(60));
25725 cx.executor().run_until_parked();
25726 assert_eq!(
25727 diagnostic_requests.load(atomic::Ordering::Acquire),
25728 2,
25729 "Editing should trigger diagnostic request"
25730 );
25731 ensure_result_id(Some("2".to_string()), cx);
25732
25733 // Moving cursor should not trigger diagnostic request
25734 editor.update_in(cx, |editor, window, cx| {
25735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25736 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25737 });
25738 });
25739 cx.executor().advance_clock(Duration::from_millis(60));
25740 cx.executor().run_until_parked();
25741 assert_eq!(
25742 diagnostic_requests.load(atomic::Ordering::Acquire),
25743 2,
25744 "Cursor movement should not trigger diagnostic request"
25745 );
25746 ensure_result_id(Some("2".to_string()), cx);
25747 // Multiple rapid edits should be debounced
25748 for _ in 0..5 {
25749 editor.update_in(cx, |editor, window, cx| {
25750 editor.handle_input("x", window, cx)
25751 });
25752 }
25753 cx.executor().advance_clock(Duration::from_millis(60));
25754 cx.executor().run_until_parked();
25755
25756 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25757 assert!(
25758 final_requests <= 4,
25759 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25760 );
25761 ensure_result_id(Some(final_requests.to_string()), cx);
25762}
25763
25764#[gpui::test]
25765async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25766 // Regression test for issue #11671
25767 // Previously, adding a cursor after moving multiple cursors would reset
25768 // the cursor count instead of adding to the existing cursors.
25769 init_test(cx, |_| {});
25770 let mut cx = EditorTestContext::new(cx).await;
25771
25772 // Create a simple buffer with cursor at start
25773 cx.set_state(indoc! {"
25774 ˇaaaa
25775 bbbb
25776 cccc
25777 dddd
25778 eeee
25779 ffff
25780 gggg
25781 hhhh"});
25782
25783 // Add 2 cursors below (so we have 3 total)
25784 cx.update_editor(|editor, window, cx| {
25785 editor.add_selection_below(&Default::default(), window, cx);
25786 editor.add_selection_below(&Default::default(), window, cx);
25787 });
25788
25789 // Verify we have 3 cursors
25790 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25791 assert_eq!(
25792 initial_count, 3,
25793 "Should have 3 cursors after adding 2 below"
25794 );
25795
25796 // Move down one line
25797 cx.update_editor(|editor, window, cx| {
25798 editor.move_down(&MoveDown, window, cx);
25799 });
25800
25801 // Add another cursor below
25802 cx.update_editor(|editor, window, cx| {
25803 editor.add_selection_below(&Default::default(), window, cx);
25804 });
25805
25806 // Should now have 4 cursors (3 original + 1 new)
25807 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25808 assert_eq!(
25809 final_count, 4,
25810 "Should have 4 cursors after moving and adding another"
25811 );
25812}
25813
25814#[gpui::test]
25815async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25816 init_test(cx, |_| {});
25817
25818 let mut cx = EditorTestContext::new(cx).await;
25819
25820 cx.set_state(indoc!(
25821 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25822 Second line here"#
25823 ));
25824
25825 cx.update_editor(|editor, window, cx| {
25826 // Enable soft wrapping with a narrow width to force soft wrapping and
25827 // confirm that more than 2 rows are being displayed.
25828 editor.set_wrap_width(Some(100.0.into()), cx);
25829 assert!(editor.display_text(cx).lines().count() > 2);
25830
25831 editor.add_selection_below(
25832 &AddSelectionBelow {
25833 skip_soft_wrap: true,
25834 },
25835 window,
25836 cx,
25837 );
25838
25839 assert_eq!(
25840 editor.selections.display_ranges(cx),
25841 &[
25842 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25843 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25844 ]
25845 );
25846
25847 editor.add_selection_above(
25848 &AddSelectionAbove {
25849 skip_soft_wrap: true,
25850 },
25851 window,
25852 cx,
25853 );
25854
25855 assert_eq!(
25856 editor.selections.display_ranges(cx),
25857 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25858 );
25859
25860 editor.add_selection_below(
25861 &AddSelectionBelow {
25862 skip_soft_wrap: false,
25863 },
25864 window,
25865 cx,
25866 );
25867
25868 assert_eq!(
25869 editor.selections.display_ranges(cx),
25870 &[
25871 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25872 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25873 ]
25874 );
25875
25876 editor.add_selection_above(
25877 &AddSelectionAbove {
25878 skip_soft_wrap: false,
25879 },
25880 window,
25881 cx,
25882 );
25883
25884 assert_eq!(
25885 editor.selections.display_ranges(cx),
25886 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25887 );
25888 });
25889}
25890
25891#[gpui::test(iterations = 10)]
25892async fn test_document_colors(cx: &mut TestAppContext) {
25893 let expected_color = Rgba {
25894 r: 0.33,
25895 g: 0.33,
25896 b: 0.33,
25897 a: 0.33,
25898 };
25899
25900 init_test(cx, |_| {});
25901
25902 let fs = FakeFs::new(cx.executor());
25903 fs.insert_tree(
25904 path!("/a"),
25905 json!({
25906 "first.rs": "fn main() { let a = 5; }",
25907 }),
25908 )
25909 .await;
25910
25911 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25912 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25913 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25914
25915 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25916 language_registry.add(rust_lang());
25917 let mut fake_servers = language_registry.register_fake_lsp(
25918 "Rust",
25919 FakeLspAdapter {
25920 capabilities: lsp::ServerCapabilities {
25921 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25922 ..lsp::ServerCapabilities::default()
25923 },
25924 name: "rust-analyzer",
25925 ..FakeLspAdapter::default()
25926 },
25927 );
25928 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25929 "Rust",
25930 FakeLspAdapter {
25931 capabilities: lsp::ServerCapabilities {
25932 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25933 ..lsp::ServerCapabilities::default()
25934 },
25935 name: "not-rust-analyzer",
25936 ..FakeLspAdapter::default()
25937 },
25938 );
25939
25940 let editor = workspace
25941 .update(cx, |workspace, window, cx| {
25942 workspace.open_abs_path(
25943 PathBuf::from(path!("/a/first.rs")),
25944 OpenOptions::default(),
25945 window,
25946 cx,
25947 )
25948 })
25949 .unwrap()
25950 .await
25951 .unwrap()
25952 .downcast::<Editor>()
25953 .unwrap();
25954 let fake_language_server = fake_servers.next().await.unwrap();
25955 let fake_language_server_without_capabilities =
25956 fake_servers_without_capabilities.next().await.unwrap();
25957 let requests_made = Arc::new(AtomicUsize::new(0));
25958 let closure_requests_made = Arc::clone(&requests_made);
25959 let mut color_request_handle = fake_language_server
25960 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25961 let requests_made = Arc::clone(&closure_requests_made);
25962 async move {
25963 assert_eq!(
25964 params.text_document.uri,
25965 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25966 );
25967 requests_made.fetch_add(1, atomic::Ordering::Release);
25968 Ok(vec![
25969 lsp::ColorInformation {
25970 range: lsp::Range {
25971 start: lsp::Position {
25972 line: 0,
25973 character: 0,
25974 },
25975 end: lsp::Position {
25976 line: 0,
25977 character: 1,
25978 },
25979 },
25980 color: lsp::Color {
25981 red: 0.33,
25982 green: 0.33,
25983 blue: 0.33,
25984 alpha: 0.33,
25985 },
25986 },
25987 lsp::ColorInformation {
25988 range: lsp::Range {
25989 start: lsp::Position {
25990 line: 0,
25991 character: 0,
25992 },
25993 end: lsp::Position {
25994 line: 0,
25995 character: 1,
25996 },
25997 },
25998 color: lsp::Color {
25999 red: 0.33,
26000 green: 0.33,
26001 blue: 0.33,
26002 alpha: 0.33,
26003 },
26004 },
26005 ])
26006 }
26007 });
26008
26009 let _handle = fake_language_server_without_capabilities
26010 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26011 panic!("Should not be called");
26012 });
26013 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26014 color_request_handle.next().await.unwrap();
26015 cx.run_until_parked();
26016 assert_eq!(
26017 1,
26018 requests_made.load(atomic::Ordering::Acquire),
26019 "Should query for colors once per editor open"
26020 );
26021 editor.update_in(cx, |editor, _, cx| {
26022 assert_eq!(
26023 vec![expected_color],
26024 extract_color_inlays(editor, cx),
26025 "Should have an initial inlay"
26026 );
26027 });
26028
26029 // opening another file in a split should not influence the LSP query counter
26030 workspace
26031 .update(cx, |workspace, window, cx| {
26032 assert_eq!(
26033 workspace.panes().len(),
26034 1,
26035 "Should have one pane with one editor"
26036 );
26037 workspace.move_item_to_pane_in_direction(
26038 &MoveItemToPaneInDirection {
26039 direction: SplitDirection::Right,
26040 focus: false,
26041 clone: true,
26042 },
26043 window,
26044 cx,
26045 );
26046 })
26047 .unwrap();
26048 cx.run_until_parked();
26049 workspace
26050 .update(cx, |workspace, _, cx| {
26051 let panes = workspace.panes();
26052 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26053 for pane in panes {
26054 let editor = pane
26055 .read(cx)
26056 .active_item()
26057 .and_then(|item| item.downcast::<Editor>())
26058 .expect("Should have opened an editor in each split");
26059 let editor_file = editor
26060 .read(cx)
26061 .buffer()
26062 .read(cx)
26063 .as_singleton()
26064 .expect("test deals with singleton buffers")
26065 .read(cx)
26066 .file()
26067 .expect("test buffese should have a file")
26068 .path();
26069 assert_eq!(
26070 editor_file.as_ref(),
26071 rel_path("first.rs"),
26072 "Both editors should be opened for the same file"
26073 )
26074 }
26075 })
26076 .unwrap();
26077
26078 cx.executor().advance_clock(Duration::from_millis(500));
26079 let save = editor.update_in(cx, |editor, window, cx| {
26080 editor.move_to_end(&MoveToEnd, window, cx);
26081 editor.handle_input("dirty", window, cx);
26082 editor.save(
26083 SaveOptions {
26084 format: true,
26085 autosave: true,
26086 },
26087 project.clone(),
26088 window,
26089 cx,
26090 )
26091 });
26092 save.await.unwrap();
26093
26094 color_request_handle.next().await.unwrap();
26095 cx.run_until_parked();
26096 assert_eq!(
26097 2,
26098 requests_made.load(atomic::Ordering::Acquire),
26099 "Should query for colors once per save (deduplicated) and once per formatting after save"
26100 );
26101
26102 drop(editor);
26103 let close = workspace
26104 .update(cx, |workspace, window, cx| {
26105 workspace.active_pane().update(cx, |pane, cx| {
26106 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26107 })
26108 })
26109 .unwrap();
26110 close.await.unwrap();
26111 let close = workspace
26112 .update(cx, |workspace, window, cx| {
26113 workspace.active_pane().update(cx, |pane, cx| {
26114 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26115 })
26116 })
26117 .unwrap();
26118 close.await.unwrap();
26119 assert_eq!(
26120 2,
26121 requests_made.load(atomic::Ordering::Acquire),
26122 "After saving and closing all editors, no extra requests should be made"
26123 );
26124 workspace
26125 .update(cx, |workspace, _, cx| {
26126 assert!(
26127 workspace.active_item(cx).is_none(),
26128 "Should close all editors"
26129 )
26130 })
26131 .unwrap();
26132
26133 workspace
26134 .update(cx, |workspace, window, cx| {
26135 workspace.active_pane().update(cx, |pane, cx| {
26136 pane.navigate_backward(&workspace::GoBack, window, cx);
26137 })
26138 })
26139 .unwrap();
26140 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26141 cx.run_until_parked();
26142 let editor = workspace
26143 .update(cx, |workspace, _, cx| {
26144 workspace
26145 .active_item(cx)
26146 .expect("Should have reopened the editor again after navigating back")
26147 .downcast::<Editor>()
26148 .expect("Should be an editor")
26149 })
26150 .unwrap();
26151
26152 assert_eq!(
26153 2,
26154 requests_made.load(atomic::Ordering::Acquire),
26155 "Cache should be reused on buffer close and reopen"
26156 );
26157 editor.update(cx, |editor, cx| {
26158 assert_eq!(
26159 vec![expected_color],
26160 extract_color_inlays(editor, cx),
26161 "Should have an initial inlay"
26162 );
26163 });
26164
26165 drop(color_request_handle);
26166 let closure_requests_made = Arc::clone(&requests_made);
26167 let mut empty_color_request_handle = fake_language_server
26168 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26169 let requests_made = Arc::clone(&closure_requests_made);
26170 async move {
26171 assert_eq!(
26172 params.text_document.uri,
26173 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26174 );
26175 requests_made.fetch_add(1, atomic::Ordering::Release);
26176 Ok(Vec::new())
26177 }
26178 });
26179 let save = editor.update_in(cx, |editor, window, cx| {
26180 editor.move_to_end(&MoveToEnd, window, cx);
26181 editor.handle_input("dirty_again", window, cx);
26182 editor.save(
26183 SaveOptions {
26184 format: false,
26185 autosave: true,
26186 },
26187 project.clone(),
26188 window,
26189 cx,
26190 )
26191 });
26192 save.await.unwrap();
26193
26194 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26195 empty_color_request_handle.next().await.unwrap();
26196 cx.run_until_parked();
26197 assert_eq!(
26198 3,
26199 requests_made.load(atomic::Ordering::Acquire),
26200 "Should query for colors once per save only, as formatting was not requested"
26201 );
26202 editor.update(cx, |editor, cx| {
26203 assert_eq!(
26204 Vec::<Rgba>::new(),
26205 extract_color_inlays(editor, cx),
26206 "Should clear all colors when the server returns an empty response"
26207 );
26208 });
26209}
26210
26211#[gpui::test]
26212async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26213 init_test(cx, |_| {});
26214 let (editor, cx) = cx.add_window_view(Editor::single_line);
26215 editor.update_in(cx, |editor, window, cx| {
26216 editor.set_text("oops\n\nwow\n", window, cx)
26217 });
26218 cx.run_until_parked();
26219 editor.update(cx, |editor, cx| {
26220 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26221 });
26222 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26223 cx.run_until_parked();
26224 editor.update(cx, |editor, cx| {
26225 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26226 });
26227}
26228
26229#[gpui::test]
26230async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26231 init_test(cx, |_| {});
26232
26233 cx.update(|cx| {
26234 register_project_item::<Editor>(cx);
26235 });
26236
26237 let fs = FakeFs::new(cx.executor());
26238 fs.insert_tree("/root1", json!({})).await;
26239 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26240 .await;
26241
26242 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26243 let (workspace, cx) =
26244 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26245
26246 let worktree_id = project.update(cx, |project, cx| {
26247 project.worktrees(cx).next().unwrap().read(cx).id()
26248 });
26249
26250 let handle = workspace
26251 .update_in(cx, |workspace, window, cx| {
26252 let project_path = (worktree_id, rel_path("one.pdf"));
26253 workspace.open_path(project_path, None, true, window, cx)
26254 })
26255 .await
26256 .unwrap();
26257
26258 assert_eq!(
26259 handle.to_any().entity_type(),
26260 TypeId::of::<InvalidItemView>()
26261 );
26262}
26263
26264#[gpui::test]
26265async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26266 init_test(cx, |_| {});
26267
26268 let language = Arc::new(Language::new(
26269 LanguageConfig::default(),
26270 Some(tree_sitter_rust::LANGUAGE.into()),
26271 ));
26272
26273 // Test hierarchical sibling navigation
26274 let text = r#"
26275 fn outer() {
26276 if condition {
26277 let a = 1;
26278 }
26279 let b = 2;
26280 }
26281
26282 fn another() {
26283 let c = 3;
26284 }
26285 "#;
26286
26287 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26288 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26289 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26290
26291 // Wait for parsing to complete
26292 editor
26293 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26294 .await;
26295
26296 editor.update_in(cx, |editor, window, cx| {
26297 // Start by selecting "let a = 1;" inside the if block
26298 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26299 s.select_display_ranges([
26300 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26301 ]);
26302 });
26303
26304 let initial_selection = editor.selections.display_ranges(cx);
26305 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26306
26307 // Test select next sibling - should move up levels to find the next sibling
26308 // Since "let a = 1;" has no siblings in the if block, it should move up
26309 // to find "let b = 2;" which is a sibling of the if block
26310 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26311 let next_selection = editor.selections.display_ranges(cx);
26312
26313 // Should have a selection and it should be different from the initial
26314 assert_eq!(
26315 next_selection.len(),
26316 1,
26317 "Should have one selection after next"
26318 );
26319 assert_ne!(
26320 next_selection[0], initial_selection[0],
26321 "Next sibling selection should be different"
26322 );
26323
26324 // Test hierarchical navigation by going to the end of the current function
26325 // and trying to navigate to the next function
26326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26327 s.select_display_ranges([
26328 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26329 ]);
26330 });
26331
26332 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26333 let function_next_selection = editor.selections.display_ranges(cx);
26334
26335 // Should move to the next function
26336 assert_eq!(
26337 function_next_selection.len(),
26338 1,
26339 "Should have one selection after function next"
26340 );
26341
26342 // Test select previous sibling navigation
26343 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26344 let prev_selection = editor.selections.display_ranges(cx);
26345
26346 // Should have a selection and it should be different
26347 assert_eq!(
26348 prev_selection.len(),
26349 1,
26350 "Should have one selection after prev"
26351 );
26352 assert_ne!(
26353 prev_selection[0], function_next_selection[0],
26354 "Previous sibling selection should be different from next"
26355 );
26356 });
26357}
26358
26359#[gpui::test]
26360async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26361 init_test(cx, |_| {});
26362
26363 let mut cx = EditorTestContext::new(cx).await;
26364 cx.set_state(
26365 "let ˇvariable = 42;
26366let another = variable + 1;
26367let result = variable * 2;",
26368 );
26369
26370 // Set up document highlights manually (simulating LSP response)
26371 cx.update_editor(|editor, _window, cx| {
26372 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26373
26374 // Create highlights for "variable" occurrences
26375 let highlight_ranges = [
26376 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26377 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26378 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26379 ];
26380
26381 let anchor_ranges: Vec<_> = highlight_ranges
26382 .iter()
26383 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26384 .collect();
26385
26386 editor.highlight_background::<DocumentHighlightRead>(
26387 &anchor_ranges,
26388 |theme| theme.colors().editor_document_highlight_read_background,
26389 cx,
26390 );
26391 });
26392
26393 // Go to next highlight - should move to second "variable"
26394 cx.update_editor(|editor, window, cx| {
26395 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26396 });
26397 cx.assert_editor_state(
26398 "let variable = 42;
26399let another = ˇvariable + 1;
26400let result = variable * 2;",
26401 );
26402
26403 // Go to next highlight - should move to third "variable"
26404 cx.update_editor(|editor, window, cx| {
26405 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26406 });
26407 cx.assert_editor_state(
26408 "let variable = 42;
26409let another = variable + 1;
26410let result = ˇvariable * 2;",
26411 );
26412
26413 // Go to next highlight - should stay at third "variable" (no wrap-around)
26414 cx.update_editor(|editor, window, cx| {
26415 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26416 });
26417 cx.assert_editor_state(
26418 "let variable = 42;
26419let another = variable + 1;
26420let result = ˇvariable * 2;",
26421 );
26422
26423 // Now test going backwards from third position
26424 cx.update_editor(|editor, window, cx| {
26425 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26426 });
26427 cx.assert_editor_state(
26428 "let variable = 42;
26429let another = ˇvariable + 1;
26430let result = variable * 2;",
26431 );
26432
26433 // Go to previous highlight - should move to first "variable"
26434 cx.update_editor(|editor, window, cx| {
26435 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26436 });
26437 cx.assert_editor_state(
26438 "let ˇvariable = 42;
26439let another = variable + 1;
26440let result = variable * 2;",
26441 );
26442
26443 // Go to previous highlight - should stay on first "variable"
26444 cx.update_editor(|editor, window, cx| {
26445 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26446 });
26447 cx.assert_editor_state(
26448 "let ˇvariable = 42;
26449let another = variable + 1;
26450let result = variable * 2;",
26451 );
26452}
26453
26454#[gpui::test]
26455async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26456 cx: &mut gpui::TestAppContext,
26457) {
26458 init_test(cx, |_| {});
26459
26460 let url = "https://zed.dev";
26461
26462 let markdown_language = Arc::new(Language::new(
26463 LanguageConfig {
26464 name: "Markdown".into(),
26465 ..LanguageConfig::default()
26466 },
26467 None,
26468 ));
26469
26470 let mut cx = EditorTestContext::new(cx).await;
26471 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26472 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26473
26474 cx.update_editor(|editor, window, cx| {
26475 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26476 editor.paste(&Paste, window, cx);
26477 });
26478
26479 cx.assert_editor_state(&format!(
26480 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26481 ));
26482}
26483
26484#[gpui::test]
26485async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26486 cx: &mut gpui::TestAppContext,
26487) {
26488 init_test(cx, |_| {});
26489
26490 let url = "https://zed.dev";
26491
26492 let markdown_language = Arc::new(Language::new(
26493 LanguageConfig {
26494 name: "Markdown".into(),
26495 ..LanguageConfig::default()
26496 },
26497 None,
26498 ));
26499
26500 let mut cx = EditorTestContext::new(cx).await;
26501 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26502 cx.set_state(&format!(
26503 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26504 ));
26505
26506 cx.update_editor(|editor, window, cx| {
26507 editor.copy(&Copy, window, cx);
26508 });
26509
26510 cx.set_state(&format!(
26511 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26512 ));
26513
26514 cx.update_editor(|editor, window, cx| {
26515 editor.paste(&Paste, window, cx);
26516 });
26517
26518 cx.assert_editor_state(&format!(
26519 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26520 ));
26521}
26522
26523#[gpui::test]
26524async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26525 cx: &mut gpui::TestAppContext,
26526) {
26527 init_test(cx, |_| {});
26528
26529 let url = "https://zed.dev";
26530
26531 let markdown_language = Arc::new(Language::new(
26532 LanguageConfig {
26533 name: "Markdown".into(),
26534 ..LanguageConfig::default()
26535 },
26536 None,
26537 ));
26538
26539 let mut cx = EditorTestContext::new(cx).await;
26540 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26541 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26542
26543 cx.update_editor(|editor, window, cx| {
26544 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26545 editor.paste(&Paste, window, cx);
26546 });
26547
26548 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26549}
26550
26551#[gpui::test]
26552async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26553 cx: &mut gpui::TestAppContext,
26554) {
26555 init_test(cx, |_| {});
26556
26557 let text = "Awesome";
26558
26559 let markdown_language = Arc::new(Language::new(
26560 LanguageConfig {
26561 name: "Markdown".into(),
26562 ..LanguageConfig::default()
26563 },
26564 None,
26565 ));
26566
26567 let mut cx = EditorTestContext::new(cx).await;
26568 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26569 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26570
26571 cx.update_editor(|editor, window, cx| {
26572 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26573 editor.paste(&Paste, window, cx);
26574 });
26575
26576 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26577}
26578
26579#[gpui::test]
26580async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26581 cx: &mut gpui::TestAppContext,
26582) {
26583 init_test(cx, |_| {});
26584
26585 let url = "https://zed.dev";
26586
26587 let markdown_language = Arc::new(Language::new(
26588 LanguageConfig {
26589 name: "Rust".into(),
26590 ..LanguageConfig::default()
26591 },
26592 None,
26593 ));
26594
26595 let mut cx = EditorTestContext::new(cx).await;
26596 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26597 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26598
26599 cx.update_editor(|editor, window, cx| {
26600 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26601 editor.paste(&Paste, window, cx);
26602 });
26603
26604 cx.assert_editor_state(&format!(
26605 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26606 ));
26607}
26608
26609#[gpui::test]
26610async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26611 cx: &mut TestAppContext,
26612) {
26613 init_test(cx, |_| {});
26614
26615 let url = "https://zed.dev";
26616
26617 let markdown_language = Arc::new(Language::new(
26618 LanguageConfig {
26619 name: "Markdown".into(),
26620 ..LanguageConfig::default()
26621 },
26622 None,
26623 ));
26624
26625 let (editor, cx) = cx.add_window_view(|window, cx| {
26626 let multi_buffer = MultiBuffer::build_multi(
26627 [
26628 ("this will embed -> link", vec![Point::row_range(0..1)]),
26629 ("this will replace -> link", vec![Point::row_range(0..1)]),
26630 ],
26631 cx,
26632 );
26633 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26635 s.select_ranges(vec![
26636 Point::new(0, 19)..Point::new(0, 23),
26637 Point::new(1, 21)..Point::new(1, 25),
26638 ])
26639 });
26640 let first_buffer_id = multi_buffer
26641 .read(cx)
26642 .excerpt_buffer_ids()
26643 .into_iter()
26644 .next()
26645 .unwrap();
26646 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26647 first_buffer.update(cx, |buffer, cx| {
26648 buffer.set_language(Some(markdown_language.clone()), cx);
26649 });
26650
26651 editor
26652 });
26653 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26654
26655 cx.update_editor(|editor, window, cx| {
26656 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26657 editor.paste(&Paste, window, cx);
26658 });
26659
26660 cx.assert_editor_state(&format!(
26661 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26662 ));
26663}
26664
26665#[gpui::test]
26666async fn test_non_linux_line_endings_registration(cx: &mut TestAppContext) {
26667 init_test(cx, |_| {});
26668
26669 let unix_newlines_file_text = "fn main() {
26670 let a = 5;
26671 }";
26672 let clrf_file_text = unix_newlines_file_text.lines().join("\r\n");
26673
26674 let fs = FakeFs::new(cx.executor());
26675 fs.insert_tree(
26676 path!("/a"),
26677 json!({
26678 "first.rs": &clrf_file_text,
26679 }),
26680 )
26681 .await;
26682
26683 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26684 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26685 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26686
26687 let registered_text = Arc::new(Mutex::new(Vec::new()));
26688 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26689 language_registry.add(rust_lang());
26690 let mut fake_servers = language_registry.register_fake_lsp(
26691 "Rust",
26692 FakeLspAdapter {
26693 capabilities: lsp::ServerCapabilities {
26694 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26695 ..lsp::ServerCapabilities::default()
26696 },
26697 name: "rust-analyzer",
26698 initializer: Some({
26699 let registered_text = registered_text.clone();
26700 Box::new(move |fake_server| {
26701 fake_server.handle_notification::<lsp::notification::DidOpenTextDocument, _>({
26702 let registered_text = registered_text.clone();
26703 move |params, _| {
26704 registered_text.lock().push(params.text_document.text);
26705 }
26706 });
26707 })
26708 }),
26709 ..FakeLspAdapter::default()
26710 },
26711 );
26712
26713 let editor = workspace
26714 .update(cx, |workspace, window, cx| {
26715 workspace.open_abs_path(
26716 PathBuf::from(path!("/a/first.rs")),
26717 OpenOptions::default(),
26718 window,
26719 cx,
26720 )
26721 })
26722 .unwrap()
26723 .await
26724 .unwrap()
26725 .downcast::<Editor>()
26726 .unwrap();
26727 let _fake_language_server = fake_servers.next().await.unwrap();
26728 cx.executor().run_until_parked();
26729
26730 assert_eq!(
26731 editor.update(cx, |editor, cx| editor.text(cx)),
26732 unix_newlines_file_text,
26733 "Default text API returns \n-separated text",
26734 );
26735 assert_eq!(
26736 vec![clrf_file_text],
26737 registered_text.lock().drain(..).collect::<Vec<_>>(),
26738 "Expected the language server to receive the exact same text from the FS",
26739 );
26740}
26741
26742#[gpui::test]
26743async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26744 init_test(cx, |_| {});
26745
26746 let fs = FakeFs::new(cx.executor());
26747 fs.insert_tree(
26748 path!("/project"),
26749 json!({
26750 "first.rs": "# First Document\nSome content here.",
26751 "second.rs": "Plain text content for second file.",
26752 }),
26753 )
26754 .await;
26755
26756 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26757 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26758 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26759
26760 let language = rust_lang();
26761 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26762 language_registry.add(language.clone());
26763 let mut fake_servers = language_registry.register_fake_lsp(
26764 "Rust",
26765 FakeLspAdapter {
26766 ..FakeLspAdapter::default()
26767 },
26768 );
26769
26770 let buffer1 = project
26771 .update(cx, |project, cx| {
26772 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26773 })
26774 .await
26775 .unwrap();
26776 let buffer2 = project
26777 .update(cx, |project, cx| {
26778 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26779 })
26780 .await
26781 .unwrap();
26782
26783 let multi_buffer = cx.new(|cx| {
26784 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26785 multi_buffer.set_excerpts_for_path(
26786 PathKey::for_buffer(&buffer1, cx),
26787 buffer1.clone(),
26788 [Point::zero()..buffer1.read(cx).max_point()],
26789 3,
26790 cx,
26791 );
26792 multi_buffer.set_excerpts_for_path(
26793 PathKey::for_buffer(&buffer2, cx),
26794 buffer2.clone(),
26795 [Point::zero()..buffer1.read(cx).max_point()],
26796 3,
26797 cx,
26798 );
26799 multi_buffer
26800 });
26801
26802 let (editor, cx) = cx.add_window_view(|window, cx| {
26803 Editor::new(
26804 EditorMode::full(),
26805 multi_buffer,
26806 Some(project.clone()),
26807 window,
26808 cx,
26809 )
26810 });
26811
26812 let fake_language_server = fake_servers.next().await.unwrap();
26813
26814 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26815
26816 let save = editor.update_in(cx, |editor, window, cx| {
26817 assert!(editor.is_dirty(cx));
26818
26819 editor.save(
26820 SaveOptions {
26821 format: true,
26822 autosave: true,
26823 },
26824 project,
26825 window,
26826 cx,
26827 )
26828 });
26829 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26830 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26831 let mut done_edit_rx = Some(done_edit_rx);
26832 let mut start_edit_tx = Some(start_edit_tx);
26833
26834 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26835 start_edit_tx.take().unwrap().send(()).unwrap();
26836 let done_edit_rx = done_edit_rx.take().unwrap();
26837 async move {
26838 done_edit_rx.await.unwrap();
26839 Ok(None)
26840 }
26841 });
26842
26843 start_edit_rx.await.unwrap();
26844 buffer2
26845 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26846 .unwrap();
26847
26848 done_edit_tx.send(()).unwrap();
26849
26850 save.await.unwrap();
26851 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26852}
26853
26854#[track_caller]
26855fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26856 editor
26857 .all_inlays(cx)
26858 .into_iter()
26859 .filter_map(|inlay| inlay.get_color())
26860 .map(Rgba::from)
26861 .collect()
26862}
26863
26864#[gpui::test]
26865fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26866 init_test(cx, |_| {});
26867
26868 let editor = cx.add_window(|window, cx| {
26869 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26870 build_editor(buffer, window, cx)
26871 });
26872
26873 editor
26874 .update(cx, |editor, window, cx| {
26875 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26876 s.select_display_ranges([
26877 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26878 ])
26879 });
26880
26881 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26882
26883 assert_eq!(
26884 editor.display_text(cx),
26885 "line1\nline2\nline2",
26886 "Duplicating last line upward should create duplicate above, not on same line"
26887 );
26888
26889 assert_eq!(
26890 editor.selections.display_ranges(cx),
26891 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26892 "Selection should move to the duplicated line"
26893 );
26894 })
26895 .unwrap();
26896}
26897
26898#[gpui::test]
26899async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26900 init_test(cx, |_| {});
26901
26902 let mut cx = EditorTestContext::new(cx).await;
26903
26904 cx.set_state("line1\nline2ˇ");
26905
26906 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26907
26908 let clipboard_text = cx
26909 .read_from_clipboard()
26910 .and_then(|item| item.text().as_deref().map(str::to_string));
26911
26912 assert_eq!(
26913 clipboard_text,
26914 Some("line2\n".to_string()),
26915 "Copying a line without trailing newline should include a newline"
26916 );
26917
26918 cx.set_state("line1\nˇ");
26919
26920 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26921
26922 cx.assert_editor_state("line1\nline2\nˇ");
26923}
26924
26925#[gpui::test]
26926async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26927 init_test(cx, |_| {});
26928
26929 let mut cx = EditorTestContext::new(cx).await;
26930
26931 cx.set_state("line1\nline2ˇ");
26932 cx.update_editor(|e, window, cx| {
26933 e.set_mode(EditorMode::SingleLine);
26934 assert!(e.key_context(window, cx).contains("end_of_input"));
26935 });
26936 cx.set_state("ˇline1\nline2");
26937 cx.update_editor(|e, window, cx| {
26938 assert!(!e.key_context(window, cx).contains("end_of_input"));
26939 });
26940 cx.set_state("line1ˇ\nline2");
26941 cx.update_editor(|e, window, cx| {
26942 assert!(!e.key_context(window, cx).contains("end_of_input"));
26943 });
26944}