1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use languages::rust_lang;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::build_editor_with_project;
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_item_view::InvalidItemView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(
224 editor.selections.ranges(&editor.display_snapshot(cx)),
225 vec![4..4]
226 );
227
228 editor.start_transaction_at(now, window, cx);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([4..5])
231 });
232 editor.insert("e", window, cx);
233 editor.end_transaction_at(now, cx);
234 assert_eq!(editor.text(cx), "12cde6");
235 assert_eq!(
236 editor.selections.ranges(&editor.display_snapshot(cx)),
237 vec![5..5]
238 );
239
240 now += group_interval + Duration::from_millis(1);
241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
242 s.select_ranges([2..2])
243 });
244
245 // Simulate an edit in another editor
246 buffer.update(cx, |buffer, cx| {
247 buffer.start_transaction_at(now, cx);
248 buffer.edit([(0..1, "a")], None, cx);
249 buffer.edit([(1..1, "b")], None, cx);
250 buffer.end_transaction_at(now, cx);
251 });
252
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(
255 editor.selections.ranges(&editor.display_snapshot(cx)),
256 vec![3..3]
257 );
258
259 // Last transaction happened past the group interval in a different editor.
260 // Undo it individually and don't restore selections.
261 editor.undo(&Undo, window, cx);
262 assert_eq!(editor.text(cx), "12cde6");
263 assert_eq!(
264 editor.selections.ranges(&editor.display_snapshot(cx)),
265 vec![2..2]
266 );
267
268 // First two transactions happened within the group interval in this editor.
269 // Undo them together and restore selections.
270 editor.undo(&Undo, window, cx);
271 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
272 assert_eq!(editor.text(cx), "123456");
273 assert_eq!(
274 editor.selections.ranges(&editor.display_snapshot(cx)),
275 vec![0..0]
276 );
277
278 // Redo the first two transactions together.
279 editor.redo(&Redo, window, cx);
280 assert_eq!(editor.text(cx), "12cde6");
281 assert_eq!(
282 editor.selections.ranges(&editor.display_snapshot(cx)),
283 vec![5..5]
284 );
285
286 // Redo the last transaction on its own.
287 editor.redo(&Redo, window, cx);
288 assert_eq!(editor.text(cx), "ab2cde6");
289 assert_eq!(
290 editor.selections.ranges(&editor.display_snapshot(cx)),
291 vec![6..6]
292 );
293
294 // Test empty transactions.
295 editor.start_transaction_at(now, window, cx);
296 editor.end_transaction_at(now, cx);
297 editor.undo(&Undo, window, cx);
298 assert_eq!(editor.text(cx), "12cde6");
299 });
300}
301
302#[gpui::test]
303fn test_ime_composition(cx: &mut TestAppContext) {
304 init_test(cx, |_| {});
305
306 let buffer = cx.new(|cx| {
307 let mut buffer = language::Buffer::local("abcde", cx);
308 // Ensure automatic grouping doesn't occur.
309 buffer.set_group_interval(Duration::ZERO);
310 buffer
311 });
312
313 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
314 cx.add_window(|window, cx| {
315 let mut editor = build_editor(buffer.clone(), window, cx);
316
317 // Start a new IME composition.
318 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
319 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
320 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
321 assert_eq!(editor.text(cx), "äbcde");
322 assert_eq!(
323 editor.marked_text_ranges(cx),
324 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
325 );
326
327 // Finalize IME composition.
328 editor.replace_text_in_range(None, "ā", window, cx);
329 assert_eq!(editor.text(cx), "ābcde");
330 assert_eq!(editor.marked_text_ranges(cx), None);
331
332 // IME composition edits are grouped and are undone/redone at once.
333 editor.undo(&Default::default(), window, cx);
334 assert_eq!(editor.text(cx), "abcde");
335 assert_eq!(editor.marked_text_ranges(cx), None);
336 editor.redo(&Default::default(), window, cx);
337 assert_eq!(editor.text(cx), "ābcde");
338 assert_eq!(editor.marked_text_ranges(cx), None);
339
340 // Start a new IME composition.
341 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
342 assert_eq!(
343 editor.marked_text_ranges(cx),
344 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
345 );
346
347 // Undoing during an IME composition cancels it.
348 editor.undo(&Default::default(), window, cx);
349 assert_eq!(editor.text(cx), "ābcde");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
353 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
354 assert_eq!(editor.text(cx), "ābcdè");
355 assert_eq!(
356 editor.marked_text_ranges(cx),
357 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
358 );
359
360 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
361 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
362 assert_eq!(editor.text(cx), "ābcdę");
363 assert_eq!(editor.marked_text_ranges(cx), None);
364
365 // Start a new IME composition with multiple cursors.
366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
367 s.select_ranges([
368 OffsetUtf16(1)..OffsetUtf16(1),
369 OffsetUtf16(3)..OffsetUtf16(3),
370 OffsetUtf16(5)..OffsetUtf16(5),
371 ])
372 });
373 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
374 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
375 assert_eq!(
376 editor.marked_text_ranges(cx),
377 Some(vec![
378 OffsetUtf16(0)..OffsetUtf16(3),
379 OffsetUtf16(4)..OffsetUtf16(7),
380 OffsetUtf16(8)..OffsetUtf16(11)
381 ])
382 );
383
384 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
385 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
386 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
387 assert_eq!(
388 editor.marked_text_ranges(cx),
389 Some(vec![
390 OffsetUtf16(1)..OffsetUtf16(2),
391 OffsetUtf16(5)..OffsetUtf16(6),
392 OffsetUtf16(9)..OffsetUtf16(10)
393 ])
394 );
395
396 // Finalize IME composition with multiple cursors.
397 editor.replace_text_in_range(Some(9..10), "2", window, cx);
398 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
399 assert_eq!(editor.marked_text_ranges(cx), None);
400
401 editor
402 });
403}
404
405#[gpui::test]
406fn test_selection_with_mouse(cx: &mut TestAppContext) {
407 init_test(cx, |_| {});
408
409 let editor = cx.add_window(|window, cx| {
410 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
411 build_editor(buffer, window, cx)
412 });
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
416 });
417 assert_eq!(
418 editor
419 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
420 .unwrap(),
421 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
422 );
423
424 _ = editor.update(cx, |editor, window, cx| {
425 editor.update_selection(
426 DisplayPoint::new(DisplayRow(3), 3),
427 0,
428 gpui::Point::<f32>::default(),
429 window,
430 cx,
431 );
432 });
433
434 assert_eq!(
435 editor
436 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
437 .unwrap(),
438 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
439 );
440
441 _ = editor.update(cx, |editor, window, cx| {
442 editor.update_selection(
443 DisplayPoint::new(DisplayRow(1), 1),
444 0,
445 gpui::Point::<f32>::default(),
446 window,
447 cx,
448 );
449 });
450
451 assert_eq!(
452 editor
453 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
454 .unwrap(),
455 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
456 );
457
458 _ = editor.update(cx, |editor, window, cx| {
459 editor.end_selection(window, cx);
460 editor.update_selection(
461 DisplayPoint::new(DisplayRow(3), 3),
462 0,
463 gpui::Point::<f32>::default(),
464 window,
465 cx,
466 );
467 });
468
469 assert_eq!(
470 editor
471 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
472 .unwrap(),
473 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
478 editor.update_selection(
479 DisplayPoint::new(DisplayRow(0), 0),
480 0,
481 gpui::Point::<f32>::default(),
482 window,
483 cx,
484 );
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
493 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.end_selection(window, cx);
499 });
500
501 assert_eq!(
502 editor
503 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
504 .unwrap(),
505 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
506 );
507}
508
509#[gpui::test]
510fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
511 init_test(cx, |_| {});
512
513 let editor = cx.add_window(|window, cx| {
514 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
515 build_editor(buffer, window, cx)
516 });
517
518 _ = editor.update(cx, |editor, window, cx| {
519 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.end_selection(window, cx);
532 });
533
534 assert_eq!(
535 editor
536 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
537 .unwrap(),
538 [
539 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
540 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
541 ]
542 );
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.end_selection(window, cx);
550 });
551
552 assert_eq!(
553 editor
554 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
555 .unwrap(),
556 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
557 );
558}
559
560#[gpui::test]
561fn test_canceling_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575 });
576
577 _ = editor.update(cx, |editor, window, cx| {
578 editor.update_selection(
579 DisplayPoint::new(DisplayRow(3), 3),
580 0,
581 gpui::Point::<f32>::default(),
582 window,
583 cx,
584 );
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
588 );
589 });
590
591 _ = editor.update(cx, |editor, window, cx| {
592 editor.cancel(&Cancel, window, cx);
593 editor.update_selection(
594 DisplayPoint::new(DisplayRow(1), 1),
595 0,
596 gpui::Point::<f32>::default(),
597 window,
598 cx,
599 );
600 assert_eq!(
601 editor.selections.display_ranges(cx),
602 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
603 );
604 });
605}
606
607#[gpui::test]
608fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
609 init_test(cx, |_| {});
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
618 assert_eq!(
619 editor.selections.display_ranges(cx),
620 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
621 );
622
623 editor.move_down(&Default::default(), window, cx);
624 assert_eq!(
625 editor.selections.display_ranges(cx),
626 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
627 );
628
629 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
630 assert_eq!(
631 editor.selections.display_ranges(cx),
632 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
633 );
634
635 editor.move_up(&Default::default(), window, cx);
636 assert_eq!(
637 editor.selections.display_ranges(cx),
638 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
639 );
640 });
641}
642
643#[gpui::test]
644fn test_extending_selection(cx: &mut TestAppContext) {
645 init_test(cx, |_| {});
646
647 let editor = cx.add_window(|window, cx| {
648 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
649 build_editor(buffer, window, cx)
650 });
651
652 _ = editor.update(cx, |editor, window, cx| {
653 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
654 editor.end_selection(window, cx);
655 assert_eq!(
656 editor.selections.display_ranges(cx),
657 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
658 );
659
660 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 editor.selections.display_ranges(cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
670 assert_eq!(
671 editor.selections.display_ranges(cx),
672 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
673 );
674
675 editor.update_selection(
676 DisplayPoint::new(DisplayRow(0), 1),
677 0,
678 gpui::Point::<f32>::default(),
679 window,
680 cx,
681 );
682 editor.end_selection(window, cx);
683 assert_eq!(
684 editor.selections.display_ranges(cx),
685 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
686 );
687
688 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
689 editor.end_selection(window, cx);
690 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
691 editor.end_selection(window, cx);
692 assert_eq!(
693 editor.selections.display_ranges(cx),
694 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
695 );
696
697 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
698 assert_eq!(
699 editor.selections.display_ranges(cx),
700 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
701 );
702
703 editor.update_selection(
704 DisplayPoint::new(DisplayRow(0), 6),
705 0,
706 gpui::Point::<f32>::default(),
707 window,
708 cx,
709 );
710 assert_eq!(
711 editor.selections.display_ranges(cx),
712 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
713 );
714
715 editor.update_selection(
716 DisplayPoint::new(DisplayRow(0), 1),
717 0,
718 gpui::Point::<f32>::default(),
719 window,
720 cx,
721 );
722 editor.end_selection(window, cx);
723 assert_eq!(
724 editor.selections.display_ranges(cx),
725 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
726 );
727 });
728}
729
730#[gpui::test]
731fn test_clone(cx: &mut TestAppContext) {
732 init_test(cx, |_| {});
733
734 let (text, selection_ranges) = marked_text_ranges(
735 indoc! {"
736 one
737 two
738 threeˇ
739 four
740 fiveˇ
741 "},
742 true,
743 );
744
745 let editor = cx.add_window(|window, cx| {
746 let buffer = MultiBuffer::build_simple(&text, cx);
747 build_editor(buffer, window, cx)
748 });
749
750 _ = editor.update(cx, |editor, window, cx| {
751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
752 s.select_ranges(selection_ranges.clone())
753 });
754 editor.fold_creases(
755 vec![
756 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
757 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
758 ],
759 true,
760 window,
761 cx,
762 );
763 });
764
765 let cloned_editor = editor
766 .update(cx, |editor, _, cx| {
767 cx.open_window(Default::default(), |window, cx| {
768 cx.new(|cx| editor.clone(window, cx))
769 })
770 })
771 .unwrap()
772 .unwrap();
773
774 let snapshot = editor
775 .update(cx, |e, window, cx| e.snapshot(window, cx))
776 .unwrap();
777 let cloned_snapshot = cloned_editor
778 .update(cx, |e, window, cx| e.snapshot(window, cx))
779 .unwrap();
780
781 assert_eq!(
782 cloned_editor
783 .update(cx, |e, _, cx| e.display_text(cx))
784 .unwrap(),
785 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
786 );
787 assert_eq!(
788 cloned_snapshot
789 .folds_in_range(0..text.len())
790 .collect::<Vec<_>>(),
791 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
792 );
793 assert_set_eq!(
794 cloned_editor
795 .update(cx, |editor, _, cx| editor
796 .selections
797 .ranges::<Point>(&editor.display_snapshot(cx)))
798 .unwrap(),
799 editor
800 .update(cx, |editor, _, cx| editor
801 .selections
802 .ranges(&editor.display_snapshot(cx)))
803 .unwrap()
804 );
805 assert_set_eq!(
806 cloned_editor
807 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
808 .unwrap(),
809 editor
810 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
811 .unwrap()
812 );
813}
814
815#[gpui::test]
816async fn test_navigation_history(cx: &mut TestAppContext) {
817 init_test(cx, |_| {});
818
819 use workspace::item::Item;
820
821 let fs = FakeFs::new(cx.executor());
822 let project = Project::test(fs, [], cx).await;
823 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
824 let pane = workspace
825 .update(cx, |workspace, _, _| workspace.active_pane().clone())
826 .unwrap();
827
828 _ = workspace.update(cx, |_v, window, cx| {
829 cx.new(|cx| {
830 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
831 let mut editor = build_editor(buffer, window, cx);
832 let handle = cx.entity();
833 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
834
835 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
836 editor.nav_history.as_mut().unwrap().pop_backward(cx)
837 }
838
839 // Move the cursor a small distance.
840 // Nothing is added to the navigation history.
841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
842 s.select_display_ranges([
843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
844 ])
845 });
846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
847 s.select_display_ranges([
848 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
849 ])
850 });
851 assert!(pop_history(&mut editor, cx).is_none());
852
853 // Move the cursor a large distance.
854 // The history can jump back to the previous position.
855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
856 s.select_display_ranges([
857 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
858 ])
859 });
860 let nav_entry = pop_history(&mut editor, cx).unwrap();
861 editor.navigate(nav_entry.data.unwrap(), window, cx);
862 assert_eq!(nav_entry.item.id(), cx.entity_id());
863 assert_eq!(
864 editor.selections.display_ranges(cx),
865 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
866 );
867 assert!(pop_history(&mut editor, cx).is_none());
868
869 // Move the cursor a small distance via the mouse.
870 // Nothing is added to the navigation history.
871 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
872 editor.end_selection(window, cx);
873 assert_eq!(
874 editor.selections.display_ranges(cx),
875 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
876 );
877 assert!(pop_history(&mut editor, cx).is_none());
878
879 // Move the cursor a large distance via the mouse.
880 // The history can jump back to the previous position.
881 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
882 editor.end_selection(window, cx);
883 assert_eq!(
884 editor.selections.display_ranges(cx),
885 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
886 );
887 let nav_entry = pop_history(&mut editor, cx).unwrap();
888 editor.navigate(nav_entry.data.unwrap(), window, cx);
889 assert_eq!(nav_entry.item.id(), cx.entity_id());
890 assert_eq!(
891 editor.selections.display_ranges(cx),
892 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
893 );
894 assert!(pop_history(&mut editor, cx).is_none());
895
896 // Set scroll position to check later
897 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
898 let original_scroll_position = editor.scroll_manager.anchor();
899
900 // Jump to the end of the document and adjust scroll
901 editor.move_to_end(&MoveToEnd, window, cx);
902 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
903 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
904
905 let nav_entry = pop_history(&mut editor, cx).unwrap();
906 editor.navigate(nav_entry.data.unwrap(), window, cx);
907 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
908
909 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
910 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
911 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
912 let invalid_point = Point::new(9999, 0);
913 editor.navigate(
914 Box::new(NavigationData {
915 cursor_anchor: invalid_anchor,
916 cursor_position: invalid_point,
917 scroll_anchor: ScrollAnchor {
918 anchor: invalid_anchor,
919 offset: Default::default(),
920 },
921 scroll_top_row: invalid_point.row,
922 }),
923 window,
924 cx,
925 );
926 assert_eq!(
927 editor.selections.display_ranges(cx),
928 &[editor.max_point(cx)..editor.max_point(cx)]
929 );
930 assert_eq!(
931 editor.scroll_position(cx),
932 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
933 );
934
935 editor
936 })
937 });
938}
939
940#[gpui::test]
941fn test_cancel(cx: &mut TestAppContext) {
942 init_test(cx, |_| {});
943
944 let editor = cx.add_window(|window, cx| {
945 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
946 build_editor(buffer, window, cx)
947 });
948
949 _ = editor.update(cx, |editor, window, cx| {
950 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
951 editor.update_selection(
952 DisplayPoint::new(DisplayRow(1), 1),
953 0,
954 gpui::Point::<f32>::default(),
955 window,
956 cx,
957 );
958 editor.end_selection(window, cx);
959
960 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
961 editor.update_selection(
962 DisplayPoint::new(DisplayRow(0), 3),
963 0,
964 gpui::Point::<f32>::default(),
965 window,
966 cx,
967 );
968 editor.end_selection(window, cx);
969 assert_eq!(
970 editor.selections.display_ranges(cx),
971 [
972 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
973 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
974 ]
975 );
976 });
977
978 _ = editor.update(cx, |editor, window, cx| {
979 editor.cancel(&Cancel, window, cx);
980 assert_eq!(
981 editor.selections.display_ranges(cx),
982 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
983 );
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.cancel(&Cancel, window, cx);
988 assert_eq!(
989 editor.selections.display_ranges(cx),
990 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
991 );
992 });
993}
994
995#[gpui::test]
996fn test_fold_action(cx: &mut TestAppContext) {
997 init_test(cx, |_| {});
998
999 let editor = cx.add_window(|window, cx| {
1000 let buffer = MultiBuffer::build_simple(
1001 &"
1002 impl Foo {
1003 // Hello!
1004
1005 fn a() {
1006 1
1007 }
1008
1009 fn b() {
1010 2
1011 }
1012
1013 fn c() {
1014 3
1015 }
1016 }
1017 "
1018 .unindent(),
1019 cx,
1020 );
1021 build_editor(buffer, window, cx)
1022 });
1023
1024 _ = editor.update(cx, |editor, window, cx| {
1025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1026 s.select_display_ranges([
1027 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1028 ]);
1029 });
1030 editor.fold(&Fold, window, cx);
1031 assert_eq!(
1032 editor.display_text(cx),
1033 "
1034 impl Foo {
1035 // Hello!
1036
1037 fn a() {
1038 1
1039 }
1040
1041 fn b() {⋯
1042 }
1043
1044 fn c() {⋯
1045 }
1046 }
1047 "
1048 .unindent(),
1049 );
1050
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {⋯
1056 }
1057 "
1058 .unindent(),
1059 );
1060
1061 editor.unfold_lines(&UnfoldLines, window, cx);
1062 assert_eq!(
1063 editor.display_text(cx),
1064 "
1065 impl Foo {
1066 // Hello!
1067
1068 fn a() {
1069 1
1070 }
1071
1072 fn b() {⋯
1073 }
1074
1075 fn c() {⋯
1076 }
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 editor.buffer.read(cx).read(cx).text()
1086 );
1087 });
1088}
1089
1090#[gpui::test]
1091fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1092 init_test(cx, |_| {});
1093
1094 let editor = cx.add_window(|window, cx| {
1095 let buffer = MultiBuffer::build_simple(
1096 &"
1097 class Foo:
1098 # Hello!
1099
1100 def a():
1101 print(1)
1102
1103 def b():
1104 print(2)
1105
1106 def c():
1107 print(3)
1108 "
1109 .unindent(),
1110 cx,
1111 );
1112 build_editor(buffer, window, cx)
1113 });
1114
1115 _ = editor.update(cx, |editor, window, cx| {
1116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1117 s.select_display_ranges([
1118 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1119 ]);
1120 });
1121 editor.fold(&Fold, window, cx);
1122 assert_eq!(
1123 editor.display_text(cx),
1124 "
1125 class Foo:
1126 # Hello!
1127
1128 def a():
1129 print(1)
1130
1131 def b():⋯
1132
1133 def c():⋯
1134 "
1135 .unindent(),
1136 );
1137
1138 editor.fold(&Fold, window, cx);
1139 assert_eq!(
1140 editor.display_text(cx),
1141 "
1142 class Foo:⋯
1143 "
1144 .unindent(),
1145 );
1146
1147 editor.unfold_lines(&UnfoldLines, window, cx);
1148 assert_eq!(
1149 editor.display_text(cx),
1150 "
1151 class Foo:
1152 # Hello!
1153
1154 def a():
1155 print(1)
1156
1157 def b():⋯
1158
1159 def c():⋯
1160 "
1161 .unindent(),
1162 );
1163
1164 editor.unfold_lines(&UnfoldLines, window, cx);
1165 assert_eq!(
1166 editor.display_text(cx),
1167 editor.buffer.read(cx).read(cx).text()
1168 );
1169 });
1170}
1171
1172#[gpui::test]
1173fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1174 init_test(cx, |_| {});
1175
1176 let editor = cx.add_window(|window, cx| {
1177 let buffer = MultiBuffer::build_simple(
1178 &"
1179 class Foo:
1180 # Hello!
1181
1182 def a():
1183 print(1)
1184
1185 def b():
1186 print(2)
1187
1188
1189 def c():
1190 print(3)
1191
1192
1193 "
1194 .unindent(),
1195 cx,
1196 );
1197 build_editor(buffer, window, cx)
1198 });
1199
1200 _ = editor.update(cx, |editor, window, cx| {
1201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1202 s.select_display_ranges([
1203 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1204 ]);
1205 });
1206 editor.fold(&Fold, window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():⋯
1217
1218
1219 def c():⋯
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.fold(&Fold, window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:⋯
1231
1232
1233 "
1234 .unindent(),
1235 );
1236
1237 editor.unfold_lines(&UnfoldLines, window, cx);
1238 assert_eq!(
1239 editor.display_text(cx),
1240 "
1241 class Foo:
1242 # Hello!
1243
1244 def a():
1245 print(1)
1246
1247 def b():⋯
1248
1249
1250 def c():⋯
1251
1252
1253 "
1254 .unindent(),
1255 );
1256
1257 editor.unfold_lines(&UnfoldLines, window, cx);
1258 assert_eq!(
1259 editor.display_text(cx),
1260 editor.buffer.read(cx).read(cx).text()
1261 );
1262 });
1263}
1264
1265#[gpui::test]
1266fn test_fold_at_level(cx: &mut TestAppContext) {
1267 init_test(cx, |_| {});
1268
1269 let editor = cx.add_window(|window, cx| {
1270 let buffer = MultiBuffer::build_simple(
1271 &"
1272 class Foo:
1273 # Hello!
1274
1275 def a():
1276 print(1)
1277
1278 def b():
1279 print(2)
1280
1281
1282 class Bar:
1283 # World!
1284
1285 def a():
1286 print(1)
1287
1288 def b():
1289 print(2)
1290
1291
1292 "
1293 .unindent(),
1294 cx,
1295 );
1296 build_editor(buffer, window, cx)
1297 });
1298
1299 _ = editor.update(cx, |editor, window, cx| {
1300 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1301 assert_eq!(
1302 editor.display_text(cx),
1303 "
1304 class Foo:
1305 # Hello!
1306
1307 def a():⋯
1308
1309 def b():⋯
1310
1311
1312 class Bar:
1313 # World!
1314
1315 def a():⋯
1316
1317 def b():⋯
1318
1319
1320 "
1321 .unindent(),
1322 );
1323
1324 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1325 assert_eq!(
1326 editor.display_text(cx),
1327 "
1328 class Foo:⋯
1329
1330
1331 class Bar:⋯
1332
1333
1334 "
1335 .unindent(),
1336 );
1337
1338 editor.unfold_all(&UnfoldAll, window, cx);
1339 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1340 assert_eq!(
1341 editor.display_text(cx),
1342 "
1343 class Foo:
1344 # Hello!
1345
1346 def a():
1347 print(1)
1348
1349 def b():
1350 print(2)
1351
1352
1353 class Bar:
1354 # World!
1355
1356 def a():
1357 print(1)
1358
1359 def b():
1360 print(2)
1361
1362
1363 "
1364 .unindent(),
1365 );
1366
1367 assert_eq!(
1368 editor.display_text(cx),
1369 editor.buffer.read(cx).read(cx).text()
1370 );
1371 let (_, positions) = marked_text_ranges(
1372 &"
1373 class Foo:
1374 # Hello!
1375
1376 def a():
1377 print(1)
1378
1379 def b():
1380 p«riˇ»nt(2)
1381
1382
1383 class Bar:
1384 # World!
1385
1386 def a():
1387 «ˇprint(1)
1388
1389 def b():
1390 print(2)»
1391
1392
1393 "
1394 .unindent(),
1395 true,
1396 );
1397
1398 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1399 s.select_ranges(positions)
1400 });
1401
1402 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1403 assert_eq!(
1404 editor.display_text(cx),
1405 "
1406 class Foo:
1407 # Hello!
1408
1409 def a():⋯
1410
1411 def b():
1412 print(2)
1413
1414
1415 class Bar:
1416 # World!
1417
1418 def a():
1419 print(1)
1420
1421 def b():
1422 print(2)
1423
1424
1425 "
1426 .unindent(),
1427 );
1428 });
1429}
1430
1431#[gpui::test]
1432fn test_move_cursor(cx: &mut TestAppContext) {
1433 init_test(cx, |_| {});
1434
1435 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1436 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1437
1438 buffer.update(cx, |buffer, cx| {
1439 buffer.edit(
1440 vec![
1441 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1442 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1443 ],
1444 None,
1445 cx,
1446 );
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1458 );
1459
1460 editor.move_right(&MoveRight, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1464 );
1465
1466 editor.move_left(&MoveLeft, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1470 );
1471
1472 editor.move_up(&MoveUp, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1476 );
1477
1478 editor.move_to_end(&MoveToEnd, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1482 );
1483
1484 editor.move_to_beginning(&MoveToBeginning, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1488 );
1489
1490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1491 s.select_display_ranges([
1492 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1493 ]);
1494 });
1495 editor.select_to_beginning(&SelectToBeginning, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1499 );
1500
1501 editor.select_to_end(&SelectToEnd, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1505 );
1506 });
1507}
1508
1509#[gpui::test]
1510fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1511 init_test(cx, |_| {});
1512
1513 let editor = cx.add_window(|window, cx| {
1514 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1515 build_editor(buffer, window, cx)
1516 });
1517
1518 assert_eq!('🟥'.len_utf8(), 4);
1519 assert_eq!('α'.len_utf8(), 2);
1520
1521 _ = editor.update(cx, |editor, window, cx| {
1522 editor.fold_creases(
1523 vec![
1524 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1525 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1526 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1527 ],
1528 true,
1529 window,
1530 cx,
1531 );
1532 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1533
1534 editor.move_right(&MoveRight, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(0, "🟥".len())]
1538 );
1539 editor.move_right(&MoveRight, window, cx);
1540 assert_eq!(
1541 editor.selections.display_ranges(cx),
1542 &[empty_range(0, "🟥🟧".len())]
1543 );
1544 editor.move_right(&MoveRight, window, cx);
1545 assert_eq!(
1546 editor.selections.display_ranges(cx),
1547 &[empty_range(0, "🟥🟧⋯".len())]
1548 );
1549
1550 editor.move_down(&MoveDown, window, cx);
1551 assert_eq!(
1552 editor.selections.display_ranges(cx),
1553 &[empty_range(1, "ab⋯e".len())]
1554 );
1555 editor.move_left(&MoveLeft, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[empty_range(1, "ab⋯".len())]
1559 );
1560 editor.move_left(&MoveLeft, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(1, "ab".len())]
1564 );
1565 editor.move_left(&MoveLeft, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[empty_range(1, "a".len())]
1569 );
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[empty_range(2, "α".len())]
1575 );
1576 editor.move_right(&MoveRight, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[empty_range(2, "αβ".len())]
1580 );
1581 editor.move_right(&MoveRight, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[empty_range(2, "αβ⋯".len())]
1585 );
1586 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(
1588 editor.selections.display_ranges(cx),
1589 &[empty_range(2, "αβ⋯ε".len())]
1590 );
1591
1592 editor.move_up(&MoveUp, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[empty_range(1, "ab⋯e".len())]
1596 );
1597 editor.move_down(&MoveDown, window, cx);
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[empty_range(2, "αβ⋯ε".len())]
1601 );
1602 editor.move_up(&MoveUp, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[empty_range(1, "ab⋯e".len())]
1606 );
1607
1608 editor.move_up(&MoveUp, window, cx);
1609 assert_eq!(
1610 editor.selections.display_ranges(cx),
1611 &[empty_range(0, "🟥🟧".len())]
1612 );
1613 editor.move_left(&MoveLeft, window, cx);
1614 assert_eq!(
1615 editor.selections.display_ranges(cx),
1616 &[empty_range(0, "🟥".len())]
1617 );
1618 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(
1620 editor.selections.display_ranges(cx),
1621 &[empty_range(0, "".len())]
1622 );
1623 });
1624}
1625
1626#[gpui::test]
1627fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1628 init_test(cx, |_| {});
1629
1630 let editor = cx.add_window(|window, cx| {
1631 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1632 build_editor(buffer, window, cx)
1633 });
1634 _ = editor.update(cx, |editor, window, cx| {
1635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1636 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1637 });
1638
1639 // moving above start of document should move selection to start of document,
1640 // but the next move down should still be at the original goal_x
1641 editor.move_up(&MoveUp, window, cx);
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[empty_range(0, "".len())]
1645 );
1646
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[empty_range(1, "abcd".len())]
1651 );
1652
1653 editor.move_down(&MoveDown, window, cx);
1654 assert_eq!(
1655 editor.selections.display_ranges(cx),
1656 &[empty_range(2, "αβγ".len())]
1657 );
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[empty_range(3, "abcd".len())]
1663 );
1664
1665 editor.move_down(&MoveDown, window, cx);
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1669 );
1670
1671 // moving past end of document should not change goal_x
1672 editor.move_down(&MoveDown, window, cx);
1673 assert_eq!(
1674 editor.selections.display_ranges(cx),
1675 &[empty_range(5, "".len())]
1676 );
1677
1678 editor.move_down(&MoveDown, window, cx);
1679 assert_eq!(
1680 editor.selections.display_ranges(cx),
1681 &[empty_range(5, "".len())]
1682 );
1683
1684 editor.move_up(&MoveUp, window, cx);
1685 assert_eq!(
1686 editor.selections.display_ranges(cx),
1687 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1688 );
1689
1690 editor.move_up(&MoveUp, window, cx);
1691 assert_eq!(
1692 editor.selections.display_ranges(cx),
1693 &[empty_range(3, "abcd".len())]
1694 );
1695
1696 editor.move_up(&MoveUp, window, cx);
1697 assert_eq!(
1698 editor.selections.display_ranges(cx),
1699 &[empty_range(2, "αβγ".len())]
1700 );
1701 });
1702}
1703
1704#[gpui::test]
1705fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1706 init_test(cx, |_| {});
1707 let move_to_beg = MoveToBeginningOfLine {
1708 stop_at_soft_wraps: true,
1709 stop_at_indent: true,
1710 };
1711
1712 let delete_to_beg = DeleteToBeginningOfLine {
1713 stop_at_indent: false,
1714 };
1715
1716 let move_to_end = MoveToEndOfLine {
1717 stop_at_soft_wraps: true,
1718 };
1719
1720 let editor = cx.add_window(|window, cx| {
1721 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1722 build_editor(buffer, window, cx)
1723 });
1724 _ = editor.update(cx, |editor, window, cx| {
1725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1726 s.select_display_ranges([
1727 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1728 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1729 ]);
1730 });
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1735 assert_eq!(
1736 editor.selections.display_ranges(cx),
1737 &[
1738 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1740 ]
1741 );
1742 });
1743
1744 _ = editor.update(cx, |editor, window, cx| {
1745 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1746 assert_eq!(
1747 editor.selections.display_ranges(cx),
1748 &[
1749 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1750 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1751 ]
1752 );
1753 });
1754
1755 _ = editor.update(cx, |editor, window, cx| {
1756 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1757 assert_eq!(
1758 editor.selections.display_ranges(cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.move_to_end_of_line(&move_to_end, window, cx);
1768 assert_eq!(
1769 editor.selections.display_ranges(cx),
1770 &[
1771 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1772 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1773 ]
1774 );
1775 });
1776
1777 // Moving to the end of line again is a no-op.
1778 _ = editor.update(cx, |editor, window, cx| {
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 editor.selections.display_ranges(cx),
1782 &[
1783 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1784 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1785 ]
1786 );
1787 });
1788
1789 _ = editor.update(cx, |editor, window, cx| {
1790 editor.move_left(&MoveLeft, window, cx);
1791 editor.select_to_beginning_of_line(
1792 &SelectToBeginningOfLine {
1793 stop_at_soft_wraps: true,
1794 stop_at_indent: true,
1795 },
1796 window,
1797 cx,
1798 );
1799 assert_eq!(
1800 editor.selections.display_ranges(cx),
1801 &[
1802 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1803 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1804 ]
1805 );
1806 });
1807
1808 _ = editor.update(cx, |editor, window, cx| {
1809 editor.select_to_beginning_of_line(
1810 &SelectToBeginningOfLine {
1811 stop_at_soft_wraps: true,
1812 stop_at_indent: true,
1813 },
1814 window,
1815 cx,
1816 );
1817 assert_eq!(
1818 editor.selections.display_ranges(cx),
1819 &[
1820 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1822 ]
1823 );
1824 });
1825
1826 _ = editor.update(cx, |editor, window, cx| {
1827 editor.select_to_beginning_of_line(
1828 &SelectToBeginningOfLine {
1829 stop_at_soft_wraps: true,
1830 stop_at_indent: true,
1831 },
1832 window,
1833 cx,
1834 );
1835 assert_eq!(
1836 editor.selections.display_ranges(cx),
1837 &[
1838 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1839 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1840 ]
1841 );
1842 });
1843
1844 _ = editor.update(cx, |editor, window, cx| {
1845 editor.select_to_end_of_line(
1846 &SelectToEndOfLine {
1847 stop_at_soft_wraps: true,
1848 },
1849 window,
1850 cx,
1851 );
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1856 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1857 ]
1858 );
1859 });
1860
1861 _ = editor.update(cx, |editor, window, cx| {
1862 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1863 assert_eq!(editor.display_text(cx), "ab\n de");
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1869 ]
1870 );
1871 });
1872
1873 _ = editor.update(cx, |editor, window, cx| {
1874 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1875 assert_eq!(editor.display_text(cx), "\n");
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883 });
1884}
1885
1886#[gpui::test]
1887fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1888 init_test(cx, |_| {});
1889 let move_to_beg = MoveToBeginningOfLine {
1890 stop_at_soft_wraps: false,
1891 stop_at_indent: false,
1892 };
1893
1894 let move_to_end = MoveToEndOfLine {
1895 stop_at_soft_wraps: false,
1896 };
1897
1898 let editor = cx.add_window(|window, cx| {
1899 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1900 build_editor(buffer, window, cx)
1901 });
1902
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.set_wrap_width(Some(140.0.into()), cx);
1905
1906 // We expect the following lines after wrapping
1907 // ```
1908 // thequickbrownfox
1909 // jumpedoverthelazydo
1910 // gs
1911 // ```
1912 // The final `gs` was soft-wrapped onto a new line.
1913 assert_eq!(
1914 "thequickbrownfox\njumpedoverthelaz\nydogs",
1915 editor.display_text(cx),
1916 );
1917
1918 // First, let's assert behavior on the first line, that was not soft-wrapped.
1919 // Start the cursor at the `k` on the first line
1920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1921 s.select_display_ranges([
1922 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1923 ]);
1924 });
1925
1926 // Moving to the beginning of the line should put us at the beginning of the line.
1927 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1928 assert_eq!(
1929 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1930 editor.selections.display_ranges(cx)
1931 );
1932
1933 // Moving to the end of the line should put us at the end of the line.
1934 editor.move_to_end_of_line(&move_to_end, window, cx);
1935 assert_eq!(
1936 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1937 editor.selections.display_ranges(cx)
1938 );
1939
1940 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1941 // Start the cursor at the last line (`y` that was wrapped to a new line)
1942 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1943 s.select_display_ranges([
1944 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1945 ]);
1946 });
1947
1948 // Moving to the beginning of the line should put us at the start of the second line of
1949 // display text, i.e., the `j`.
1950 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1951 assert_eq!(
1952 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1953 editor.selections.display_ranges(cx)
1954 );
1955
1956 // Moving to the beginning of the line again should be a no-op.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1960 editor.selections.display_ranges(cx)
1961 );
1962
1963 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1964 // next display line.
1965 editor.move_to_end_of_line(&move_to_end, window, cx);
1966 assert_eq!(
1967 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1968 editor.selections.display_ranges(cx)
1969 );
1970
1971 // Moving to the end of the line again should be a no-op.
1972 editor.move_to_end_of_line(&move_to_end, window, cx);
1973 assert_eq!(
1974 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1975 editor.selections.display_ranges(cx)
1976 );
1977 });
1978}
1979
1980#[gpui::test]
1981fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1982 init_test(cx, |_| {});
1983
1984 let move_to_beg = MoveToBeginningOfLine {
1985 stop_at_soft_wraps: true,
1986 stop_at_indent: true,
1987 };
1988
1989 let select_to_beg = SelectToBeginningOfLine {
1990 stop_at_soft_wraps: true,
1991 stop_at_indent: true,
1992 };
1993
1994 let delete_to_beg = DeleteToBeginningOfLine {
1995 stop_at_indent: true,
1996 };
1997
1998 let move_to_end = MoveToEndOfLine {
1999 stop_at_soft_wraps: false,
2000 };
2001
2002 let editor = cx.add_window(|window, cx| {
2003 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2004 build_editor(buffer, window, cx)
2005 });
2006
2007 _ = editor.update(cx, |editor, window, cx| {
2008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2009 s.select_display_ranges([
2010 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2011 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2012 ]);
2013 });
2014
2015 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2016 // and the second cursor at the first non-whitespace character in the line.
2017 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2018 assert_eq!(
2019 editor.selections.display_ranges(cx),
2020 &[
2021 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2022 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2023 ]
2024 );
2025
2026 // Moving to the beginning of the line again should be a no-op for the first cursor,
2027 // and should move the second cursor to the beginning of the line.
2028 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2029 assert_eq!(
2030 editor.selections.display_ranges(cx),
2031 &[
2032 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2033 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2034 ]
2035 );
2036
2037 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2038 // and should move the second cursor back to the first non-whitespace character in the line.
2039 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2040 assert_eq!(
2041 editor.selections.display_ranges(cx),
2042 &[
2043 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2044 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2045 ]
2046 );
2047
2048 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2049 // and to the first non-whitespace character in the line for the second cursor.
2050 editor.move_to_end_of_line(&move_to_end, window, cx);
2051 editor.move_left(&MoveLeft, window, cx);
2052 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2053 assert_eq!(
2054 editor.selections.display_ranges(cx),
2055 &[
2056 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2057 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2058 ]
2059 );
2060
2061 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2062 // and should select to the beginning of the line for the second cursor.
2063 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2064 assert_eq!(
2065 editor.selections.display_ranges(cx),
2066 &[
2067 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2068 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2069 ]
2070 );
2071
2072 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2073 // and should delete to the first non-whitespace character in the line for the second cursor.
2074 editor.move_to_end_of_line(&move_to_end, window, cx);
2075 editor.move_left(&MoveLeft, window, cx);
2076 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2077 assert_eq!(editor.text(cx), "c\n f");
2078 });
2079}
2080
2081#[gpui::test]
2082fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2083 init_test(cx, |_| {});
2084
2085 let move_to_beg = MoveToBeginningOfLine {
2086 stop_at_soft_wraps: true,
2087 stop_at_indent: true,
2088 };
2089
2090 let editor = cx.add_window(|window, cx| {
2091 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2092 build_editor(buffer, window, cx)
2093 });
2094
2095 _ = editor.update(cx, |editor, window, cx| {
2096 // test cursor between line_start and indent_start
2097 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2098 s.select_display_ranges([
2099 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2100 ]);
2101 });
2102
2103 // cursor should move to line_start
2104 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2105 assert_eq!(
2106 editor.selections.display_ranges(cx),
2107 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2108 );
2109
2110 // cursor should move to indent_start
2111 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2112 assert_eq!(
2113 editor.selections.display_ranges(cx),
2114 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2115 );
2116
2117 // cursor should move to back to line_start
2118 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2119 assert_eq!(
2120 editor.selections.display_ranges(cx),
2121 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2122 );
2123 });
2124}
2125
2126#[gpui::test]
2127fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2128 init_test(cx, |_| {});
2129
2130 let editor = cx.add_window(|window, cx| {
2131 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2132 build_editor(buffer, window, cx)
2133 });
2134 _ = editor.update(cx, |editor, window, cx| {
2135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2136 s.select_display_ranges([
2137 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2138 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2139 ])
2140 });
2141 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2142 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2143
2144 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2145 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2146
2147 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2148 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2149
2150 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2151 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2152
2153 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2154 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2155
2156 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2157 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2158
2159 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2160 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2161
2162 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2163 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2164
2165 editor.move_right(&MoveRight, window, cx);
2166 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2167 assert_selection_ranges(
2168 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2169 editor,
2170 cx,
2171 );
2172
2173 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2174 assert_selection_ranges(
2175 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2176 editor,
2177 cx,
2178 );
2179
2180 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2181 assert_selection_ranges(
2182 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2183 editor,
2184 cx,
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192
2193 let editor = cx.add_window(|window, cx| {
2194 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2195 build_editor(buffer, window, cx)
2196 });
2197
2198 _ = editor.update(cx, |editor, window, cx| {
2199 editor.set_wrap_width(Some(140.0.into()), cx);
2200 assert_eq!(
2201 editor.display_text(cx),
2202 "use one::{\n two::three::\n four::five\n};"
2203 );
2204
2205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2206 s.select_display_ranges([
2207 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2208 ]);
2209 });
2210
2211 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2212 assert_eq!(
2213 editor.selections.display_ranges(cx),
2214 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2215 );
2216
2217 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2218 assert_eq!(
2219 editor.selections.display_ranges(cx),
2220 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2221 );
2222
2223 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2224 assert_eq!(
2225 editor.selections.display_ranges(cx),
2226 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2227 );
2228
2229 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2230 assert_eq!(
2231 editor.selections.display_ranges(cx),
2232 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2233 );
2234
2235 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2236 assert_eq!(
2237 editor.selections.display_ranges(cx),
2238 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2239 );
2240
2241 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2242 assert_eq!(
2243 editor.selections.display_ranges(cx),
2244 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2245 );
2246 });
2247}
2248
2249#[gpui::test]
2250async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2251 init_test(cx, |_| {});
2252 let mut cx = EditorTestContext::new(cx).await;
2253
2254 let line_height = cx.editor(|editor, window, _| {
2255 editor
2256 .style()
2257 .unwrap()
2258 .text
2259 .line_height_in_pixels(window.rem_size())
2260 });
2261 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2262
2263 cx.set_state(
2264 &r#"ˇone
2265 two
2266
2267 three
2268 fourˇ
2269 five
2270
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285 ˇ
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"one
2295 two
2296
2297 three
2298 four
2299 five
2300 ˇ
2301 sixˇ"#
2302 .unindent(),
2303 );
2304
2305 cx.update_editor(|editor, window, cx| {
2306 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2307 });
2308 cx.assert_editor_state(
2309 &r#"one
2310 two
2311
2312 three
2313 four
2314 five
2315
2316 sixˇ"#
2317 .unindent(),
2318 );
2319
2320 cx.update_editor(|editor, window, cx| {
2321 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2322 });
2323 cx.assert_editor_state(
2324 &r#"one
2325 two
2326
2327 three
2328 four
2329 five
2330 ˇ
2331 six"#
2332 .unindent(),
2333 );
2334
2335 cx.update_editor(|editor, window, cx| {
2336 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2337 });
2338 cx.assert_editor_state(
2339 &r#"one
2340 two
2341 ˇ
2342 three
2343 four
2344 five
2345
2346 six"#
2347 .unindent(),
2348 );
2349
2350 cx.update_editor(|editor, window, cx| {
2351 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2352 });
2353 cx.assert_editor_state(
2354 &r#"ˇone
2355 two
2356
2357 three
2358 four
2359 five
2360
2361 six"#
2362 .unindent(),
2363 );
2364}
2365
2366#[gpui::test]
2367async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2368 init_test(cx, |_| {});
2369 let mut cx = EditorTestContext::new(cx).await;
2370 let line_height = cx.editor(|editor, window, _| {
2371 editor
2372 .style()
2373 .unwrap()
2374 .text
2375 .line_height_in_pixels(window.rem_size())
2376 });
2377 let window = cx.window;
2378 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2379
2380 cx.set_state(
2381 r#"ˇone
2382 two
2383 three
2384 four
2385 five
2386 six
2387 seven
2388 eight
2389 nine
2390 ten
2391 "#,
2392 );
2393
2394 cx.update_editor(|editor, window, cx| {
2395 assert_eq!(
2396 editor.snapshot(window, cx).scroll_position(),
2397 gpui::Point::new(0., 0.)
2398 );
2399 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2400 assert_eq!(
2401 editor.snapshot(window, cx).scroll_position(),
2402 gpui::Point::new(0., 3.)
2403 );
2404 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2405 assert_eq!(
2406 editor.snapshot(window, cx).scroll_position(),
2407 gpui::Point::new(0., 6.)
2408 );
2409 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2410 assert_eq!(
2411 editor.snapshot(window, cx).scroll_position(),
2412 gpui::Point::new(0., 3.)
2413 );
2414
2415 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 1.)
2419 );
2420 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2421 assert_eq!(
2422 editor.snapshot(window, cx).scroll_position(),
2423 gpui::Point::new(0., 3.)
2424 );
2425 });
2426}
2427
2428#[gpui::test]
2429async fn test_autoscroll(cx: &mut TestAppContext) {
2430 init_test(cx, |_| {});
2431 let mut cx = EditorTestContext::new(cx).await;
2432
2433 let line_height = cx.update_editor(|editor, window, cx| {
2434 editor.set_vertical_scroll_margin(2, cx);
2435 editor
2436 .style()
2437 .unwrap()
2438 .text
2439 .line_height_in_pixels(window.rem_size())
2440 });
2441 let window = cx.window;
2442 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2443
2444 cx.set_state(
2445 r#"ˇone
2446 two
2447 three
2448 four
2449 five
2450 six
2451 seven
2452 eight
2453 nine
2454 ten
2455 "#,
2456 );
2457 cx.update_editor(|editor, window, cx| {
2458 assert_eq!(
2459 editor.snapshot(window, cx).scroll_position(),
2460 gpui::Point::new(0., 0.0)
2461 );
2462 });
2463
2464 // Add a cursor below the visible area. Since both cursors cannot fit
2465 // on screen, the editor autoscrolls to reveal the newest cursor, and
2466 // allows the vertical scroll margin below that cursor.
2467 cx.update_editor(|editor, window, cx| {
2468 editor.change_selections(Default::default(), window, cx, |selections| {
2469 selections.select_ranges([
2470 Point::new(0, 0)..Point::new(0, 0),
2471 Point::new(6, 0)..Point::new(6, 0),
2472 ]);
2473 })
2474 });
2475 cx.update_editor(|editor, window, cx| {
2476 assert_eq!(
2477 editor.snapshot(window, cx).scroll_position(),
2478 gpui::Point::new(0., 3.0)
2479 );
2480 });
2481
2482 // Move down. The editor cursor scrolls down to track the newest cursor.
2483 cx.update_editor(|editor, window, cx| {
2484 editor.move_down(&Default::default(), window, cx);
2485 });
2486 cx.update_editor(|editor, window, cx| {
2487 assert_eq!(
2488 editor.snapshot(window, cx).scroll_position(),
2489 gpui::Point::new(0., 4.0)
2490 );
2491 });
2492
2493 // Add a cursor above the visible area. Since both cursors fit on screen,
2494 // the editor scrolls to show both.
2495 cx.update_editor(|editor, window, cx| {
2496 editor.change_selections(Default::default(), window, cx, |selections| {
2497 selections.select_ranges([
2498 Point::new(1, 0)..Point::new(1, 0),
2499 Point::new(6, 0)..Point::new(6, 0),
2500 ]);
2501 })
2502 });
2503 cx.update_editor(|editor, window, cx| {
2504 assert_eq!(
2505 editor.snapshot(window, cx).scroll_position(),
2506 gpui::Point::new(0., 1.0)
2507 );
2508 });
2509}
2510
2511#[gpui::test]
2512async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2513 init_test(cx, |_| {});
2514 let mut cx = EditorTestContext::new(cx).await;
2515
2516 let line_height = cx.editor(|editor, window, _cx| {
2517 editor
2518 .style()
2519 .unwrap()
2520 .text
2521 .line_height_in_pixels(window.rem_size())
2522 });
2523 let window = cx.window;
2524 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2525 cx.set_state(
2526 &r#"
2527 ˇone
2528 two
2529 threeˇ
2530 four
2531 five
2532 six
2533 seven
2534 eight
2535 nine
2536 ten
2537 "#
2538 .unindent(),
2539 );
2540
2541 cx.update_editor(|editor, window, cx| {
2542 editor.move_page_down(&MovePageDown::default(), window, cx)
2543 });
2544 cx.assert_editor_state(
2545 &r#"
2546 one
2547 two
2548 three
2549 ˇfour
2550 five
2551 sixˇ
2552 seven
2553 eight
2554 nine
2555 ten
2556 "#
2557 .unindent(),
2558 );
2559
2560 cx.update_editor(|editor, window, cx| {
2561 editor.move_page_down(&MovePageDown::default(), window, cx)
2562 });
2563 cx.assert_editor_state(
2564 &r#"
2565 one
2566 two
2567 three
2568 four
2569 five
2570 six
2571 ˇseven
2572 eight
2573 nineˇ
2574 ten
2575 "#
2576 .unindent(),
2577 );
2578
2579 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2580 cx.assert_editor_state(
2581 &r#"
2582 one
2583 two
2584 three
2585 ˇfour
2586 five
2587 sixˇ
2588 seven
2589 eight
2590 nine
2591 ten
2592 "#
2593 .unindent(),
2594 );
2595
2596 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2597 cx.assert_editor_state(
2598 &r#"
2599 ˇone
2600 two
2601 threeˇ
2602 four
2603 five
2604 six
2605 seven
2606 eight
2607 nine
2608 ten
2609 "#
2610 .unindent(),
2611 );
2612
2613 // Test select collapsing
2614 cx.update_editor(|editor, window, cx| {
2615 editor.move_page_down(&MovePageDown::default(), window, cx);
2616 editor.move_page_down(&MovePageDown::default(), window, cx);
2617 editor.move_page_down(&MovePageDown::default(), window, cx);
2618 });
2619 cx.assert_editor_state(
2620 &r#"
2621 one
2622 two
2623 three
2624 four
2625 five
2626 six
2627 seven
2628 eight
2629 nine
2630 ˇten
2631 ˇ"#
2632 .unindent(),
2633 );
2634}
2635
2636#[gpui::test]
2637async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2638 init_test(cx, |_| {});
2639 let mut cx = EditorTestContext::new(cx).await;
2640 cx.set_state("one «two threeˇ» four");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_beginning_of_line(
2643 &DeleteToBeginningOfLine {
2644 stop_at_indent: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 assert_eq!(editor.text(cx), " four");
2650 });
2651}
2652
2653#[gpui::test]
2654async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 // For an empty selection, the preceding word fragment is deleted.
2660 // For non-empty selections, only selected characters are deleted.
2661 cx.set_state("onˇe two t«hreˇ»e four");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_previous_word_start(
2664 &DeleteToPreviousWordStart {
2665 ignore_newlines: false,
2666 ignore_brackets: false,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 cx.assert_editor_state("ˇe two tˇe four");
2673
2674 cx.set_state("e tˇwo te «fˇ»our");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: false,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("e tˇ te ˇour");
2686}
2687
2688#[gpui::test]
2689async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2690 init_test(cx, |_| {});
2691
2692 let mut cx = EditorTestContext::new(cx).await;
2693
2694 cx.set_state("here is some text ˇwith a space");
2695 cx.update_editor(|editor, window, cx| {
2696 editor.delete_to_previous_word_start(
2697 &DeleteToPreviousWordStart {
2698 ignore_newlines: false,
2699 ignore_brackets: true,
2700 },
2701 window,
2702 cx,
2703 );
2704 });
2705 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2706 cx.assert_editor_state("here is some textˇwith a space");
2707
2708 cx.set_state("here is some text ˇwith a space");
2709 cx.update_editor(|editor, window, cx| {
2710 editor.delete_to_previous_word_start(
2711 &DeleteToPreviousWordStart {
2712 ignore_newlines: false,
2713 ignore_brackets: false,
2714 },
2715 window,
2716 cx,
2717 );
2718 });
2719 cx.assert_editor_state("here is some textˇwith a space");
2720
2721 cx.set_state("here is some textˇ with a space");
2722 cx.update_editor(|editor, window, cx| {
2723 editor.delete_to_next_word_end(
2724 &DeleteToNextWordEnd {
2725 ignore_newlines: false,
2726 ignore_brackets: true,
2727 },
2728 window,
2729 cx,
2730 );
2731 });
2732 // Same happens in the other direction.
2733 cx.assert_editor_state("here is some textˇwith a space");
2734
2735 cx.set_state("here is some textˇ with a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_next_word_end(
2738 &DeleteToNextWordEnd {
2739 ignore_newlines: false,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 cx.assert_editor_state("here is some textˇwith a space");
2747
2748 cx.set_state("here is some textˇ with a space");
2749 cx.update_editor(|editor, window, cx| {
2750 editor.delete_to_next_word_end(
2751 &DeleteToNextWordEnd {
2752 ignore_newlines: true,
2753 ignore_brackets: false,
2754 },
2755 window,
2756 cx,
2757 );
2758 });
2759 cx.assert_editor_state("here is some textˇwith a space");
2760 cx.update_editor(|editor, window, cx| {
2761 editor.delete_to_previous_word_start(
2762 &DeleteToPreviousWordStart {
2763 ignore_newlines: true,
2764 ignore_brackets: false,
2765 },
2766 window,
2767 cx,
2768 );
2769 });
2770 cx.assert_editor_state("here is some ˇwith a space");
2771 cx.update_editor(|editor, window, cx| {
2772 editor.delete_to_previous_word_start(
2773 &DeleteToPreviousWordStart {
2774 ignore_newlines: true,
2775 ignore_brackets: false,
2776 },
2777 window,
2778 cx,
2779 );
2780 });
2781 // Single whitespaces are removed with the word behind them.
2782 cx.assert_editor_state("here is ˇwith a space");
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_previous_word_start(
2785 &DeleteToPreviousWordStart {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 cx.assert_editor_state("here ˇwith a space");
2794 cx.update_editor(|editor, window, cx| {
2795 editor.delete_to_previous_word_start(
2796 &DeleteToPreviousWordStart {
2797 ignore_newlines: true,
2798 ignore_brackets: false,
2799 },
2800 window,
2801 cx,
2802 );
2803 });
2804 cx.assert_editor_state("ˇwith a space");
2805 cx.update_editor(|editor, window, cx| {
2806 editor.delete_to_previous_word_start(
2807 &DeleteToPreviousWordStart {
2808 ignore_newlines: true,
2809 ignore_brackets: false,
2810 },
2811 window,
2812 cx,
2813 );
2814 });
2815 cx.assert_editor_state("ˇwith a space");
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 // Same happens in the other direction.
2827 cx.assert_editor_state("ˇ a space");
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state("ˇ space");
2839 cx.update_editor(|editor, window, cx| {
2840 editor.delete_to_next_word_end(
2841 &DeleteToNextWordEnd {
2842 ignore_newlines: true,
2843 ignore_brackets: false,
2844 },
2845 window,
2846 cx,
2847 );
2848 });
2849 cx.assert_editor_state("ˇ");
2850 cx.update_editor(|editor, window, cx| {
2851 editor.delete_to_next_word_end(
2852 &DeleteToNextWordEnd {
2853 ignore_newlines: true,
2854 ignore_brackets: false,
2855 },
2856 window,
2857 cx,
2858 );
2859 });
2860 cx.assert_editor_state("ˇ");
2861 cx.update_editor(|editor, window, cx| {
2862 editor.delete_to_previous_word_start(
2863 &DeleteToPreviousWordStart {
2864 ignore_newlines: true,
2865 ignore_brackets: false,
2866 },
2867 window,
2868 cx,
2869 );
2870 });
2871 cx.assert_editor_state("ˇ");
2872}
2873
2874#[gpui::test]
2875async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2876 init_test(cx, |_| {});
2877
2878 let language = Arc::new(
2879 Language::new(
2880 LanguageConfig {
2881 brackets: BracketPairConfig {
2882 pairs: vec![
2883 BracketPair {
2884 start: "\"".to_string(),
2885 end: "\"".to_string(),
2886 close: true,
2887 surround: true,
2888 newline: false,
2889 },
2890 BracketPair {
2891 start: "(".to_string(),
2892 end: ")".to_string(),
2893 close: true,
2894 surround: true,
2895 newline: true,
2896 },
2897 ],
2898 ..BracketPairConfig::default()
2899 },
2900 ..LanguageConfig::default()
2901 },
2902 Some(tree_sitter_rust::LANGUAGE.into()),
2903 )
2904 .with_brackets_query(
2905 r#"
2906 ("(" @open ")" @close)
2907 ("\"" @open "\"" @close)
2908 "#,
2909 )
2910 .unwrap(),
2911 );
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2915
2916 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_previous_word_start(
2919 &DeleteToPreviousWordStart {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 // Deletion stops before brackets if asked to not ignore them.
2928 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2929 cx.update_editor(|editor, window, cx| {
2930 editor.delete_to_previous_word_start(
2931 &DeleteToPreviousWordStart {
2932 ignore_newlines: true,
2933 ignore_brackets: false,
2934 },
2935 window,
2936 cx,
2937 );
2938 });
2939 // Deletion has to remove a single bracket and then stop again.
2940 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2941
2942 cx.update_editor(|editor, window, cx| {
2943 editor.delete_to_previous_word_start(
2944 &DeleteToPreviousWordStart {
2945 ignore_newlines: true,
2946 ignore_brackets: false,
2947 },
2948 window,
2949 cx,
2950 );
2951 });
2952 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2953
2954 cx.update_editor(|editor, window, cx| {
2955 editor.delete_to_previous_word_start(
2956 &DeleteToPreviousWordStart {
2957 ignore_newlines: true,
2958 ignore_brackets: false,
2959 },
2960 window,
2961 cx,
2962 );
2963 });
2964 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2965
2966 cx.update_editor(|editor, window, cx| {
2967 editor.delete_to_previous_word_start(
2968 &DeleteToPreviousWordStart {
2969 ignore_newlines: true,
2970 ignore_brackets: false,
2971 },
2972 window,
2973 cx,
2974 );
2975 });
2976 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2977
2978 cx.update_editor(|editor, window, cx| {
2979 editor.delete_to_next_word_end(
2980 &DeleteToNextWordEnd {
2981 ignore_newlines: true,
2982 ignore_brackets: false,
2983 },
2984 window,
2985 cx,
2986 );
2987 });
2988 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2989 cx.assert_editor_state(r#"ˇ");"#);
2990
2991 cx.update_editor(|editor, window, cx| {
2992 editor.delete_to_next_word_end(
2993 &DeleteToNextWordEnd {
2994 ignore_newlines: true,
2995 ignore_brackets: false,
2996 },
2997 window,
2998 cx,
2999 );
3000 });
3001 cx.assert_editor_state(r#"ˇ"#);
3002
3003 cx.update_editor(|editor, window, cx| {
3004 editor.delete_to_next_word_end(
3005 &DeleteToNextWordEnd {
3006 ignore_newlines: true,
3007 ignore_brackets: false,
3008 },
3009 window,
3010 cx,
3011 );
3012 });
3013 cx.assert_editor_state(r#"ˇ"#);
3014
3015 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
3016 cx.update_editor(|editor, window, cx| {
3017 editor.delete_to_previous_word_start(
3018 &DeleteToPreviousWordStart {
3019 ignore_newlines: true,
3020 ignore_brackets: true,
3021 },
3022 window,
3023 cx,
3024 );
3025 });
3026 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3027}
3028
3029#[gpui::test]
3030fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3031 init_test(cx, |_| {});
3032
3033 let editor = cx.add_window(|window, cx| {
3034 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3035 build_editor(buffer, window, cx)
3036 });
3037 let del_to_prev_word_start = DeleteToPreviousWordStart {
3038 ignore_newlines: false,
3039 ignore_brackets: false,
3040 };
3041 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3042 ignore_newlines: true,
3043 ignore_brackets: false,
3044 };
3045
3046 _ = editor.update(cx, |editor, window, cx| {
3047 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3048 s.select_display_ranges([
3049 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3050 ])
3051 });
3052 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3053 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3054 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3055 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3056 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3057 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3058 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3059 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3060 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3061 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3062 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3063 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3064 });
3065}
3066
3067#[gpui::test]
3068fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3069 init_test(cx, |_| {});
3070
3071 let editor = cx.add_window(|window, cx| {
3072 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3073 build_editor(buffer, window, cx)
3074 });
3075 let del_to_next_word_end = DeleteToNextWordEnd {
3076 ignore_newlines: false,
3077 ignore_brackets: false,
3078 };
3079 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3080 ignore_newlines: true,
3081 ignore_brackets: false,
3082 };
3083
3084 _ = editor.update(cx, |editor, window, cx| {
3085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3086 s.select_display_ranges([
3087 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3088 ])
3089 });
3090 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3091 assert_eq!(
3092 editor.buffer.read(cx).read(cx).text(),
3093 "one\n two\nthree\n four"
3094 );
3095 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3096 assert_eq!(
3097 editor.buffer.read(cx).read(cx).text(),
3098 "\n two\nthree\n four"
3099 );
3100 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3101 assert_eq!(
3102 editor.buffer.read(cx).read(cx).text(),
3103 "two\nthree\n four"
3104 );
3105 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3106 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3107 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3108 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3109 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3110 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3111 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3112 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3113 });
3114}
3115
3116#[gpui::test]
3117fn test_newline(cx: &mut TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let editor = cx.add_window(|window, cx| {
3121 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3122 build_editor(buffer, window, cx)
3123 });
3124
3125 _ = editor.update(cx, |editor, window, cx| {
3126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3127 s.select_display_ranges([
3128 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3129 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3130 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3131 ])
3132 });
3133
3134 editor.newline(&Newline, window, cx);
3135 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3136 });
3137}
3138
3139#[gpui::test]
3140fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3141 init_test(cx, |_| {});
3142
3143 let editor = cx.add_window(|window, cx| {
3144 let buffer = MultiBuffer::build_simple(
3145 "
3146 a
3147 b(
3148 X
3149 )
3150 c(
3151 X
3152 )
3153 "
3154 .unindent()
3155 .as_str(),
3156 cx,
3157 );
3158 let mut editor = build_editor(buffer, window, cx);
3159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3160 s.select_ranges([
3161 Point::new(2, 4)..Point::new(2, 5),
3162 Point::new(5, 4)..Point::new(5, 5),
3163 ])
3164 });
3165 editor
3166 });
3167
3168 _ = editor.update(cx, |editor, window, cx| {
3169 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3170 editor.buffer.update(cx, |buffer, cx| {
3171 buffer.edit(
3172 [
3173 (Point::new(1, 2)..Point::new(3, 0), ""),
3174 (Point::new(4, 2)..Point::new(6, 0), ""),
3175 ],
3176 None,
3177 cx,
3178 );
3179 assert_eq!(
3180 buffer.read(cx).text(),
3181 "
3182 a
3183 b()
3184 c()
3185 "
3186 .unindent()
3187 );
3188 });
3189 assert_eq!(
3190 editor.selections.ranges(&editor.display_snapshot(cx)),
3191 &[
3192 Point::new(1, 2)..Point::new(1, 2),
3193 Point::new(2, 2)..Point::new(2, 2),
3194 ],
3195 );
3196
3197 editor.newline(&Newline, window, cx);
3198 assert_eq!(
3199 editor.text(cx),
3200 "
3201 a
3202 b(
3203 )
3204 c(
3205 )
3206 "
3207 .unindent()
3208 );
3209
3210 // The selections are moved after the inserted newlines
3211 assert_eq!(
3212 editor.selections.ranges(&editor.display_snapshot(cx)),
3213 &[
3214 Point::new(2, 0)..Point::new(2, 0),
3215 Point::new(4, 0)..Point::new(4, 0),
3216 ],
3217 );
3218 });
3219}
3220
3221#[gpui::test]
3222async fn test_newline_above(cx: &mut TestAppContext) {
3223 init_test(cx, |settings| {
3224 settings.defaults.tab_size = NonZeroU32::new(4)
3225 });
3226
3227 let language = Arc::new(
3228 Language::new(
3229 LanguageConfig::default(),
3230 Some(tree_sitter_rust::LANGUAGE.into()),
3231 )
3232 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3233 .unwrap(),
3234 );
3235
3236 let mut cx = EditorTestContext::new(cx).await;
3237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3238 cx.set_state(indoc! {"
3239 const a: ˇA = (
3240 (ˇ
3241 «const_functionˇ»(ˇ),
3242 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3243 )ˇ
3244 ˇ);ˇ
3245 "});
3246
3247 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3248 cx.assert_editor_state(indoc! {"
3249 ˇ
3250 const a: A = (
3251 ˇ
3252 (
3253 ˇ
3254 ˇ
3255 const_function(),
3256 ˇ
3257 ˇ
3258 ˇ
3259 ˇ
3260 something_else,
3261 ˇ
3262 )
3263 ˇ
3264 ˇ
3265 );
3266 "});
3267}
3268
3269#[gpui::test]
3270async fn test_newline_below(cx: &mut TestAppContext) {
3271 init_test(cx, |settings| {
3272 settings.defaults.tab_size = NonZeroU32::new(4)
3273 });
3274
3275 let language = Arc::new(
3276 Language::new(
3277 LanguageConfig::default(),
3278 Some(tree_sitter_rust::LANGUAGE.into()),
3279 )
3280 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3281 .unwrap(),
3282 );
3283
3284 let mut cx = EditorTestContext::new(cx).await;
3285 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3286 cx.set_state(indoc! {"
3287 const a: ˇA = (
3288 (ˇ
3289 «const_functionˇ»(ˇ),
3290 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3291 )ˇ
3292 ˇ);ˇ
3293 "});
3294
3295 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: A = (
3298 ˇ
3299 (
3300 ˇ
3301 const_function(),
3302 ˇ
3303 ˇ
3304 something_else,
3305 ˇ
3306 ˇ
3307 ˇ
3308 ˇ
3309 )
3310 ˇ
3311 );
3312 ˇ
3313 ˇ
3314 "});
3315}
3316
3317#[gpui::test]
3318async fn test_newline_comments(cx: &mut TestAppContext) {
3319 init_test(cx, |settings| {
3320 settings.defaults.tab_size = NonZeroU32::new(4)
3321 });
3322
3323 let language = Arc::new(Language::new(
3324 LanguageConfig {
3325 line_comments: vec!["// ".into()],
3326 ..LanguageConfig::default()
3327 },
3328 None,
3329 ));
3330 {
3331 let mut cx = EditorTestContext::new(cx).await;
3332 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3333 cx.set_state(indoc! {"
3334 // Fooˇ
3335 "});
3336
3337 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3338 cx.assert_editor_state(indoc! {"
3339 // Foo
3340 // ˇ
3341 "});
3342 // Ensure that we add comment prefix when existing line contains space
3343 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3344 cx.assert_editor_state(
3345 indoc! {"
3346 // Foo
3347 //s
3348 // ˇ
3349 "}
3350 .replace("s", " ") // s is used as space placeholder to prevent format on save
3351 .as_str(),
3352 );
3353 // Ensure that we add comment prefix when existing line does not contain space
3354 cx.set_state(indoc! {"
3355 // Foo
3356 //ˇ
3357 "});
3358 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3359 cx.assert_editor_state(indoc! {"
3360 // Foo
3361 //
3362 // ˇ
3363 "});
3364 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3365 cx.set_state(indoc! {"
3366 ˇ// Foo
3367 "});
3368 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3369 cx.assert_editor_state(indoc! {"
3370
3371 ˇ// Foo
3372 "});
3373 }
3374 // Ensure that comment continuations can be disabled.
3375 update_test_language_settings(cx, |settings| {
3376 settings.defaults.extend_comment_on_newline = Some(false);
3377 });
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.set_state(indoc! {"
3380 // Fooˇ
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 // Foo
3385 ˇ
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4)
3393 });
3394
3395 let language = Arc::new(Language::new(
3396 LanguageConfig {
3397 line_comments: vec!["// ".into(), "/// ".into()],
3398 ..LanguageConfig::default()
3399 },
3400 None,
3401 ));
3402 {
3403 let mut cx = EditorTestContext::new(cx).await;
3404 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3405 cx.set_state(indoc! {"
3406 //ˇ
3407 "});
3408 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 //
3411 // ˇ
3412 "});
3413
3414 cx.set_state(indoc! {"
3415 ///ˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 ///
3420 /// ˇ
3421 "});
3422 }
3423}
3424
3425#[gpui::test]
3426async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3427 init_test(cx, |settings| {
3428 settings.defaults.tab_size = NonZeroU32::new(4)
3429 });
3430
3431 let language = Arc::new(
3432 Language::new(
3433 LanguageConfig {
3434 documentation_comment: Some(language::BlockCommentConfig {
3435 start: "/**".into(),
3436 end: "*/".into(),
3437 prefix: "* ".into(),
3438 tab_size: 1,
3439 }),
3440
3441 ..LanguageConfig::default()
3442 },
3443 Some(tree_sitter_rust::LANGUAGE.into()),
3444 )
3445 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3446 .unwrap(),
3447 );
3448
3449 {
3450 let mut cx = EditorTestContext::new(cx).await;
3451 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3452 cx.set_state(indoc! {"
3453 /**ˇ
3454 "});
3455
3456 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3457 cx.assert_editor_state(indoc! {"
3458 /**
3459 * ˇ
3460 "});
3461 // Ensure that if cursor is before the comment start,
3462 // we do not actually insert a comment prefix.
3463 cx.set_state(indoc! {"
3464 ˇ/**
3465 "});
3466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3467 cx.assert_editor_state(indoc! {"
3468
3469 ˇ/**
3470 "});
3471 // Ensure that if cursor is between it doesn't add comment prefix.
3472 cx.set_state(indoc! {"
3473 /*ˇ*
3474 "});
3475 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 /*
3478 ˇ*
3479 "});
3480 // Ensure that if suffix exists on same line after cursor it adds new line.
3481 cx.set_state(indoc! {"
3482 /**ˇ*/
3483 "});
3484 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3485 cx.assert_editor_state(indoc! {"
3486 /**
3487 * ˇ
3488 */
3489 "});
3490 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3491 cx.set_state(indoc! {"
3492 /**ˇ */
3493 "});
3494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 /**
3497 * ˇ
3498 */
3499 "});
3500 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3501 cx.set_state(indoc! {"
3502 /** ˇ*/
3503 "});
3504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3505 cx.assert_editor_state(
3506 indoc! {"
3507 /**s
3508 * ˇ
3509 */
3510 "}
3511 .replace("s", " ") // s is used as space placeholder to prevent format on save
3512 .as_str(),
3513 );
3514 // Ensure that delimiter space is preserved when newline on already
3515 // spaced delimiter.
3516 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3517 cx.assert_editor_state(
3518 indoc! {"
3519 /**s
3520 *s
3521 * ˇ
3522 */
3523 "}
3524 .replace("s", " ") // s is used as space placeholder to prevent format on save
3525 .as_str(),
3526 );
3527 // Ensure that delimiter space is preserved when space is not
3528 // on existing delimiter.
3529 cx.set_state(indoc! {"
3530 /**
3531 *ˇ
3532 */
3533 "});
3534 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3535 cx.assert_editor_state(indoc! {"
3536 /**
3537 *
3538 * ˇ
3539 */
3540 "});
3541 // Ensure that if suffix exists on same line after cursor it
3542 // doesn't add extra new line if prefix is not on same line.
3543 cx.set_state(indoc! {"
3544 /**
3545 ˇ*/
3546 "});
3547 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3548 cx.assert_editor_state(indoc! {"
3549 /**
3550
3551 ˇ*/
3552 "});
3553 // Ensure that it detects suffix after existing prefix.
3554 cx.set_state(indoc! {"
3555 /**ˇ/
3556 "});
3557 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3558 cx.assert_editor_state(indoc! {"
3559 /**
3560 ˇ/
3561 "});
3562 // Ensure that if suffix exists on same line before
3563 // cursor it does not add comment prefix.
3564 cx.set_state(indoc! {"
3565 /** */ˇ
3566 "});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 /** */
3570 ˇ
3571 "});
3572 // Ensure that if suffix exists on same line before
3573 // cursor it does not add comment prefix.
3574 cx.set_state(indoc! {"
3575 /**
3576 *
3577 */ˇ
3578 "});
3579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 /**
3582 *
3583 */
3584 ˇ
3585 "});
3586
3587 // Ensure that inline comment followed by code
3588 // doesn't add comment prefix on newline
3589 cx.set_state(indoc! {"
3590 /** */ textˇ
3591 "});
3592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 /** */ text
3595 ˇ
3596 "});
3597
3598 // Ensure that text after comment end tag
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /**
3602 *
3603 */ˇtext
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /**
3608 *
3609 */
3610 ˇtext
3611 "});
3612
3613 // Ensure if not comment block it doesn't
3614 // add comment prefix on newline
3615 cx.set_state(indoc! {"
3616 * textˇ
3617 "});
3618 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3619 cx.assert_editor_state(indoc! {"
3620 * text
3621 ˇ
3622 "});
3623 }
3624 // Ensure that comment continuations can be disabled.
3625 update_test_language_settings(cx, |settings| {
3626 settings.defaults.extend_comment_on_newline = Some(false);
3627 });
3628 let mut cx = EditorTestContext::new(cx).await;
3629 cx.set_state(indoc! {"
3630 /**ˇ
3631 "});
3632 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 /**
3635 ˇ
3636 "});
3637}
3638
3639#[gpui::test]
3640async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3641 init_test(cx, |settings| {
3642 settings.defaults.tab_size = NonZeroU32::new(4)
3643 });
3644
3645 let lua_language = Arc::new(Language::new(
3646 LanguageConfig {
3647 line_comments: vec!["--".into()],
3648 block_comment: Some(language::BlockCommentConfig {
3649 start: "--[[".into(),
3650 prefix: "".into(),
3651 end: "]]".into(),
3652 tab_size: 0,
3653 }),
3654 ..LanguageConfig::default()
3655 },
3656 None,
3657 ));
3658
3659 let mut cx = EditorTestContext::new(cx).await;
3660 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3661
3662 // Line with line comment should extend
3663 cx.set_state(indoc! {"
3664 --ˇ
3665 "});
3666 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3667 cx.assert_editor_state(indoc! {"
3668 --
3669 --ˇ
3670 "});
3671
3672 // Line with block comment that matches line comment should not extend
3673 cx.set_state(indoc! {"
3674 --[[ˇ
3675 "});
3676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 --[[
3679 ˇ
3680 "});
3681}
3682
3683#[gpui::test]
3684fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3685 init_test(cx, |_| {});
3686
3687 let editor = cx.add_window(|window, cx| {
3688 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3689 let mut editor = build_editor(buffer, window, cx);
3690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3691 s.select_ranges([3..4, 11..12, 19..20])
3692 });
3693 editor
3694 });
3695
3696 _ = editor.update(cx, |editor, window, cx| {
3697 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3698 editor.buffer.update(cx, |buffer, cx| {
3699 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3700 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3701 });
3702 assert_eq!(
3703 editor.selections.ranges(&editor.display_snapshot(cx)),
3704 &[2..2, 7..7, 12..12],
3705 );
3706
3707 editor.insert("Z", window, cx);
3708 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3709
3710 // The selections are moved after the inserted characters
3711 assert_eq!(
3712 editor.selections.ranges(&editor.display_snapshot(cx)),
3713 &[3..3, 9..9, 15..15],
3714 );
3715 });
3716}
3717
3718#[gpui::test]
3719async fn test_tab(cx: &mut TestAppContext) {
3720 init_test(cx, |settings| {
3721 settings.defaults.tab_size = NonZeroU32::new(3)
3722 });
3723
3724 let mut cx = EditorTestContext::new(cx).await;
3725 cx.set_state(indoc! {"
3726 ˇabˇc
3727 ˇ🏀ˇ🏀ˇefg
3728 dˇ
3729 "});
3730 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3731 cx.assert_editor_state(indoc! {"
3732 ˇab ˇc
3733 ˇ🏀 ˇ🏀 ˇefg
3734 d ˇ
3735 "});
3736
3737 cx.set_state(indoc! {"
3738 a
3739 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 a
3744 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3745 "});
3746}
3747
3748#[gpui::test]
3749async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3750 init_test(cx, |_| {});
3751
3752 let mut cx = EditorTestContext::new(cx).await;
3753 let language = Arc::new(
3754 Language::new(
3755 LanguageConfig::default(),
3756 Some(tree_sitter_rust::LANGUAGE.into()),
3757 )
3758 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3759 .unwrap(),
3760 );
3761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3762
3763 // test when all cursors are not at suggested indent
3764 // then simply move to their suggested indent location
3765 cx.set_state(indoc! {"
3766 const a: B = (
3767 c(
3768 ˇ
3769 ˇ )
3770 );
3771 "});
3772 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3773 cx.assert_editor_state(indoc! {"
3774 const a: B = (
3775 c(
3776 ˇ
3777 ˇ)
3778 );
3779 "});
3780
3781 // test cursor already at suggested indent not moving when
3782 // other cursors are yet to reach their suggested indents
3783 cx.set_state(indoc! {"
3784 ˇ
3785 const a: B = (
3786 c(
3787 d(
3788 ˇ
3789 )
3790 ˇ
3791 ˇ )
3792 );
3793 "});
3794 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3795 cx.assert_editor_state(indoc! {"
3796 ˇ
3797 const a: B = (
3798 c(
3799 d(
3800 ˇ
3801 )
3802 ˇ
3803 ˇ)
3804 );
3805 "});
3806 // test when all cursors are at suggested indent then tab is inserted
3807 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 ˇ
3810 const a: B = (
3811 c(
3812 d(
3813 ˇ
3814 )
3815 ˇ
3816 ˇ)
3817 );
3818 "});
3819
3820 // test when current indent is less than suggested indent,
3821 // we adjust line to match suggested indent and move cursor to it
3822 //
3823 // when no other cursor is at word boundary, all of them should move
3824 cx.set_state(indoc! {"
3825 const a: B = (
3826 c(
3827 d(
3828 ˇ
3829 ˇ )
3830 ˇ )
3831 );
3832 "});
3833 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3834 cx.assert_editor_state(indoc! {"
3835 const a: B = (
3836 c(
3837 d(
3838 ˇ
3839 ˇ)
3840 ˇ)
3841 );
3842 "});
3843
3844 // test when current indent is less than suggested indent,
3845 // we adjust line to match suggested indent and move cursor to it
3846 //
3847 // when some other cursor is at word boundary, it should not move
3848 cx.set_state(indoc! {"
3849 const a: B = (
3850 c(
3851 d(
3852 ˇ
3853 ˇ )
3854 ˇ)
3855 );
3856 "});
3857 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3858 cx.assert_editor_state(indoc! {"
3859 const a: B = (
3860 c(
3861 d(
3862 ˇ
3863 ˇ)
3864 ˇ)
3865 );
3866 "});
3867
3868 // test when current indent is more than suggested indent,
3869 // we just move cursor to current indent instead of suggested indent
3870 //
3871 // when no other cursor is at word boundary, all of them should move
3872 cx.set_state(indoc! {"
3873 const a: B = (
3874 c(
3875 d(
3876 ˇ
3877 ˇ )
3878 ˇ )
3879 );
3880 "});
3881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3882 cx.assert_editor_state(indoc! {"
3883 const a: B = (
3884 c(
3885 d(
3886 ˇ
3887 ˇ)
3888 ˇ)
3889 );
3890 "});
3891 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3892 cx.assert_editor_state(indoc! {"
3893 const a: B = (
3894 c(
3895 d(
3896 ˇ
3897 ˇ)
3898 ˇ)
3899 );
3900 "});
3901
3902 // test when current indent is more than suggested indent,
3903 // we just move cursor to current indent instead of suggested indent
3904 //
3905 // when some other cursor is at word boundary, it doesn't move
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 d(
3910 ˇ
3911 ˇ )
3912 ˇ)
3913 );
3914 "});
3915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3916 cx.assert_editor_state(indoc! {"
3917 const a: B = (
3918 c(
3919 d(
3920 ˇ
3921 ˇ)
3922 ˇ)
3923 );
3924 "});
3925
3926 // handle auto-indent when there are multiple cursors on the same line
3927 cx.set_state(indoc! {"
3928 const a: B = (
3929 c(
3930 ˇ ˇ
3931 ˇ )
3932 );
3933 "});
3934 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3935 cx.assert_editor_state(indoc! {"
3936 const a: B = (
3937 c(
3938 ˇ
3939 ˇ)
3940 );
3941 "});
3942}
3943
3944#[gpui::test]
3945async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3946 init_test(cx, |settings| {
3947 settings.defaults.tab_size = NonZeroU32::new(3)
3948 });
3949
3950 let mut cx = EditorTestContext::new(cx).await;
3951 cx.set_state(indoc! {"
3952 ˇ
3953 \t ˇ
3954 \t ˇ
3955 \t ˇ
3956 \t \t\t \t \t\t \t\t \t \t ˇ
3957 "});
3958
3959 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 ˇ
3962 \t ˇ
3963 \t ˇ
3964 \t ˇ
3965 \t \t\t \t \t\t \t\t \t \t ˇ
3966 "});
3967}
3968
3969#[gpui::test]
3970async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3971 init_test(cx, |settings| {
3972 settings.defaults.tab_size = NonZeroU32::new(4)
3973 });
3974
3975 let language = Arc::new(
3976 Language::new(
3977 LanguageConfig::default(),
3978 Some(tree_sitter_rust::LANGUAGE.into()),
3979 )
3980 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3981 .unwrap(),
3982 );
3983
3984 let mut cx = EditorTestContext::new(cx).await;
3985 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3986 cx.set_state(indoc! {"
3987 fn a() {
3988 if b {
3989 \t ˇc
3990 }
3991 }
3992 "});
3993
3994 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3995 cx.assert_editor_state(indoc! {"
3996 fn a() {
3997 if b {
3998 ˇc
3999 }
4000 }
4001 "});
4002}
4003
4004#[gpui::test]
4005async fn test_indent_outdent(cx: &mut TestAppContext) {
4006 init_test(cx, |settings| {
4007 settings.defaults.tab_size = NonZeroU32::new(4);
4008 });
4009
4010 let mut cx = EditorTestContext::new(cx).await;
4011
4012 cx.set_state(indoc! {"
4013 «oneˇ» «twoˇ»
4014 three
4015 four
4016 "});
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 «oneˇ» «twoˇ»
4020 three
4021 four
4022 "});
4023
4024 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4025 cx.assert_editor_state(indoc! {"
4026 «oneˇ» «twoˇ»
4027 three
4028 four
4029 "});
4030
4031 // select across line ending
4032 cx.set_state(indoc! {"
4033 one two
4034 t«hree
4035 ˇ» four
4036 "});
4037 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 t«hree
4041 ˇ» four
4042 "});
4043
4044 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4045 cx.assert_editor_state(indoc! {"
4046 one two
4047 t«hree
4048 ˇ» four
4049 "});
4050
4051 // Ensure that indenting/outdenting works when the cursor is at column 0.
4052 cx.set_state(indoc! {"
4053 one two
4054 ˇthree
4055 four
4056 "});
4057 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4058 cx.assert_editor_state(indoc! {"
4059 one two
4060 ˇthree
4061 four
4062 "});
4063
4064 cx.set_state(indoc! {"
4065 one two
4066 ˇ three
4067 four
4068 "});
4069 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4070 cx.assert_editor_state(indoc! {"
4071 one two
4072 ˇthree
4073 four
4074 "});
4075}
4076
4077#[gpui::test]
4078async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4079 // This is a regression test for issue #33761
4080 init_test(cx, |_| {});
4081
4082 let mut cx = EditorTestContext::new(cx).await;
4083 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4085
4086 cx.set_state(
4087 r#"ˇ# ingress:
4088ˇ# api:
4089ˇ# enabled: false
4090ˇ# pathType: Prefix
4091ˇ# console:
4092ˇ# enabled: false
4093ˇ# pathType: Prefix
4094"#,
4095 );
4096
4097 // Press tab to indent all lines
4098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4099
4100 cx.assert_editor_state(
4101 r#" ˇ# ingress:
4102 ˇ# api:
4103 ˇ# enabled: false
4104 ˇ# pathType: Prefix
4105 ˇ# console:
4106 ˇ# enabled: false
4107 ˇ# pathType: Prefix
4108"#,
4109 );
4110}
4111
4112#[gpui::test]
4113async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4114 // This is a test to make sure our fix for issue #33761 didn't break anything
4115 init_test(cx, |_| {});
4116
4117 let mut cx = EditorTestContext::new(cx).await;
4118 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4119 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4120
4121 cx.set_state(
4122 r#"ˇingress:
4123ˇ api:
4124ˇ enabled: false
4125ˇ pathType: Prefix
4126"#,
4127 );
4128
4129 // Press tab to indent all lines
4130 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4131
4132 cx.assert_editor_state(
4133 r#"ˇingress:
4134 ˇapi:
4135 ˇenabled: false
4136 ˇpathType: Prefix
4137"#,
4138 );
4139}
4140
4141#[gpui::test]
4142async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4143 init_test(cx, |settings| {
4144 settings.defaults.hard_tabs = Some(true);
4145 });
4146
4147 let mut cx = EditorTestContext::new(cx).await;
4148
4149 // select two ranges on one line
4150 cx.set_state(indoc! {"
4151 «oneˇ» «twoˇ»
4152 three
4153 four
4154 "});
4155 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4156 cx.assert_editor_state(indoc! {"
4157 \t«oneˇ» «twoˇ»
4158 three
4159 four
4160 "});
4161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4162 cx.assert_editor_state(indoc! {"
4163 \t\t«oneˇ» «twoˇ»
4164 three
4165 four
4166 "});
4167 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4168 cx.assert_editor_state(indoc! {"
4169 \t«oneˇ» «twoˇ»
4170 three
4171 four
4172 "});
4173 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4174 cx.assert_editor_state(indoc! {"
4175 «oneˇ» «twoˇ»
4176 three
4177 four
4178 "});
4179
4180 // select across a line ending
4181 cx.set_state(indoc! {"
4182 one two
4183 t«hree
4184 ˇ»four
4185 "});
4186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4187 cx.assert_editor_state(indoc! {"
4188 one two
4189 \tt«hree
4190 ˇ»four
4191 "});
4192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4193 cx.assert_editor_state(indoc! {"
4194 one two
4195 \t\tt«hree
4196 ˇ»four
4197 "});
4198 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 \tt«hree
4202 ˇ»four
4203 "});
4204 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4205 cx.assert_editor_state(indoc! {"
4206 one two
4207 t«hree
4208 ˇ»four
4209 "});
4210
4211 // Ensure that indenting/outdenting works when the cursor is at column 0.
4212 cx.set_state(indoc! {"
4213 one two
4214 ˇthree
4215 four
4216 "});
4217 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 one two
4220 ˇthree
4221 four
4222 "});
4223 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4224 cx.assert_editor_state(indoc! {"
4225 one two
4226 \tˇthree
4227 four
4228 "});
4229 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4230 cx.assert_editor_state(indoc! {"
4231 one two
4232 ˇthree
4233 four
4234 "});
4235}
4236
4237#[gpui::test]
4238fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4239 init_test(cx, |settings| {
4240 settings.languages.0.extend([
4241 (
4242 "TOML".into(),
4243 LanguageSettingsContent {
4244 tab_size: NonZeroU32::new(2),
4245 ..Default::default()
4246 },
4247 ),
4248 (
4249 "Rust".into(),
4250 LanguageSettingsContent {
4251 tab_size: NonZeroU32::new(4),
4252 ..Default::default()
4253 },
4254 ),
4255 ]);
4256 });
4257
4258 let toml_language = Arc::new(Language::new(
4259 LanguageConfig {
4260 name: "TOML".into(),
4261 ..Default::default()
4262 },
4263 None,
4264 ));
4265 let rust_language = Arc::new(Language::new(
4266 LanguageConfig {
4267 name: "Rust".into(),
4268 ..Default::default()
4269 },
4270 None,
4271 ));
4272
4273 let toml_buffer =
4274 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4275 let rust_buffer =
4276 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4277 let multibuffer = cx.new(|cx| {
4278 let mut multibuffer = MultiBuffer::new(ReadWrite);
4279 multibuffer.push_excerpts(
4280 toml_buffer.clone(),
4281 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4282 cx,
4283 );
4284 multibuffer.push_excerpts(
4285 rust_buffer.clone(),
4286 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4287 cx,
4288 );
4289 multibuffer
4290 });
4291
4292 cx.add_window(|window, cx| {
4293 let mut editor = build_editor(multibuffer, window, cx);
4294
4295 assert_eq!(
4296 editor.text(cx),
4297 indoc! {"
4298 a = 1
4299 b = 2
4300
4301 const c: usize = 3;
4302 "}
4303 );
4304
4305 select_ranges(
4306 &mut editor,
4307 indoc! {"
4308 «aˇ» = 1
4309 b = 2
4310
4311 «const c:ˇ» usize = 3;
4312 "},
4313 window,
4314 cx,
4315 );
4316
4317 editor.tab(&Tab, window, cx);
4318 assert_text_with_selections(
4319 &mut editor,
4320 indoc! {"
4321 «aˇ» = 1
4322 b = 2
4323
4324 «const c:ˇ» usize = 3;
4325 "},
4326 cx,
4327 );
4328 editor.backtab(&Backtab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339
4340 editor
4341 });
4342}
4343
4344#[gpui::test]
4345async fn test_backspace(cx: &mut TestAppContext) {
4346 init_test(cx, |_| {});
4347
4348 let mut cx = EditorTestContext::new(cx).await;
4349
4350 // Basic backspace
4351 cx.set_state(indoc! {"
4352 onˇe two three
4353 fou«rˇ» five six
4354 seven «ˇeight nine
4355 »ten
4356 "});
4357 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 oˇe two three
4360 fouˇ five six
4361 seven ˇten
4362 "});
4363
4364 // Test backspace inside and around indents
4365 cx.set_state(indoc! {"
4366 zero
4367 ˇone
4368 ˇtwo
4369 ˇ ˇ ˇ three
4370 ˇ ˇ four
4371 "});
4372 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4373 cx.assert_editor_state(indoc! {"
4374 zero
4375 ˇone
4376 ˇtwo
4377 ˇ threeˇ four
4378 "});
4379}
4380
4381#[gpui::test]
4382async fn test_delete(cx: &mut TestAppContext) {
4383 init_test(cx, |_| {});
4384
4385 let mut cx = EditorTestContext::new(cx).await;
4386 cx.set_state(indoc! {"
4387 onˇe two three
4388 fou«rˇ» five six
4389 seven «ˇeight nine
4390 »ten
4391 "});
4392 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4393 cx.assert_editor_state(indoc! {"
4394 onˇ two three
4395 fouˇ five six
4396 seven ˇten
4397 "});
4398}
4399
4400#[gpui::test]
4401fn test_delete_line(cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let editor = cx.add_window(|window, cx| {
4405 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4406 build_editor(buffer, window, cx)
4407 });
4408 _ = editor.update(cx, |editor, window, cx| {
4409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4410 s.select_display_ranges([
4411 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4412 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4413 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4414 ])
4415 });
4416 editor.delete_line(&DeleteLine, window, cx);
4417 assert_eq!(editor.display_text(cx), "ghi");
4418 assert_eq!(
4419 editor.selections.display_ranges(cx),
4420 vec![
4421 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 ]
4424 );
4425 });
4426
4427 let editor = cx.add_window(|window, cx| {
4428 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4429 build_editor(buffer, window, cx)
4430 });
4431 _ = editor.update(cx, |editor, window, cx| {
4432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4433 s.select_display_ranges([
4434 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4435 ])
4436 });
4437 editor.delete_line(&DeleteLine, window, cx);
4438 assert_eq!(editor.display_text(cx), "ghi\n");
4439 assert_eq!(
4440 editor.selections.display_ranges(cx),
4441 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4442 );
4443 });
4444
4445 let editor = cx.add_window(|window, cx| {
4446 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4447 build_editor(buffer, window, cx)
4448 });
4449 _ = editor.update(cx, |editor, window, cx| {
4450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4451 s.select_display_ranges([
4452 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4453 ])
4454 });
4455 editor.delete_line(&DeleteLine, window, cx);
4456 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4457 assert_eq!(
4458 editor.selections.display_ranges(cx),
4459 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4460 );
4461 });
4462}
4463
4464#[gpui::test]
4465fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4466 init_test(cx, |_| {});
4467
4468 cx.add_window(|window, cx| {
4469 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4470 let mut editor = build_editor(buffer.clone(), window, cx);
4471 let buffer = buffer.read(cx).as_singleton().unwrap();
4472
4473 assert_eq!(
4474 editor
4475 .selections
4476 .ranges::<Point>(&editor.display_snapshot(cx)),
4477 &[Point::new(0, 0)..Point::new(0, 0)]
4478 );
4479
4480 // When on single line, replace newline at end by space
4481 editor.join_lines(&JoinLines, window, cx);
4482 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4483 assert_eq!(
4484 editor
4485 .selections
4486 .ranges::<Point>(&editor.display_snapshot(cx)),
4487 &[Point::new(0, 3)..Point::new(0, 3)]
4488 );
4489
4490 // When multiple lines are selected, remove newlines that are spanned by the selection
4491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4492 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4493 });
4494 editor.join_lines(&JoinLines, window, cx);
4495 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4496 assert_eq!(
4497 editor
4498 .selections
4499 .ranges::<Point>(&editor.display_snapshot(cx)),
4500 &[Point::new(0, 11)..Point::new(0, 11)]
4501 );
4502
4503 // Undo should be transactional
4504 editor.undo(&Undo, window, cx);
4505 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4506 assert_eq!(
4507 editor
4508 .selections
4509 .ranges::<Point>(&editor.display_snapshot(cx)),
4510 &[Point::new(0, 5)..Point::new(2, 2)]
4511 );
4512
4513 // When joining an empty line don't insert a space
4514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4515 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4516 });
4517 editor.join_lines(&JoinLines, window, cx);
4518 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4519 assert_eq!(
4520 editor
4521 .selections
4522 .ranges::<Point>(&editor.display_snapshot(cx)),
4523 [Point::new(2, 3)..Point::new(2, 3)]
4524 );
4525
4526 // We can remove trailing newlines
4527 editor.join_lines(&JoinLines, window, cx);
4528 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 [Point::new(2, 3)..Point::new(2, 3)]
4534 );
4535
4536 // We don't blow up on the last line
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 [Point::new(2, 3)..Point::new(2, 3)]
4544 );
4545
4546 // reset to test indentation
4547 editor.buffer.update(cx, |buffer, cx| {
4548 buffer.edit(
4549 [
4550 (Point::new(1, 0)..Point::new(1, 2), " "),
4551 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4552 ],
4553 None,
4554 cx,
4555 )
4556 });
4557
4558 // We remove any leading spaces
4559 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4560 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4561 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4562 });
4563 editor.join_lines(&JoinLines, window, cx);
4564 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4565
4566 // We don't insert a space for a line containing only spaces
4567 editor.join_lines(&JoinLines, window, cx);
4568 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4569
4570 // We ignore any leading tabs
4571 editor.join_lines(&JoinLines, window, cx);
4572 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4573
4574 editor
4575 });
4576}
4577
4578#[gpui::test]
4579fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4580 init_test(cx, |_| {});
4581
4582 cx.add_window(|window, cx| {
4583 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4584 let mut editor = build_editor(buffer.clone(), window, cx);
4585 let buffer = buffer.read(cx).as_singleton().unwrap();
4586
4587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4588 s.select_ranges([
4589 Point::new(0, 2)..Point::new(1, 1),
4590 Point::new(1, 2)..Point::new(1, 2),
4591 Point::new(3, 1)..Point::new(3, 2),
4592 ])
4593 });
4594
4595 editor.join_lines(&JoinLines, window, cx);
4596 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4597
4598 assert_eq!(
4599 editor
4600 .selections
4601 .ranges::<Point>(&editor.display_snapshot(cx)),
4602 [
4603 Point::new(0, 7)..Point::new(0, 7),
4604 Point::new(1, 3)..Point::new(1, 3)
4605 ]
4606 );
4607 editor
4608 });
4609}
4610
4611#[gpui::test]
4612async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4613 init_test(cx, |_| {});
4614
4615 let mut cx = EditorTestContext::new(cx).await;
4616
4617 let diff_base = r#"
4618 Line 0
4619 Line 1
4620 Line 2
4621 Line 3
4622 "#
4623 .unindent();
4624
4625 cx.set_state(
4626 &r#"
4627 ˇLine 0
4628 Line 1
4629 Line 2
4630 Line 3
4631 "#
4632 .unindent(),
4633 );
4634
4635 cx.set_head_text(&diff_base);
4636 executor.run_until_parked();
4637
4638 // Join lines
4639 cx.update_editor(|editor, window, cx| {
4640 editor.join_lines(&JoinLines, window, cx);
4641 });
4642 executor.run_until_parked();
4643
4644 cx.assert_editor_state(
4645 &r#"
4646 Line 0ˇ Line 1
4647 Line 2
4648 Line 3
4649 "#
4650 .unindent(),
4651 );
4652 // Join again
4653 cx.update_editor(|editor, window, cx| {
4654 editor.join_lines(&JoinLines, window, cx);
4655 });
4656 executor.run_until_parked();
4657
4658 cx.assert_editor_state(
4659 &r#"
4660 Line 0 Line 1ˇ Line 2
4661 Line 3
4662 "#
4663 .unindent(),
4664 );
4665}
4666
4667#[gpui::test]
4668async fn test_custom_newlines_cause_no_false_positive_diffs(
4669 executor: BackgroundExecutor,
4670 cx: &mut TestAppContext,
4671) {
4672 init_test(cx, |_| {});
4673 let mut cx = EditorTestContext::new(cx).await;
4674 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4675 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4676 executor.run_until_parked();
4677
4678 cx.update_editor(|editor, window, cx| {
4679 let snapshot = editor.snapshot(window, cx);
4680 assert_eq!(
4681 snapshot
4682 .buffer_snapshot()
4683 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4684 .collect::<Vec<_>>(),
4685 Vec::new(),
4686 "Should not have any diffs for files with custom newlines"
4687 );
4688 });
4689}
4690
4691#[gpui::test]
4692async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4693 init_test(cx, |_| {});
4694
4695 let mut cx = EditorTestContext::new(cx).await;
4696
4697 // Test sort_lines_case_insensitive()
4698 cx.set_state(indoc! {"
4699 «z
4700 y
4701 x
4702 Z
4703 Y
4704 Xˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| {
4707 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4708 });
4709 cx.assert_editor_state(indoc! {"
4710 «x
4711 X
4712 y
4713 Y
4714 z
4715 Zˇ»
4716 "});
4717
4718 // Test sort_lines_by_length()
4719 //
4720 // Demonstrates:
4721 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4722 // - sort is stable
4723 cx.set_state(indoc! {"
4724 «123
4725 æ
4726 12
4727 ∞
4728 1
4729 æˇ»
4730 "});
4731 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4732 cx.assert_editor_state(indoc! {"
4733 «æ
4734 ∞
4735 1
4736 æ
4737 12
4738 123ˇ»
4739 "});
4740
4741 // Test reverse_lines()
4742 cx.set_state(indoc! {"
4743 «5
4744 4
4745 3
4746 2
4747 1ˇ»
4748 "});
4749 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4750 cx.assert_editor_state(indoc! {"
4751 «1
4752 2
4753 3
4754 4
4755 5ˇ»
4756 "});
4757
4758 // Skip testing shuffle_line()
4759
4760 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4761 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4762
4763 // Don't manipulate when cursor is on single line, but expand the selection
4764 cx.set_state(indoc! {"
4765 ddˇdd
4766 ccc
4767 bb
4768 a
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «ddddˇ»
4775 ccc
4776 bb
4777 a
4778 "});
4779
4780 // Basic manipulate case
4781 // Start selection moves to column 0
4782 // End of selection shrinks to fit shorter line
4783 cx.set_state(indoc! {"
4784 dd«d
4785 ccc
4786 bb
4787 aaaaaˇ»
4788 "});
4789 cx.update_editor(|e, window, cx| {
4790 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4791 });
4792 cx.assert_editor_state(indoc! {"
4793 «aaaaa
4794 bb
4795 ccc
4796 dddˇ»
4797 "});
4798
4799 // Manipulate case with newlines
4800 cx.set_state(indoc! {"
4801 dd«d
4802 ccc
4803
4804 bb
4805 aaaaa
4806
4807 ˇ»
4808 "});
4809 cx.update_editor(|e, window, cx| {
4810 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4811 });
4812 cx.assert_editor_state(indoc! {"
4813 «
4814
4815 aaaaa
4816 bb
4817 ccc
4818 dddˇ»
4819
4820 "});
4821
4822 // Adding new line
4823 cx.set_state(indoc! {"
4824 aa«a
4825 bbˇ»b
4826 "});
4827 cx.update_editor(|e, window, cx| {
4828 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4829 });
4830 cx.assert_editor_state(indoc! {"
4831 «aaa
4832 bbb
4833 added_lineˇ»
4834 "});
4835
4836 // Removing line
4837 cx.set_state(indoc! {"
4838 aa«a
4839 bbbˇ»
4840 "});
4841 cx.update_editor(|e, window, cx| {
4842 e.manipulate_immutable_lines(window, cx, |lines| {
4843 lines.pop();
4844 })
4845 });
4846 cx.assert_editor_state(indoc! {"
4847 «aaaˇ»
4848 "});
4849
4850 // Removing all lines
4851 cx.set_state(indoc! {"
4852 aa«a
4853 bbbˇ»
4854 "});
4855 cx.update_editor(|e, window, cx| {
4856 e.manipulate_immutable_lines(window, cx, |lines| {
4857 lines.drain(..);
4858 })
4859 });
4860 cx.assert_editor_state(indoc! {"
4861 ˇ
4862 "});
4863}
4864
4865#[gpui::test]
4866async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4867 init_test(cx, |_| {});
4868
4869 let mut cx = EditorTestContext::new(cx).await;
4870
4871 // Consider continuous selection as single selection
4872 cx.set_state(indoc! {"
4873 Aaa«aa
4874 cˇ»c«c
4875 bb
4876 aaaˇ»aa
4877 "});
4878 cx.update_editor(|e, window, cx| {
4879 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4880 });
4881 cx.assert_editor_state(indoc! {"
4882 «Aaaaa
4883 ccc
4884 bb
4885 aaaaaˇ»
4886 "});
4887
4888 cx.set_state(indoc! {"
4889 Aaa«aa
4890 cˇ»c«c
4891 bb
4892 aaaˇ»aa
4893 "});
4894 cx.update_editor(|e, window, cx| {
4895 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4896 });
4897 cx.assert_editor_state(indoc! {"
4898 «Aaaaa
4899 ccc
4900 bbˇ»
4901 "});
4902
4903 // Consider non continuous selection as distinct dedup operations
4904 cx.set_state(indoc! {"
4905 «aaaaa
4906 bb
4907 aaaaa
4908 aaaaaˇ»
4909
4910 aaa«aaˇ»
4911 "});
4912 cx.update_editor(|e, window, cx| {
4913 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4914 });
4915 cx.assert_editor_state(indoc! {"
4916 «aaaaa
4917 bbˇ»
4918
4919 «aaaaaˇ»
4920 "});
4921}
4922
4923#[gpui::test]
4924async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4925 init_test(cx, |_| {});
4926
4927 let mut cx = EditorTestContext::new(cx).await;
4928
4929 cx.set_state(indoc! {"
4930 «Aaa
4931 aAa
4932 Aaaˇ»
4933 "});
4934 cx.update_editor(|e, window, cx| {
4935 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4936 });
4937 cx.assert_editor_state(indoc! {"
4938 «Aaa
4939 aAaˇ»
4940 "});
4941
4942 cx.set_state(indoc! {"
4943 «Aaa
4944 aAa
4945 aaAˇ»
4946 "});
4947 cx.update_editor(|e, window, cx| {
4948 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4949 });
4950 cx.assert_editor_state(indoc! {"
4951 «Aaaˇ»
4952 "});
4953}
4954
4955#[gpui::test]
4956async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4957 init_test(cx, |_| {});
4958
4959 let mut cx = EditorTestContext::new(cx).await;
4960
4961 let js_language = Arc::new(Language::new(
4962 LanguageConfig {
4963 name: "JavaScript".into(),
4964 wrap_characters: Some(language::WrapCharactersConfig {
4965 start_prefix: "<".into(),
4966 start_suffix: ">".into(),
4967 end_prefix: "</".into(),
4968 end_suffix: ">".into(),
4969 }),
4970 ..LanguageConfig::default()
4971 },
4972 None,
4973 ));
4974
4975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4976
4977 cx.set_state(indoc! {"
4978 «testˇ»
4979 "});
4980 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4981 cx.assert_editor_state(indoc! {"
4982 <«ˇ»>test</«ˇ»>
4983 "});
4984
4985 cx.set_state(indoc! {"
4986 «test
4987 testˇ»
4988 "});
4989 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4990 cx.assert_editor_state(indoc! {"
4991 <«ˇ»>test
4992 test</«ˇ»>
4993 "});
4994
4995 cx.set_state(indoc! {"
4996 teˇst
4997 "});
4998 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4999 cx.assert_editor_state(indoc! {"
5000 te<«ˇ»></«ˇ»>st
5001 "});
5002}
5003
5004#[gpui::test]
5005async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5006 init_test(cx, |_| {});
5007
5008 let mut cx = EditorTestContext::new(cx).await;
5009
5010 let js_language = Arc::new(Language::new(
5011 LanguageConfig {
5012 name: "JavaScript".into(),
5013 wrap_characters: Some(language::WrapCharactersConfig {
5014 start_prefix: "<".into(),
5015 start_suffix: ">".into(),
5016 end_prefix: "</".into(),
5017 end_suffix: ">".into(),
5018 }),
5019 ..LanguageConfig::default()
5020 },
5021 None,
5022 ));
5023
5024 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5025
5026 cx.set_state(indoc! {"
5027 «testˇ»
5028 «testˇ» «testˇ»
5029 «testˇ»
5030 "});
5031 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5032 cx.assert_editor_state(indoc! {"
5033 <«ˇ»>test</«ˇ»>
5034 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5035 <«ˇ»>test</«ˇ»>
5036 "});
5037
5038 cx.set_state(indoc! {"
5039 «test
5040 testˇ»
5041 «test
5042 testˇ»
5043 "});
5044 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5045 cx.assert_editor_state(indoc! {"
5046 <«ˇ»>test
5047 test</«ˇ»>
5048 <«ˇ»>test
5049 test</«ˇ»>
5050 "});
5051}
5052
5053#[gpui::test]
5054async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5055 init_test(cx, |_| {});
5056
5057 let mut cx = EditorTestContext::new(cx).await;
5058
5059 let plaintext_language = Arc::new(Language::new(
5060 LanguageConfig {
5061 name: "Plain Text".into(),
5062 ..LanguageConfig::default()
5063 },
5064 None,
5065 ));
5066
5067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5068
5069 cx.set_state(indoc! {"
5070 «testˇ»
5071 "});
5072 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5073 cx.assert_editor_state(indoc! {"
5074 «testˇ»
5075 "});
5076}
5077
5078#[gpui::test]
5079async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5080 init_test(cx, |_| {});
5081
5082 let mut cx = EditorTestContext::new(cx).await;
5083
5084 // Manipulate with multiple selections on a single line
5085 cx.set_state(indoc! {"
5086 dd«dd
5087 cˇ»c«c
5088 bb
5089 aaaˇ»aa
5090 "});
5091 cx.update_editor(|e, window, cx| {
5092 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5093 });
5094 cx.assert_editor_state(indoc! {"
5095 «aaaaa
5096 bb
5097 ccc
5098 ddddˇ»
5099 "});
5100
5101 // Manipulate with multiple disjoin selections
5102 cx.set_state(indoc! {"
5103 5«
5104 4
5105 3
5106 2
5107 1ˇ»
5108
5109 dd«dd
5110 ccc
5111 bb
5112 aaaˇ»aa
5113 "});
5114 cx.update_editor(|e, window, cx| {
5115 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5116 });
5117 cx.assert_editor_state(indoc! {"
5118 «1
5119 2
5120 3
5121 4
5122 5ˇ»
5123
5124 «aaaaa
5125 bb
5126 ccc
5127 ddddˇ»
5128 "});
5129
5130 // Adding lines on each selection
5131 cx.set_state(indoc! {"
5132 2«
5133 1ˇ»
5134
5135 bb«bb
5136 aaaˇ»aa
5137 "});
5138 cx.update_editor(|e, window, cx| {
5139 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5140 });
5141 cx.assert_editor_state(indoc! {"
5142 «2
5143 1
5144 added lineˇ»
5145
5146 «bbbb
5147 aaaaa
5148 added lineˇ»
5149 "});
5150
5151 // Removing lines on each selection
5152 cx.set_state(indoc! {"
5153 2«
5154 1ˇ»
5155
5156 bb«bb
5157 aaaˇ»aa
5158 "});
5159 cx.update_editor(|e, window, cx| {
5160 e.manipulate_immutable_lines(window, cx, |lines| {
5161 lines.pop();
5162 })
5163 });
5164 cx.assert_editor_state(indoc! {"
5165 «2ˇ»
5166
5167 «bbbbˇ»
5168 "});
5169}
5170
5171#[gpui::test]
5172async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5173 init_test(cx, |settings| {
5174 settings.defaults.tab_size = NonZeroU32::new(3)
5175 });
5176
5177 let mut cx = EditorTestContext::new(cx).await;
5178
5179 // MULTI SELECTION
5180 // Ln.1 "«" tests empty lines
5181 // Ln.9 tests just leading whitespace
5182 cx.set_state(indoc! {"
5183 «
5184 abc // No indentationˇ»
5185 «\tabc // 1 tabˇ»
5186 \t\tabc « ˇ» // 2 tabs
5187 \t ab«c // Tab followed by space
5188 \tabc // Space followed by tab (3 spaces should be the result)
5189 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5190 abˇ»ˇc ˇ ˇ // Already space indented«
5191 \t
5192 \tabc\tdef // Only the leading tab is manipulatedˇ»
5193 "});
5194 cx.update_editor(|e, window, cx| {
5195 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5196 });
5197 cx.assert_editor_state(
5198 indoc! {"
5199 «
5200 abc // No indentation
5201 abc // 1 tab
5202 abc // 2 tabs
5203 abc // Tab followed by space
5204 abc // Space followed by tab (3 spaces should be the result)
5205 abc // Mixed indentation (tab conversion depends on the column)
5206 abc // Already space indented
5207 ·
5208 abc\tdef // Only the leading tab is manipulatedˇ»
5209 "}
5210 .replace("·", "")
5211 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5212 );
5213
5214 // Test on just a few lines, the others should remain unchanged
5215 // Only lines (3, 5, 10, 11) should change
5216 cx.set_state(
5217 indoc! {"
5218 ·
5219 abc // No indentation
5220 \tabcˇ // 1 tab
5221 \t\tabc // 2 tabs
5222 \t abcˇ // Tab followed by space
5223 \tabc // Space followed by tab (3 spaces should be the result)
5224 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5225 abc // Already space indented
5226 «\t
5227 \tabc\tdef // Only the leading tab is manipulatedˇ»
5228 "}
5229 .replace("·", "")
5230 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5231 );
5232 cx.update_editor(|e, window, cx| {
5233 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5234 });
5235 cx.assert_editor_state(
5236 indoc! {"
5237 ·
5238 abc // No indentation
5239 « abc // 1 tabˇ»
5240 \t\tabc // 2 tabs
5241 « abc // Tab followed by spaceˇ»
5242 \tabc // Space followed by tab (3 spaces should be the result)
5243 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5244 abc // Already space indented
5245 « ·
5246 abc\tdef // Only the leading tab is manipulatedˇ»
5247 "}
5248 .replace("·", "")
5249 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5250 );
5251
5252 // SINGLE SELECTION
5253 // Ln.1 "«" tests empty lines
5254 // Ln.9 tests just leading whitespace
5255 cx.set_state(indoc! {"
5256 «
5257 abc // No indentation
5258 \tabc // 1 tab
5259 \t\tabc // 2 tabs
5260 \t abc // Tab followed by space
5261 \tabc // Space followed by tab (3 spaces should be the result)
5262 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5263 abc // Already space indented
5264 \t
5265 \tabc\tdef // Only the leading tab is manipulatedˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| {
5268 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5269 });
5270 cx.assert_editor_state(
5271 indoc! {"
5272 «
5273 abc // No indentation
5274 abc // 1 tab
5275 abc // 2 tabs
5276 abc // Tab followed by space
5277 abc // Space followed by tab (3 spaces should be the result)
5278 abc // Mixed indentation (tab conversion depends on the column)
5279 abc // Already space indented
5280 ·
5281 abc\tdef // Only the leading tab is manipulatedˇ»
5282 "}
5283 .replace("·", "")
5284 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5285 );
5286}
5287
5288#[gpui::test]
5289async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5290 init_test(cx, |settings| {
5291 settings.defaults.tab_size = NonZeroU32::new(3)
5292 });
5293
5294 let mut cx = EditorTestContext::new(cx).await;
5295
5296 // MULTI SELECTION
5297 // Ln.1 "«" tests empty lines
5298 // Ln.11 tests just leading whitespace
5299 cx.set_state(indoc! {"
5300 «
5301 abˇ»ˇc // No indentation
5302 abc ˇ ˇ // 1 space (< 3 so dont convert)
5303 abc « // 2 spaces (< 3 so dont convert)
5304 abc // 3 spaces (convert)
5305 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5306 «\tˇ»\t«\tˇ»abc // Already tab indented
5307 «\t abc // Tab followed by space
5308 \tabc // Space followed by tab (should be consumed due to tab)
5309 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5310 \tˇ» «\t
5311 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5312 "});
5313 cx.update_editor(|e, window, cx| {
5314 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5315 });
5316 cx.assert_editor_state(indoc! {"
5317 «
5318 abc // No indentation
5319 abc // 1 space (< 3 so dont convert)
5320 abc // 2 spaces (< 3 so dont convert)
5321 \tabc // 3 spaces (convert)
5322 \t abc // 5 spaces (1 tab + 2 spaces)
5323 \t\t\tabc // Already tab indented
5324 \t abc // Tab followed by space
5325 \tabc // Space followed by tab (should be consumed due to tab)
5326 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5327 \t\t\t
5328 \tabc \t // Only the leading spaces should be convertedˇ»
5329 "});
5330
5331 // Test on just a few lines, the other should remain unchanged
5332 // Only lines (4, 8, 11, 12) should change
5333 cx.set_state(
5334 indoc! {"
5335 ·
5336 abc // No indentation
5337 abc // 1 space (< 3 so dont convert)
5338 abc // 2 spaces (< 3 so dont convert)
5339 « abc // 3 spaces (convert)ˇ»
5340 abc // 5 spaces (1 tab + 2 spaces)
5341 \t\t\tabc // Already tab indented
5342 \t abc // Tab followed by space
5343 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5344 \t\t \tabc // Mixed indentation
5345 \t \t \t \tabc // Mixed indentation
5346 \t \tˇ
5347 « abc \t // Only the leading spaces should be convertedˇ»
5348 "}
5349 .replace("·", "")
5350 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5351 );
5352 cx.update_editor(|e, window, cx| {
5353 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5354 });
5355 cx.assert_editor_state(
5356 indoc! {"
5357 ·
5358 abc // No indentation
5359 abc // 1 space (< 3 so dont convert)
5360 abc // 2 spaces (< 3 so dont convert)
5361 «\tabc // 3 spaces (convert)ˇ»
5362 abc // 5 spaces (1 tab + 2 spaces)
5363 \t\t\tabc // Already tab indented
5364 \t abc // Tab followed by space
5365 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5366 \t\t \tabc // Mixed indentation
5367 \t \t \t \tabc // Mixed indentation
5368 «\t\t\t
5369 \tabc \t // Only the leading spaces should be convertedˇ»
5370 "}
5371 .replace("·", "")
5372 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5373 );
5374
5375 // SINGLE SELECTION
5376 // Ln.1 "«" tests empty lines
5377 // Ln.11 tests just leading whitespace
5378 cx.set_state(indoc! {"
5379 «
5380 abc // No indentation
5381 abc // 1 space (< 3 so dont convert)
5382 abc // 2 spaces (< 3 so dont convert)
5383 abc // 3 spaces (convert)
5384 abc // 5 spaces (1 tab + 2 spaces)
5385 \t\t\tabc // Already tab indented
5386 \t abc // Tab followed by space
5387 \tabc // Space followed by tab (should be consumed due to tab)
5388 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5389 \t \t
5390 abc \t // Only the leading spaces should be convertedˇ»
5391 "});
5392 cx.update_editor(|e, window, cx| {
5393 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5394 });
5395 cx.assert_editor_state(indoc! {"
5396 «
5397 abc // No indentation
5398 abc // 1 space (< 3 so dont convert)
5399 abc // 2 spaces (< 3 so dont convert)
5400 \tabc // 3 spaces (convert)
5401 \t abc // 5 spaces (1 tab + 2 spaces)
5402 \t\t\tabc // Already tab indented
5403 \t abc // Tab followed by space
5404 \tabc // Space followed by tab (should be consumed due to tab)
5405 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5406 \t\t\t
5407 \tabc \t // Only the leading spaces should be convertedˇ»
5408 "});
5409}
5410
5411#[gpui::test]
5412async fn test_toggle_case(cx: &mut TestAppContext) {
5413 init_test(cx, |_| {});
5414
5415 let mut cx = EditorTestContext::new(cx).await;
5416
5417 // If all lower case -> upper case
5418 cx.set_state(indoc! {"
5419 «hello worldˇ»
5420 "});
5421 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5422 cx.assert_editor_state(indoc! {"
5423 «HELLO WORLDˇ»
5424 "});
5425
5426 // If all upper case -> lower case
5427 cx.set_state(indoc! {"
5428 «HELLO WORLDˇ»
5429 "});
5430 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5431 cx.assert_editor_state(indoc! {"
5432 «hello worldˇ»
5433 "});
5434
5435 // If any upper case characters are identified -> lower case
5436 // This matches JetBrains IDEs
5437 cx.set_state(indoc! {"
5438 «hEllo worldˇ»
5439 "});
5440 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5441 cx.assert_editor_state(indoc! {"
5442 «hello worldˇ»
5443 "});
5444}
5445
5446#[gpui::test]
5447async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5448 init_test(cx, |_| {});
5449
5450 let mut cx = EditorTestContext::new(cx).await;
5451
5452 cx.set_state(indoc! {"
5453 «implement-windows-supportˇ»
5454 "});
5455 cx.update_editor(|e, window, cx| {
5456 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5457 });
5458 cx.assert_editor_state(indoc! {"
5459 «Implement windows supportˇ»
5460 "});
5461}
5462
5463#[gpui::test]
5464async fn test_manipulate_text(cx: &mut TestAppContext) {
5465 init_test(cx, |_| {});
5466
5467 let mut cx = EditorTestContext::new(cx).await;
5468
5469 // Test convert_to_upper_case()
5470 cx.set_state(indoc! {"
5471 «hello worldˇ»
5472 "});
5473 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5474 cx.assert_editor_state(indoc! {"
5475 «HELLO WORLDˇ»
5476 "});
5477
5478 // Test convert_to_lower_case()
5479 cx.set_state(indoc! {"
5480 «HELLO WORLDˇ»
5481 "});
5482 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5483 cx.assert_editor_state(indoc! {"
5484 «hello worldˇ»
5485 "});
5486
5487 // Test multiple line, single selection case
5488 cx.set_state(indoc! {"
5489 «The quick brown
5490 fox jumps over
5491 the lazy dogˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «The Quick Brown
5496 Fox Jumps Over
5497 The Lazy Dogˇ»
5498 "});
5499
5500 // Test multiple line, single selection case
5501 cx.set_state(indoc! {"
5502 «The quick brown
5503 fox jumps over
5504 the lazy dogˇ»
5505 "});
5506 cx.update_editor(|e, window, cx| {
5507 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5508 });
5509 cx.assert_editor_state(indoc! {"
5510 «TheQuickBrown
5511 FoxJumpsOver
5512 TheLazyDogˇ»
5513 "});
5514
5515 // From here on out, test more complex cases of manipulate_text()
5516
5517 // Test no selection case - should affect words cursors are in
5518 // Cursor at beginning, middle, and end of word
5519 cx.set_state(indoc! {"
5520 ˇhello big beauˇtiful worldˇ
5521 "});
5522 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5523 cx.assert_editor_state(indoc! {"
5524 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5525 "});
5526
5527 // Test multiple selections on a single line and across multiple lines
5528 cx.set_state(indoc! {"
5529 «Theˇ» quick «brown
5530 foxˇ» jumps «overˇ»
5531 the «lazyˇ» dog
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «THEˇ» quick «BROWN
5536 FOXˇ» jumps «OVERˇ»
5537 the «LAZYˇ» dog
5538 "});
5539
5540 // Test case where text length grows
5541 cx.set_state(indoc! {"
5542 «tschüߡ»
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «TSCHÜSSˇ»
5547 "});
5548
5549 // Test to make sure we don't crash when text shrinks
5550 cx.set_state(indoc! {"
5551 aaa_bbbˇ
5552 "});
5553 cx.update_editor(|e, window, cx| {
5554 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5555 });
5556 cx.assert_editor_state(indoc! {"
5557 «aaaBbbˇ»
5558 "});
5559
5560 // Test to make sure we all aware of the fact that each word can grow and shrink
5561 // Final selections should be aware of this fact
5562 cx.set_state(indoc! {"
5563 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5564 "});
5565 cx.update_editor(|e, window, cx| {
5566 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5567 });
5568 cx.assert_editor_state(indoc! {"
5569 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5570 "});
5571
5572 cx.set_state(indoc! {"
5573 «hElLo, WoRld!ˇ»
5574 "});
5575 cx.update_editor(|e, window, cx| {
5576 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5577 });
5578 cx.assert_editor_state(indoc! {"
5579 «HeLlO, wOrLD!ˇ»
5580 "});
5581
5582 // Test selections with `line_mode() = true`.
5583 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5584 cx.set_state(indoc! {"
5585 «The quick brown
5586 fox jumps over
5587 tˇ»he lazy dog
5588 "});
5589 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 «THE QUICK BROWN
5592 FOX JUMPS OVER
5593 THE LAZY DOGˇ»
5594 "});
5595}
5596
5597#[gpui::test]
5598fn test_duplicate_line(cx: &mut TestAppContext) {
5599 init_test(cx, |_| {});
5600
5601 let editor = cx.add_window(|window, cx| {
5602 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5603 build_editor(buffer, window, cx)
5604 });
5605 _ = editor.update(cx, |editor, window, cx| {
5606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5607 s.select_display_ranges([
5608 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5609 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5610 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5611 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5612 ])
5613 });
5614 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5615 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5616 assert_eq!(
5617 editor.selections.display_ranges(cx),
5618 vec![
5619 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5620 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5621 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5622 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5623 ]
5624 );
5625 });
5626
5627 let editor = cx.add_window(|window, cx| {
5628 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5629 build_editor(buffer, window, cx)
5630 });
5631 _ = editor.update(cx, |editor, window, cx| {
5632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5633 s.select_display_ranges([
5634 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5635 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5636 ])
5637 });
5638 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5639 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5640 assert_eq!(
5641 editor.selections.display_ranges(cx),
5642 vec![
5643 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5644 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5645 ]
5646 );
5647 });
5648
5649 // With `move_upwards` the selections stay in place, except for
5650 // the lines inserted above them
5651 let editor = cx.add_window(|window, cx| {
5652 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5653 build_editor(buffer, window, cx)
5654 });
5655 _ = editor.update(cx, |editor, window, cx| {
5656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5657 s.select_display_ranges([
5658 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5659 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5660 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5661 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5662 ])
5663 });
5664 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5665 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5666 assert_eq!(
5667 editor.selections.display_ranges(cx),
5668 vec![
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5672 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5673 ]
5674 );
5675 });
5676
5677 let editor = cx.add_window(|window, cx| {
5678 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5679 build_editor(buffer, window, cx)
5680 });
5681 _ = editor.update(cx, |editor, window, cx| {
5682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5683 s.select_display_ranges([
5684 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5685 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5686 ])
5687 });
5688 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5689 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5690 assert_eq!(
5691 editor.selections.display_ranges(cx),
5692 vec![
5693 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5694 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5695 ]
5696 );
5697 });
5698
5699 let editor = cx.add_window(|window, cx| {
5700 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5701 build_editor(buffer, window, cx)
5702 });
5703 _ = editor.update(cx, |editor, window, cx| {
5704 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5705 s.select_display_ranges([
5706 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5707 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5708 ])
5709 });
5710 editor.duplicate_selection(&DuplicateSelection, window, cx);
5711 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5712 assert_eq!(
5713 editor.selections.display_ranges(cx),
5714 vec![
5715 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5716 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5717 ]
5718 );
5719 });
5720}
5721
5722#[gpui::test]
5723fn test_move_line_up_down(cx: &mut TestAppContext) {
5724 init_test(cx, |_| {});
5725
5726 let editor = cx.add_window(|window, cx| {
5727 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5728 build_editor(buffer, window, cx)
5729 });
5730 _ = editor.update(cx, |editor, window, cx| {
5731 editor.fold_creases(
5732 vec![
5733 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5734 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5735 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5736 ],
5737 true,
5738 window,
5739 cx,
5740 );
5741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5742 s.select_display_ranges([
5743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5744 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5745 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5746 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5747 ])
5748 });
5749 assert_eq!(
5750 editor.display_text(cx),
5751 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5752 );
5753
5754 editor.move_line_up(&MoveLineUp, window, cx);
5755 assert_eq!(
5756 editor.display_text(cx),
5757 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5758 );
5759 assert_eq!(
5760 editor.selections.display_ranges(cx),
5761 vec![
5762 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5763 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5764 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5765 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5766 ]
5767 );
5768 });
5769
5770 _ = editor.update(cx, |editor, window, cx| {
5771 editor.move_line_down(&MoveLineDown, window, cx);
5772 assert_eq!(
5773 editor.display_text(cx),
5774 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5775 );
5776 assert_eq!(
5777 editor.selections.display_ranges(cx),
5778 vec![
5779 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5780 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5781 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5782 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5783 ]
5784 );
5785 });
5786
5787 _ = editor.update(cx, |editor, window, cx| {
5788 editor.move_line_down(&MoveLineDown, window, cx);
5789 assert_eq!(
5790 editor.display_text(cx),
5791 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5792 );
5793 assert_eq!(
5794 editor.selections.display_ranges(cx),
5795 vec![
5796 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5797 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5798 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5799 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5800 ]
5801 );
5802 });
5803
5804 _ = editor.update(cx, |editor, window, cx| {
5805 editor.move_line_up(&MoveLineUp, window, cx);
5806 assert_eq!(
5807 editor.display_text(cx),
5808 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5809 );
5810 assert_eq!(
5811 editor.selections.display_ranges(cx),
5812 vec![
5813 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5814 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5815 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5816 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5817 ]
5818 );
5819 });
5820}
5821
5822#[gpui::test]
5823fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825 let editor = cx.add_window(|window, cx| {
5826 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5827 build_editor(buffer, window, cx)
5828 });
5829 _ = editor.update(cx, |editor, window, cx| {
5830 editor.fold_creases(
5831 vec![Crease::simple(
5832 Point::new(6, 4)..Point::new(7, 4),
5833 FoldPlaceholder::test(),
5834 )],
5835 true,
5836 window,
5837 cx,
5838 );
5839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5840 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5841 });
5842 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5843 editor.move_line_up(&MoveLineUp, window, cx);
5844 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5845 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5846 });
5847}
5848
5849#[gpui::test]
5850fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5851 init_test(cx, |_| {});
5852
5853 let editor = cx.add_window(|window, cx| {
5854 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5855 build_editor(buffer, window, cx)
5856 });
5857 _ = editor.update(cx, |editor, window, cx| {
5858 let snapshot = editor.buffer.read(cx).snapshot(cx);
5859 editor.insert_blocks(
5860 [BlockProperties {
5861 style: BlockStyle::Fixed,
5862 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5863 height: Some(1),
5864 render: Arc::new(|_| div().into_any()),
5865 priority: 0,
5866 }],
5867 Some(Autoscroll::fit()),
5868 cx,
5869 );
5870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5871 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5872 });
5873 editor.move_line_down(&MoveLineDown, window, cx);
5874 });
5875}
5876
5877#[gpui::test]
5878async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5879 init_test(cx, |_| {});
5880
5881 let mut cx = EditorTestContext::new(cx).await;
5882 cx.set_state(
5883 &"
5884 ˇzero
5885 one
5886 two
5887 three
5888 four
5889 five
5890 "
5891 .unindent(),
5892 );
5893
5894 // Create a four-line block that replaces three lines of text.
5895 cx.update_editor(|editor, window, cx| {
5896 let snapshot = editor.snapshot(window, cx);
5897 let snapshot = &snapshot.buffer_snapshot();
5898 let placement = BlockPlacement::Replace(
5899 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5900 );
5901 editor.insert_blocks(
5902 [BlockProperties {
5903 placement,
5904 height: Some(4),
5905 style: BlockStyle::Sticky,
5906 render: Arc::new(|_| gpui::div().into_any_element()),
5907 priority: 0,
5908 }],
5909 None,
5910 cx,
5911 );
5912 });
5913
5914 // Move down so that the cursor touches the block.
5915 cx.update_editor(|editor, window, cx| {
5916 editor.move_down(&Default::default(), window, cx);
5917 });
5918 cx.assert_editor_state(
5919 &"
5920 zero
5921 «one
5922 two
5923 threeˇ»
5924 four
5925 five
5926 "
5927 .unindent(),
5928 );
5929
5930 // Move down past the block.
5931 cx.update_editor(|editor, window, cx| {
5932 editor.move_down(&Default::default(), window, cx);
5933 });
5934 cx.assert_editor_state(
5935 &"
5936 zero
5937 one
5938 two
5939 three
5940 ˇfour
5941 five
5942 "
5943 .unindent(),
5944 );
5945}
5946
5947#[gpui::test]
5948fn test_transpose(cx: &mut TestAppContext) {
5949 init_test(cx, |_| {});
5950
5951 _ = cx.add_window(|window, cx| {
5952 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5953 editor.set_style(EditorStyle::default(), window, cx);
5954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5955 s.select_ranges([1..1])
5956 });
5957 editor.transpose(&Default::default(), window, cx);
5958 assert_eq!(editor.text(cx), "bac");
5959 assert_eq!(
5960 editor.selections.ranges(&editor.display_snapshot(cx)),
5961 [2..2]
5962 );
5963
5964 editor.transpose(&Default::default(), window, cx);
5965 assert_eq!(editor.text(cx), "bca");
5966 assert_eq!(
5967 editor.selections.ranges(&editor.display_snapshot(cx)),
5968 [3..3]
5969 );
5970
5971 editor.transpose(&Default::default(), window, cx);
5972 assert_eq!(editor.text(cx), "bac");
5973 assert_eq!(
5974 editor.selections.ranges(&editor.display_snapshot(cx)),
5975 [3..3]
5976 );
5977
5978 editor
5979 });
5980
5981 _ = cx.add_window(|window, cx| {
5982 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5983 editor.set_style(EditorStyle::default(), window, cx);
5984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5985 s.select_ranges([3..3])
5986 });
5987 editor.transpose(&Default::default(), window, cx);
5988 assert_eq!(editor.text(cx), "acb\nde");
5989 assert_eq!(
5990 editor.selections.ranges(&editor.display_snapshot(cx)),
5991 [3..3]
5992 );
5993
5994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5995 s.select_ranges([4..4])
5996 });
5997 editor.transpose(&Default::default(), window, cx);
5998 assert_eq!(editor.text(cx), "acbd\ne");
5999 assert_eq!(
6000 editor.selections.ranges(&editor.display_snapshot(cx)),
6001 [5..5]
6002 );
6003
6004 editor.transpose(&Default::default(), window, cx);
6005 assert_eq!(editor.text(cx), "acbde\n");
6006 assert_eq!(
6007 editor.selections.ranges(&editor.display_snapshot(cx)),
6008 [6..6]
6009 );
6010
6011 editor.transpose(&Default::default(), window, cx);
6012 assert_eq!(editor.text(cx), "acbd\ne");
6013 assert_eq!(
6014 editor.selections.ranges(&editor.display_snapshot(cx)),
6015 [6..6]
6016 );
6017
6018 editor
6019 });
6020
6021 _ = cx.add_window(|window, cx| {
6022 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6023 editor.set_style(EditorStyle::default(), window, cx);
6024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6025 s.select_ranges([1..1, 2..2, 4..4])
6026 });
6027 editor.transpose(&Default::default(), window, cx);
6028 assert_eq!(editor.text(cx), "bacd\ne");
6029 assert_eq!(
6030 editor.selections.ranges(&editor.display_snapshot(cx)),
6031 [2..2, 3..3, 5..5]
6032 );
6033
6034 editor.transpose(&Default::default(), window, cx);
6035 assert_eq!(editor.text(cx), "bcade\n");
6036 assert_eq!(
6037 editor.selections.ranges(&editor.display_snapshot(cx)),
6038 [3..3, 4..4, 6..6]
6039 );
6040
6041 editor.transpose(&Default::default(), window, cx);
6042 assert_eq!(editor.text(cx), "bcda\ne");
6043 assert_eq!(
6044 editor.selections.ranges(&editor.display_snapshot(cx)),
6045 [4..4, 6..6]
6046 );
6047
6048 editor.transpose(&Default::default(), window, cx);
6049 assert_eq!(editor.text(cx), "bcade\n");
6050 assert_eq!(
6051 editor.selections.ranges(&editor.display_snapshot(cx)),
6052 [4..4, 6..6]
6053 );
6054
6055 editor.transpose(&Default::default(), window, cx);
6056 assert_eq!(editor.text(cx), "bcaed\n");
6057 assert_eq!(
6058 editor.selections.ranges(&editor.display_snapshot(cx)),
6059 [5..5, 6..6]
6060 );
6061
6062 editor
6063 });
6064
6065 _ = cx.add_window(|window, cx| {
6066 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6067 editor.set_style(EditorStyle::default(), window, cx);
6068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6069 s.select_ranges([4..4])
6070 });
6071 editor.transpose(&Default::default(), window, cx);
6072 assert_eq!(editor.text(cx), "🏀🍐✋");
6073 assert_eq!(
6074 editor.selections.ranges(&editor.display_snapshot(cx)),
6075 [8..8]
6076 );
6077
6078 editor.transpose(&Default::default(), window, cx);
6079 assert_eq!(editor.text(cx), "🏀✋🍐");
6080 assert_eq!(
6081 editor.selections.ranges(&editor.display_snapshot(cx)),
6082 [11..11]
6083 );
6084
6085 editor.transpose(&Default::default(), window, cx);
6086 assert_eq!(editor.text(cx), "🏀🍐✋");
6087 assert_eq!(
6088 editor.selections.ranges(&editor.display_snapshot(cx)),
6089 [11..11]
6090 );
6091
6092 editor
6093 });
6094}
6095
6096#[gpui::test]
6097async fn test_rewrap(cx: &mut TestAppContext) {
6098 init_test(cx, |settings| {
6099 settings.languages.0.extend([
6100 (
6101 "Markdown".into(),
6102 LanguageSettingsContent {
6103 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6104 preferred_line_length: Some(40),
6105 ..Default::default()
6106 },
6107 ),
6108 (
6109 "Plain Text".into(),
6110 LanguageSettingsContent {
6111 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6112 preferred_line_length: Some(40),
6113 ..Default::default()
6114 },
6115 ),
6116 (
6117 "C++".into(),
6118 LanguageSettingsContent {
6119 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6120 preferred_line_length: Some(40),
6121 ..Default::default()
6122 },
6123 ),
6124 (
6125 "Python".into(),
6126 LanguageSettingsContent {
6127 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6128 preferred_line_length: Some(40),
6129 ..Default::default()
6130 },
6131 ),
6132 (
6133 "Rust".into(),
6134 LanguageSettingsContent {
6135 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6136 preferred_line_length: Some(40),
6137 ..Default::default()
6138 },
6139 ),
6140 ])
6141 });
6142
6143 let mut cx = EditorTestContext::new(cx).await;
6144
6145 let cpp_language = Arc::new(Language::new(
6146 LanguageConfig {
6147 name: "C++".into(),
6148 line_comments: vec!["// ".into()],
6149 ..LanguageConfig::default()
6150 },
6151 None,
6152 ));
6153 let python_language = Arc::new(Language::new(
6154 LanguageConfig {
6155 name: "Python".into(),
6156 line_comments: vec!["# ".into()],
6157 ..LanguageConfig::default()
6158 },
6159 None,
6160 ));
6161 let markdown_language = Arc::new(Language::new(
6162 LanguageConfig {
6163 name: "Markdown".into(),
6164 rewrap_prefixes: vec![
6165 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6166 regex::Regex::new("[-*+]\\s+").unwrap(),
6167 ],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let rust_language = Arc::new(
6173 Language::new(
6174 LanguageConfig {
6175 name: "Rust".into(),
6176 line_comments: vec!["// ".into(), "/// ".into()],
6177 ..LanguageConfig::default()
6178 },
6179 Some(tree_sitter_rust::LANGUAGE.into()),
6180 )
6181 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6182 .unwrap(),
6183 );
6184
6185 let plaintext_language = Arc::new(Language::new(
6186 LanguageConfig {
6187 name: "Plain Text".into(),
6188 ..LanguageConfig::default()
6189 },
6190 None,
6191 ));
6192
6193 // Test basic rewrapping of a long line with a cursor
6194 assert_rewrap(
6195 indoc! {"
6196 // ˇThis is a long comment that needs to be wrapped.
6197 "},
6198 indoc! {"
6199 // ˇThis is a long comment that needs to
6200 // be wrapped.
6201 "},
6202 cpp_language.clone(),
6203 &mut cx,
6204 );
6205
6206 // Test rewrapping a full selection
6207 assert_rewrap(
6208 indoc! {"
6209 «// This selected long comment needs to be wrapped.ˇ»"
6210 },
6211 indoc! {"
6212 «// This selected long comment needs to
6213 // be wrapped.ˇ»"
6214 },
6215 cpp_language.clone(),
6216 &mut cx,
6217 );
6218
6219 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6220 assert_rewrap(
6221 indoc! {"
6222 // ˇThis is the first line.
6223 // Thisˇ is the second line.
6224 // This is the thirdˇ line, all part of one paragraph.
6225 "},
6226 indoc! {"
6227 // ˇThis is the first line. Thisˇ is the
6228 // second line. This is the thirdˇ line,
6229 // all part of one paragraph.
6230 "},
6231 cpp_language.clone(),
6232 &mut cx,
6233 );
6234
6235 // Test multiple cursors in different paragraphs trigger separate rewraps
6236 assert_rewrap(
6237 indoc! {"
6238 // ˇThis is the first paragraph, first line.
6239 // ˇThis is the first paragraph, second line.
6240
6241 // ˇThis is the second paragraph, first line.
6242 // ˇThis is the second paragraph, second line.
6243 "},
6244 indoc! {"
6245 // ˇThis is the first paragraph, first
6246 // line. ˇThis is the first paragraph,
6247 // second line.
6248
6249 // ˇThis is the second paragraph, first
6250 // line. ˇThis is the second paragraph,
6251 // second line.
6252 "},
6253 cpp_language.clone(),
6254 &mut cx,
6255 );
6256
6257 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6258 assert_rewrap(
6259 indoc! {"
6260 «// A regular long long comment to be wrapped.
6261 /// A documentation long comment to be wrapped.ˇ»
6262 "},
6263 indoc! {"
6264 «// A regular long long comment to be
6265 // wrapped.
6266 /// A documentation long comment to be
6267 /// wrapped.ˇ»
6268 "},
6269 rust_language.clone(),
6270 &mut cx,
6271 );
6272
6273 // Test that change in indentation level trigger seperate rewraps
6274 assert_rewrap(
6275 indoc! {"
6276 fn foo() {
6277 «// This is a long comment at the base indent.
6278 // This is a long comment at the next indent.ˇ»
6279 }
6280 "},
6281 indoc! {"
6282 fn foo() {
6283 «// This is a long comment at the
6284 // base indent.
6285 // This is a long comment at the
6286 // next indent.ˇ»
6287 }
6288 "},
6289 rust_language.clone(),
6290 &mut cx,
6291 );
6292
6293 // Test that different comment prefix characters (e.g., '#') are handled correctly
6294 assert_rewrap(
6295 indoc! {"
6296 # ˇThis is a long comment using a pound sign.
6297 "},
6298 indoc! {"
6299 # ˇThis is a long comment using a pound
6300 # sign.
6301 "},
6302 python_language,
6303 &mut cx,
6304 );
6305
6306 // Test rewrapping only affects comments, not code even when selected
6307 assert_rewrap(
6308 indoc! {"
6309 «/// This doc comment is long and should be wrapped.
6310 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6311 "},
6312 indoc! {"
6313 «/// This doc comment is long and should
6314 /// be wrapped.
6315 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6316 "},
6317 rust_language.clone(),
6318 &mut cx,
6319 );
6320
6321 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6322 assert_rewrap(
6323 indoc! {"
6324 # Header
6325
6326 A long long long line of markdown text to wrap.ˇ
6327 "},
6328 indoc! {"
6329 # Header
6330
6331 A long long long line of markdown text
6332 to wrap.ˇ
6333 "},
6334 markdown_language.clone(),
6335 &mut cx,
6336 );
6337
6338 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6339 assert_rewrap(
6340 indoc! {"
6341 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6342 2. This is a numbered list item that is very long and needs to be wrapped properly.
6343 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6344 "},
6345 indoc! {"
6346 «1. This is a numbered list item that is
6347 very long and needs to be wrapped
6348 properly.
6349 2. This is a numbered list item that is
6350 very long and needs to be wrapped
6351 properly.
6352 - This is an unordered list item that is
6353 also very long and should not merge
6354 with the numbered item.ˇ»
6355 "},
6356 markdown_language.clone(),
6357 &mut cx,
6358 );
6359
6360 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6361 assert_rewrap(
6362 indoc! {"
6363 «1. This is a numbered list item that is
6364 very long and needs to be wrapped
6365 properly.
6366 2. This is a numbered list item that is
6367 very long and needs to be wrapped
6368 properly.
6369 - This is an unordered list item that is
6370 also very long and should not merge with
6371 the numbered item.ˇ»
6372 "},
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge
6382 with the numbered item.ˇ»
6383 "},
6384 markdown_language.clone(),
6385 &mut cx,
6386 );
6387
6388 // Test that rewrapping maintain indents even when they already exists.
6389 assert_rewrap(
6390 indoc! {"
6391 «1. This is a numbered list
6392 item that is very long and needs to be wrapped properly.
6393 2. This is a numbered list
6394 item that is very long and needs to be wrapped properly.
6395 - This is an unordered list item that is also very long and
6396 should not merge with the numbered item.ˇ»
6397 "},
6398 indoc! {"
6399 «1. This is a numbered list item that is
6400 very long and needs to be wrapped
6401 properly.
6402 2. This is a numbered list item that is
6403 very long and needs to be wrapped
6404 properly.
6405 - This is an unordered list item that is
6406 also very long and should not merge
6407 with the numbered item.ˇ»
6408 "},
6409 markdown_language,
6410 &mut cx,
6411 );
6412
6413 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6414 assert_rewrap(
6415 indoc! {"
6416 ˇThis is a very long line of plain text that will be wrapped.
6417 "},
6418 indoc! {"
6419 ˇThis is a very long line of plain text
6420 that will be wrapped.
6421 "},
6422 plaintext_language.clone(),
6423 &mut cx,
6424 );
6425
6426 // Test that non-commented code acts as a paragraph boundary within a selection
6427 assert_rewrap(
6428 indoc! {"
6429 «// This is the first long comment block to be wrapped.
6430 fn my_func(a: u32);
6431 // This is the second long comment block to be wrapped.ˇ»
6432 "},
6433 indoc! {"
6434 «// This is the first long comment block
6435 // to be wrapped.
6436 fn my_func(a: u32);
6437 // This is the second long comment block
6438 // to be wrapped.ˇ»
6439 "},
6440 rust_language,
6441 &mut cx,
6442 );
6443
6444 // Test rewrapping multiple selections, including ones with blank lines or tabs
6445 assert_rewrap(
6446 indoc! {"
6447 «ˇThis is a very long line that will be wrapped.
6448
6449 This is another paragraph in the same selection.»
6450
6451 «\tThis is a very long indented line that will be wrapped.ˇ»
6452 "},
6453 indoc! {"
6454 «ˇThis is a very long line that will be
6455 wrapped.
6456
6457 This is another paragraph in the same
6458 selection.»
6459
6460 «\tThis is a very long indented line
6461 \tthat will be wrapped.ˇ»
6462 "},
6463 plaintext_language,
6464 &mut cx,
6465 );
6466
6467 // Test that an empty comment line acts as a paragraph boundary
6468 assert_rewrap(
6469 indoc! {"
6470 // ˇThis is a long comment that will be wrapped.
6471 //
6472 // And this is another long comment that will also be wrapped.ˇ
6473 "},
6474 indoc! {"
6475 // ˇThis is a long comment that will be
6476 // wrapped.
6477 //
6478 // And this is another long comment that
6479 // will also be wrapped.ˇ
6480 "},
6481 cpp_language,
6482 &mut cx,
6483 );
6484
6485 #[track_caller]
6486 fn assert_rewrap(
6487 unwrapped_text: &str,
6488 wrapped_text: &str,
6489 language: Arc<Language>,
6490 cx: &mut EditorTestContext,
6491 ) {
6492 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6493 cx.set_state(unwrapped_text);
6494 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6495 cx.assert_editor_state(wrapped_text);
6496 }
6497}
6498
6499#[gpui::test]
6500async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6501 init_test(cx, |settings| {
6502 settings.languages.0.extend([(
6503 "Rust".into(),
6504 LanguageSettingsContent {
6505 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6506 preferred_line_length: Some(40),
6507 ..Default::default()
6508 },
6509 )])
6510 });
6511
6512 let mut cx = EditorTestContext::new(cx).await;
6513
6514 let rust_lang = Arc::new(
6515 Language::new(
6516 LanguageConfig {
6517 name: "Rust".into(),
6518 line_comments: vec!["// ".into()],
6519 block_comment: Some(BlockCommentConfig {
6520 start: "/*".into(),
6521 end: "*/".into(),
6522 prefix: "* ".into(),
6523 tab_size: 1,
6524 }),
6525 documentation_comment: Some(BlockCommentConfig {
6526 start: "/**".into(),
6527 end: "*/".into(),
6528 prefix: "* ".into(),
6529 tab_size: 1,
6530 }),
6531
6532 ..LanguageConfig::default()
6533 },
6534 Some(tree_sitter_rust::LANGUAGE.into()),
6535 )
6536 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6537 .unwrap(),
6538 );
6539
6540 // regular block comment
6541 assert_rewrap(
6542 indoc! {"
6543 /*
6544 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6545 */
6546 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6547 "},
6548 indoc! {"
6549 /*
6550 *ˇ Lorem ipsum dolor sit amet,
6551 * consectetur adipiscing elit.
6552 */
6553 /*
6554 *ˇ Lorem ipsum dolor sit amet,
6555 * consectetur adipiscing elit.
6556 */
6557 "},
6558 rust_lang.clone(),
6559 &mut cx,
6560 );
6561
6562 // indent is respected
6563 assert_rewrap(
6564 indoc! {"
6565 {}
6566 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6567 "},
6568 indoc! {"
6569 {}
6570 /*
6571 *ˇ Lorem ipsum dolor sit amet,
6572 * consectetur adipiscing elit.
6573 */
6574 "},
6575 rust_lang.clone(),
6576 &mut cx,
6577 );
6578
6579 // short block comments with inline delimiters
6580 assert_rewrap(
6581 indoc! {"
6582 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6583 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6584 */
6585 /*
6586 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6587 "},
6588 indoc! {"
6589 /*
6590 *ˇ Lorem ipsum dolor sit amet,
6591 * consectetur adipiscing elit.
6592 */
6593 /*
6594 *ˇ Lorem ipsum dolor sit amet,
6595 * consectetur adipiscing elit.
6596 */
6597 /*
6598 *ˇ Lorem ipsum dolor sit amet,
6599 * consectetur adipiscing elit.
6600 */
6601 "},
6602 rust_lang.clone(),
6603 &mut cx,
6604 );
6605
6606 // multiline block comment with inline start/end delimiters
6607 assert_rewrap(
6608 indoc! {"
6609 /*ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit. */
6611 "},
6612 indoc! {"
6613 /*
6614 *ˇ Lorem ipsum dolor sit amet,
6615 * consectetur adipiscing elit.
6616 */
6617 "},
6618 rust_lang.clone(),
6619 &mut cx,
6620 );
6621
6622 // block comment rewrap still respects paragraph bounds
6623 assert_rewrap(
6624 indoc! {"
6625 /*
6626 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6627 *
6628 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6629 */
6630 "},
6631 indoc! {"
6632 /*
6633 *ˇ Lorem ipsum dolor sit amet,
6634 * consectetur adipiscing elit.
6635 *
6636 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6637 */
6638 "},
6639 rust_lang.clone(),
6640 &mut cx,
6641 );
6642
6643 // documentation comments
6644 assert_rewrap(
6645 indoc! {"
6646 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6647 /**
6648 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6649 */
6650 "},
6651 indoc! {"
6652 /**
6653 *ˇ Lorem ipsum dolor sit amet,
6654 * consectetur adipiscing elit.
6655 */
6656 /**
6657 *ˇ Lorem ipsum dolor sit amet,
6658 * consectetur adipiscing elit.
6659 */
6660 "},
6661 rust_lang.clone(),
6662 &mut cx,
6663 );
6664
6665 // different, adjacent comments
6666 assert_rewrap(
6667 indoc! {"
6668 /**
6669 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6670 */
6671 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6672 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6673 "},
6674 indoc! {"
6675 /**
6676 *ˇ Lorem ipsum dolor sit amet,
6677 * consectetur adipiscing elit.
6678 */
6679 /*
6680 *ˇ Lorem ipsum dolor sit amet,
6681 * consectetur adipiscing elit.
6682 */
6683 //ˇ Lorem ipsum dolor sit amet,
6684 // consectetur adipiscing elit.
6685 "},
6686 rust_lang.clone(),
6687 &mut cx,
6688 );
6689
6690 // selection w/ single short block comment
6691 assert_rewrap(
6692 indoc! {"
6693 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6694 "},
6695 indoc! {"
6696 «/*
6697 * Lorem ipsum dolor sit amet,
6698 * consectetur adipiscing elit.
6699 */ˇ»
6700 "},
6701 rust_lang.clone(),
6702 &mut cx,
6703 );
6704
6705 // rewrapping a single comment w/ abutting comments
6706 assert_rewrap(
6707 indoc! {"
6708 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6709 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6710 "},
6711 indoc! {"
6712 /*
6713 * ˇLorem ipsum dolor sit amet,
6714 * consectetur adipiscing elit.
6715 */
6716 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6717 "},
6718 rust_lang.clone(),
6719 &mut cx,
6720 );
6721
6722 // selection w/ non-abutting short block comments
6723 assert_rewrap(
6724 indoc! {"
6725 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6726
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6728 "},
6729 indoc! {"
6730 «/*
6731 * Lorem ipsum dolor sit amet,
6732 * consectetur adipiscing elit.
6733 */
6734
6735 /*
6736 * Lorem ipsum dolor sit amet,
6737 * consectetur adipiscing elit.
6738 */ˇ»
6739 "},
6740 rust_lang.clone(),
6741 &mut cx,
6742 );
6743
6744 // selection of multiline block comments
6745 assert_rewrap(
6746 indoc! {"
6747 «/* Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit. */ˇ»
6749 "},
6750 indoc! {"
6751 «/*
6752 * Lorem ipsum dolor sit amet,
6753 * consectetur adipiscing elit.
6754 */ˇ»
6755 "},
6756 rust_lang.clone(),
6757 &mut cx,
6758 );
6759
6760 // partial selection of multiline block comments
6761 assert_rewrap(
6762 indoc! {"
6763 «/* Lorem ipsum dolor sit amet,ˇ»
6764 * consectetur adipiscing elit. */
6765 /* Lorem ipsum dolor sit amet,
6766 «* consectetur adipiscing elit. */ˇ»
6767 "},
6768 indoc! {"
6769 «/*
6770 * Lorem ipsum dolor sit amet,ˇ»
6771 * consectetur adipiscing elit. */
6772 /* Lorem ipsum dolor sit amet,
6773 «* consectetur adipiscing elit.
6774 */ˇ»
6775 "},
6776 rust_lang.clone(),
6777 &mut cx,
6778 );
6779
6780 // selection w/ abutting short block comments
6781 // TODO: should not be combined; should rewrap as 2 comments
6782 assert_rewrap(
6783 indoc! {"
6784 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6785 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6786 "},
6787 // desired behavior:
6788 // indoc! {"
6789 // «/*
6790 // * Lorem ipsum dolor sit amet,
6791 // * consectetur adipiscing elit.
6792 // */
6793 // /*
6794 // * Lorem ipsum dolor sit amet,
6795 // * consectetur adipiscing elit.
6796 // */ˇ»
6797 // "},
6798 // actual behaviour:
6799 indoc! {"
6800 «/*
6801 * Lorem ipsum dolor sit amet,
6802 * consectetur adipiscing elit. Lorem
6803 * ipsum dolor sit amet, consectetur
6804 * adipiscing elit.
6805 */ˇ»
6806 "},
6807 rust_lang.clone(),
6808 &mut cx,
6809 );
6810
6811 // TODO: same as above, but with delimiters on separate line
6812 // assert_rewrap(
6813 // indoc! {"
6814 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6815 // */
6816 // /*
6817 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6818 // "},
6819 // // desired:
6820 // // indoc! {"
6821 // // «/*
6822 // // * Lorem ipsum dolor sit amet,
6823 // // * consectetur adipiscing elit.
6824 // // */
6825 // // /*
6826 // // * Lorem ipsum dolor sit amet,
6827 // // * consectetur adipiscing elit.
6828 // // */ˇ»
6829 // // "},
6830 // // actual: (but with trailing w/s on the empty lines)
6831 // indoc! {"
6832 // «/*
6833 // * Lorem ipsum dolor sit amet,
6834 // * consectetur adipiscing elit.
6835 // *
6836 // */
6837 // /*
6838 // *
6839 // * Lorem ipsum dolor sit amet,
6840 // * consectetur adipiscing elit.
6841 // */ˇ»
6842 // "},
6843 // rust_lang.clone(),
6844 // &mut cx,
6845 // );
6846
6847 // TODO these are unhandled edge cases; not correct, just documenting known issues
6848 assert_rewrap(
6849 indoc! {"
6850 /*
6851 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6852 */
6853 /*
6854 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6855 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6856 "},
6857 // desired:
6858 // indoc! {"
6859 // /*
6860 // *ˇ Lorem ipsum dolor sit amet,
6861 // * consectetur adipiscing elit.
6862 // */
6863 // /*
6864 // *ˇ Lorem ipsum dolor sit amet,
6865 // * consectetur adipiscing elit.
6866 // */
6867 // /*
6868 // *ˇ Lorem ipsum dolor sit amet
6869 // */ /* consectetur adipiscing elit. */
6870 // "},
6871 // actual:
6872 indoc! {"
6873 /*
6874 //ˇ Lorem ipsum dolor sit amet,
6875 // consectetur adipiscing elit.
6876 */
6877 /*
6878 * //ˇ Lorem ipsum dolor sit amet,
6879 * consectetur adipiscing elit.
6880 */
6881 /*
6882 *ˇ Lorem ipsum dolor sit amet */ /*
6883 * consectetur adipiscing elit.
6884 */
6885 "},
6886 rust_lang,
6887 &mut cx,
6888 );
6889
6890 #[track_caller]
6891 fn assert_rewrap(
6892 unwrapped_text: &str,
6893 wrapped_text: &str,
6894 language: Arc<Language>,
6895 cx: &mut EditorTestContext,
6896 ) {
6897 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6898 cx.set_state(unwrapped_text);
6899 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6900 cx.assert_editor_state(wrapped_text);
6901 }
6902}
6903
6904#[gpui::test]
6905async fn test_hard_wrap(cx: &mut TestAppContext) {
6906 init_test(cx, |_| {});
6907 let mut cx = EditorTestContext::new(cx).await;
6908
6909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6910 cx.update_editor(|editor, _, cx| {
6911 editor.set_hard_wrap(Some(14), cx);
6912 });
6913
6914 cx.set_state(indoc!(
6915 "
6916 one two three ˇ
6917 "
6918 ));
6919 cx.simulate_input("four");
6920 cx.run_until_parked();
6921
6922 cx.assert_editor_state(indoc!(
6923 "
6924 one two three
6925 fourˇ
6926 "
6927 ));
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.newline(&Default::default(), window, cx);
6931 });
6932 cx.run_until_parked();
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 four
6937 ˇ
6938 "
6939 ));
6940
6941 cx.simulate_input("five");
6942 cx.run_until_parked();
6943 cx.assert_editor_state(indoc!(
6944 "
6945 one two three
6946 four
6947 fiveˇ
6948 "
6949 ));
6950
6951 cx.update_editor(|editor, window, cx| {
6952 editor.newline(&Default::default(), window, cx);
6953 });
6954 cx.run_until_parked();
6955 cx.simulate_input("# ");
6956 cx.run_until_parked();
6957 cx.assert_editor_state(indoc!(
6958 "
6959 one two three
6960 four
6961 five
6962 # ˇ
6963 "
6964 ));
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.newline(&Default::default(), window, cx);
6968 });
6969 cx.run_until_parked();
6970 cx.assert_editor_state(indoc!(
6971 "
6972 one two three
6973 four
6974 five
6975 #\x20
6976 #ˇ
6977 "
6978 ));
6979
6980 cx.simulate_input(" 6");
6981 cx.run_until_parked();
6982 cx.assert_editor_state(indoc!(
6983 "
6984 one two three
6985 four
6986 five
6987 #
6988 # 6ˇ
6989 "
6990 ));
6991}
6992
6993#[gpui::test]
6994async fn test_cut_line_ends(cx: &mut TestAppContext) {
6995 init_test(cx, |_| {});
6996
6997 let mut cx = EditorTestContext::new(cx).await;
6998
6999 cx.set_state(indoc! {"The quick brownˇ"});
7000 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7001 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7002
7003 cx.set_state(indoc! {"The emacs foxˇ"});
7004 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7005 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7006
7007 cx.set_state(indoc! {"
7008 The quick« brownˇ»
7009 fox jumps overˇ
7010 the lazy dog"});
7011 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7012 cx.assert_editor_state(indoc! {"
7013 The quickˇ
7014 ˇthe lazy dog"});
7015
7016 cx.set_state(indoc! {"
7017 The quick« brownˇ»
7018 fox jumps overˇ
7019 the lazy dog"});
7020 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7021 cx.assert_editor_state(indoc! {"
7022 The quickˇ
7023 fox jumps overˇthe lazy dog"});
7024
7025 cx.set_state(indoc! {"
7026 The quick« brownˇ»
7027 fox jumps overˇ
7028 the lazy dog"});
7029 cx.update_editor(|e, window, cx| {
7030 e.cut_to_end_of_line(
7031 &CutToEndOfLine {
7032 stop_at_newlines: true,
7033 },
7034 window,
7035 cx,
7036 )
7037 });
7038 cx.assert_editor_state(indoc! {"
7039 The quickˇ
7040 fox jumps overˇ
7041 the lazy dog"});
7042
7043 cx.set_state(indoc! {"
7044 The quick« brownˇ»
7045 fox jumps overˇ
7046 the lazy dog"});
7047 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7048 cx.assert_editor_state(indoc! {"
7049 The quickˇ
7050 fox jumps overˇthe lazy dog"});
7051}
7052
7053#[gpui::test]
7054async fn test_clipboard(cx: &mut TestAppContext) {
7055 init_test(cx, |_| {});
7056
7057 let mut cx = EditorTestContext::new(cx).await;
7058
7059 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7060 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7061 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7062
7063 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7064 cx.set_state("two ˇfour ˇsix ˇ");
7065 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7066 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7067
7068 // Paste again but with only two cursors. Since the number of cursors doesn't
7069 // match the number of slices in the clipboard, the entire clipboard text
7070 // is pasted at each cursor.
7071 cx.set_state("ˇtwo one✅ four three six five ˇ");
7072 cx.update_editor(|e, window, cx| {
7073 e.handle_input("( ", window, cx);
7074 e.paste(&Paste, window, cx);
7075 e.handle_input(") ", window, cx);
7076 });
7077 cx.assert_editor_state(
7078 &([
7079 "( one✅ ",
7080 "three ",
7081 "five ) ˇtwo one✅ four three six five ( one✅ ",
7082 "three ",
7083 "five ) ˇ",
7084 ]
7085 .join("\n")),
7086 );
7087
7088 // Cut with three selections, one of which is full-line.
7089 cx.set_state(indoc! {"
7090 1«2ˇ»3
7091 4ˇ567
7092 «8ˇ»9"});
7093 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7094 cx.assert_editor_state(indoc! {"
7095 1ˇ3
7096 ˇ9"});
7097
7098 // Paste with three selections, noticing how the copied selection that was full-line
7099 // gets inserted before the second cursor.
7100 cx.set_state(indoc! {"
7101 1ˇ3
7102 9ˇ
7103 «oˇ»ne"});
7104 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 12ˇ3
7107 4567
7108 9ˇ
7109 8ˇne"});
7110
7111 // Copy with a single cursor only, which writes the whole line into the clipboard.
7112 cx.set_state(indoc! {"
7113 The quick brown
7114 fox juˇmps over
7115 the lazy dog"});
7116 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7117 assert_eq!(
7118 cx.read_from_clipboard()
7119 .and_then(|item| item.text().as_deref().map(str::to_string)),
7120 Some("fox jumps over\n".to_string())
7121 );
7122
7123 // Paste with three selections, noticing how the copied full-line selection is inserted
7124 // before the empty selections but replaces the selection that is non-empty.
7125 cx.set_state(indoc! {"
7126 Tˇhe quick brown
7127 «foˇ»x jumps over
7128 tˇhe lazy dog"});
7129 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7130 cx.assert_editor_state(indoc! {"
7131 fox jumps over
7132 Tˇhe quick brown
7133 fox jumps over
7134 ˇx jumps over
7135 fox jumps over
7136 tˇhe lazy dog"});
7137}
7138
7139#[gpui::test]
7140async fn test_copy_trim(cx: &mut TestAppContext) {
7141 init_test(cx, |_| {});
7142
7143 let mut cx = EditorTestContext::new(cx).await;
7144 cx.set_state(
7145 r#" «for selection in selections.iter() {
7146 let mut start = selection.start;
7147 let mut end = selection.end;
7148 let is_entire_line = selection.is_empty();
7149 if is_entire_line {
7150 start = Point::new(start.row, 0);ˇ»
7151 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7152 }
7153 "#,
7154 );
7155 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7156 assert_eq!(
7157 cx.read_from_clipboard()
7158 .and_then(|item| item.text().as_deref().map(str::to_string)),
7159 Some(
7160 "for selection in selections.iter() {
7161 let mut start = selection.start;
7162 let mut end = selection.end;
7163 let is_entire_line = selection.is_empty();
7164 if is_entire_line {
7165 start = Point::new(start.row, 0);"
7166 .to_string()
7167 ),
7168 "Regular copying preserves all indentation selected",
7169 );
7170 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7171 assert_eq!(
7172 cx.read_from_clipboard()
7173 .and_then(|item| item.text().as_deref().map(str::to_string)),
7174 Some(
7175 "for selection in selections.iter() {
7176let mut start = selection.start;
7177let mut end = selection.end;
7178let is_entire_line = selection.is_empty();
7179if is_entire_line {
7180 start = Point::new(start.row, 0);"
7181 .to_string()
7182 ),
7183 "Copying with stripping should strip all leading whitespaces"
7184 );
7185
7186 cx.set_state(
7187 r#" « for selection in selections.iter() {
7188 let mut start = selection.start;
7189 let mut end = selection.end;
7190 let is_entire_line = selection.is_empty();
7191 if is_entire_line {
7192 start = Point::new(start.row, 0);ˇ»
7193 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7194 }
7195 "#,
7196 );
7197 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7198 assert_eq!(
7199 cx.read_from_clipboard()
7200 .and_then(|item| item.text().as_deref().map(str::to_string)),
7201 Some(
7202 " for selection in selections.iter() {
7203 let mut start = selection.start;
7204 let mut end = selection.end;
7205 let is_entire_line = selection.is_empty();
7206 if is_entire_line {
7207 start = Point::new(start.row, 0);"
7208 .to_string()
7209 ),
7210 "Regular copying preserves all indentation selected",
7211 );
7212 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7213 assert_eq!(
7214 cx.read_from_clipboard()
7215 .and_then(|item| item.text().as_deref().map(str::to_string)),
7216 Some(
7217 "for selection in selections.iter() {
7218let mut start = selection.start;
7219let mut end = selection.end;
7220let is_entire_line = selection.is_empty();
7221if is_entire_line {
7222 start = Point::new(start.row, 0);"
7223 .to_string()
7224 ),
7225 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7226 );
7227
7228 cx.set_state(
7229 r#" «ˇ for selection in selections.iter() {
7230 let mut start = selection.start;
7231 let mut end = selection.end;
7232 let is_entire_line = selection.is_empty();
7233 if is_entire_line {
7234 start = Point::new(start.row, 0);»
7235 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7236 }
7237 "#,
7238 );
7239 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7240 assert_eq!(
7241 cx.read_from_clipboard()
7242 .and_then(|item| item.text().as_deref().map(str::to_string)),
7243 Some(
7244 " for selection in selections.iter() {
7245 let mut start = selection.start;
7246 let mut end = selection.end;
7247 let is_entire_line = selection.is_empty();
7248 if is_entire_line {
7249 start = Point::new(start.row, 0);"
7250 .to_string()
7251 ),
7252 "Regular copying for reverse selection works the same",
7253 );
7254 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7255 assert_eq!(
7256 cx.read_from_clipboard()
7257 .and_then(|item| item.text().as_deref().map(str::to_string)),
7258 Some(
7259 "for selection in selections.iter() {
7260let mut start = selection.start;
7261let mut end = selection.end;
7262let is_entire_line = selection.is_empty();
7263if is_entire_line {
7264 start = Point::new(start.row, 0);"
7265 .to_string()
7266 ),
7267 "Copying with stripping for reverse selection works the same"
7268 );
7269
7270 cx.set_state(
7271 r#" for selection «in selections.iter() {
7272 let mut start = selection.start;
7273 let mut end = selection.end;
7274 let is_entire_line = selection.is_empty();
7275 if is_entire_line {
7276 start = Point::new(start.row, 0);ˇ»
7277 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7278 }
7279 "#,
7280 );
7281 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7282 assert_eq!(
7283 cx.read_from_clipboard()
7284 .and_then(|item| item.text().as_deref().map(str::to_string)),
7285 Some(
7286 "in selections.iter() {
7287 let mut start = selection.start;
7288 let mut end = selection.end;
7289 let is_entire_line = selection.is_empty();
7290 if is_entire_line {
7291 start = Point::new(start.row, 0);"
7292 .to_string()
7293 ),
7294 "When selecting past the indent, the copying works as usual",
7295 );
7296 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7297 assert_eq!(
7298 cx.read_from_clipboard()
7299 .and_then(|item| item.text().as_deref().map(str::to_string)),
7300 Some(
7301 "in selections.iter() {
7302 let mut start = selection.start;
7303 let mut end = selection.end;
7304 let is_entire_line = selection.is_empty();
7305 if is_entire_line {
7306 start = Point::new(start.row, 0);"
7307 .to_string()
7308 ),
7309 "When selecting past the indent, nothing is trimmed"
7310 );
7311
7312 cx.set_state(
7313 r#" «for selection in selections.iter() {
7314 let mut start = selection.start;
7315
7316 let mut end = selection.end;
7317 let is_entire_line = selection.is_empty();
7318 if is_entire_line {
7319 start = Point::new(start.row, 0);
7320ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7321 }
7322 "#,
7323 );
7324 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7325 assert_eq!(
7326 cx.read_from_clipboard()
7327 .and_then(|item| item.text().as_deref().map(str::to_string)),
7328 Some(
7329 "for selection in selections.iter() {
7330let mut start = selection.start;
7331
7332let mut end = selection.end;
7333let is_entire_line = selection.is_empty();
7334if is_entire_line {
7335 start = Point::new(start.row, 0);
7336"
7337 .to_string()
7338 ),
7339 "Copying with stripping should ignore empty lines"
7340 );
7341}
7342
7343#[gpui::test]
7344async fn test_paste_multiline(cx: &mut TestAppContext) {
7345 init_test(cx, |_| {});
7346
7347 let mut cx = EditorTestContext::new(cx).await;
7348 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7349
7350 // Cut an indented block, without the leading whitespace.
7351 cx.set_state(indoc! {"
7352 const a: B = (
7353 c(),
7354 «d(
7355 e,
7356 f
7357 )ˇ»
7358 );
7359 "});
7360 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7361 cx.assert_editor_state(indoc! {"
7362 const a: B = (
7363 c(),
7364 ˇ
7365 );
7366 "});
7367
7368 // Paste it at the same position.
7369 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7370 cx.assert_editor_state(indoc! {"
7371 const a: B = (
7372 c(),
7373 d(
7374 e,
7375 f
7376 )ˇ
7377 );
7378 "});
7379
7380 // Paste it at a line with a lower indent level.
7381 cx.set_state(indoc! {"
7382 ˇ
7383 const a: B = (
7384 c(),
7385 );
7386 "});
7387 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7388 cx.assert_editor_state(indoc! {"
7389 d(
7390 e,
7391 f
7392 )ˇ
7393 const a: B = (
7394 c(),
7395 );
7396 "});
7397
7398 // Cut an indented block, with the leading whitespace.
7399 cx.set_state(indoc! {"
7400 const a: B = (
7401 c(),
7402 « d(
7403 e,
7404 f
7405 )
7406 ˇ»);
7407 "});
7408 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7409 cx.assert_editor_state(indoc! {"
7410 const a: B = (
7411 c(),
7412 ˇ);
7413 "});
7414
7415 // Paste it at the same position.
7416 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7417 cx.assert_editor_state(indoc! {"
7418 const a: B = (
7419 c(),
7420 d(
7421 e,
7422 f
7423 )
7424 ˇ);
7425 "});
7426
7427 // Paste it at a line with a higher indent level.
7428 cx.set_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 fˇ
7434 )
7435 );
7436 "});
7437 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7438 cx.assert_editor_state(indoc! {"
7439 const a: B = (
7440 c(),
7441 d(
7442 e,
7443 f d(
7444 e,
7445 f
7446 )
7447 ˇ
7448 )
7449 );
7450 "});
7451
7452 // Copy an indented block, starting mid-line
7453 cx.set_state(indoc! {"
7454 const a: B = (
7455 c(),
7456 somethin«g(
7457 e,
7458 f
7459 )ˇ»
7460 );
7461 "});
7462 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7463
7464 // Paste it on a line with a lower indent level
7465 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7466 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7467 cx.assert_editor_state(indoc! {"
7468 const a: B = (
7469 c(),
7470 something(
7471 e,
7472 f
7473 )
7474 );
7475 g(
7476 e,
7477 f
7478 )ˇ"});
7479}
7480
7481#[gpui::test]
7482async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7483 init_test(cx, |_| {});
7484
7485 cx.write_to_clipboard(ClipboardItem::new_string(
7486 " d(\n e\n );\n".into(),
7487 ));
7488
7489 let mut cx = EditorTestContext::new(cx).await;
7490 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7491
7492 cx.set_state(indoc! {"
7493 fn a() {
7494 b();
7495 if c() {
7496 ˇ
7497 }
7498 }
7499 "});
7500
7501 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7502 cx.assert_editor_state(indoc! {"
7503 fn a() {
7504 b();
7505 if c() {
7506 d(
7507 e
7508 );
7509 ˇ
7510 }
7511 }
7512 "});
7513
7514 cx.set_state(indoc! {"
7515 fn a() {
7516 b();
7517 ˇ
7518 }
7519 "});
7520
7521 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7522 cx.assert_editor_state(indoc! {"
7523 fn a() {
7524 b();
7525 d(
7526 e
7527 );
7528 ˇ
7529 }
7530 "});
7531}
7532
7533#[gpui::test]
7534fn test_select_all(cx: &mut TestAppContext) {
7535 init_test(cx, |_| {});
7536
7537 let editor = cx.add_window(|window, cx| {
7538 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7539 build_editor(buffer, window, cx)
7540 });
7541 _ = editor.update(cx, |editor, window, cx| {
7542 editor.select_all(&SelectAll, window, cx);
7543 assert_eq!(
7544 editor.selections.display_ranges(cx),
7545 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7546 );
7547 });
7548}
7549
7550#[gpui::test]
7551fn test_select_line(cx: &mut TestAppContext) {
7552 init_test(cx, |_| {});
7553
7554 let editor = cx.add_window(|window, cx| {
7555 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7556 build_editor(buffer, window, cx)
7557 });
7558 _ = editor.update(cx, |editor, window, cx| {
7559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7560 s.select_display_ranges([
7561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7564 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7565 ])
7566 });
7567 editor.select_line(&SelectLine, window, cx);
7568 assert_eq!(
7569 editor.selections.display_ranges(cx),
7570 vec![
7571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7572 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7573 ]
7574 );
7575 });
7576
7577 _ = editor.update(cx, |editor, window, cx| {
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 editor.selections.display_ranges(cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 editor.selections.display_ranges(cx),
7592 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7593 );
7594 });
7595}
7596
7597#[gpui::test]
7598async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7599 init_test(cx, |_| {});
7600 let mut cx = EditorTestContext::new(cx).await;
7601
7602 #[track_caller]
7603 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7604 cx.set_state(initial_state);
7605 cx.update_editor(|e, window, cx| {
7606 e.split_selection_into_lines(&Default::default(), window, cx)
7607 });
7608 cx.assert_editor_state(expected_state);
7609 }
7610
7611 // Selection starts and ends at the middle of lines, left-to-right
7612 test(
7613 &mut cx,
7614 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7615 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7616 );
7617 // Same thing, right-to-left
7618 test(
7619 &mut cx,
7620 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7621 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7622 );
7623
7624 // Whole buffer, left-to-right, last line *doesn't* end with newline
7625 test(
7626 &mut cx,
7627 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7628 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7629 );
7630 // Same thing, right-to-left
7631 test(
7632 &mut cx,
7633 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7634 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7635 );
7636
7637 // Whole buffer, left-to-right, last line ends with newline
7638 test(
7639 &mut cx,
7640 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7641 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7642 );
7643 // Same thing, right-to-left
7644 test(
7645 &mut cx,
7646 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7647 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7648 );
7649
7650 // Starts at the end of a line, ends at the start of another
7651 test(
7652 &mut cx,
7653 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7654 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7655 );
7656}
7657
7658#[gpui::test]
7659async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7660 init_test(cx, |_| {});
7661
7662 let editor = cx.add_window(|window, cx| {
7663 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7664 build_editor(buffer, window, cx)
7665 });
7666
7667 // setup
7668 _ = editor.update(cx, |editor, window, cx| {
7669 editor.fold_creases(
7670 vec![
7671 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7672 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7673 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7674 ],
7675 true,
7676 window,
7677 cx,
7678 );
7679 assert_eq!(
7680 editor.display_text(cx),
7681 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7682 );
7683 });
7684
7685 _ = editor.update(cx, |editor, window, cx| {
7686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7687 s.select_display_ranges([
7688 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7689 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7691 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7692 ])
7693 });
7694 editor.split_selection_into_lines(&Default::default(), window, cx);
7695 assert_eq!(
7696 editor.display_text(cx),
7697 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7698 );
7699 });
7700 EditorTestContext::for_editor(editor, cx)
7701 .await
7702 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7703
7704 _ = editor.update(cx, |editor, window, cx| {
7705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7706 s.select_display_ranges([
7707 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7708 ])
7709 });
7710 editor.split_selection_into_lines(&Default::default(), window, cx);
7711 assert_eq!(
7712 editor.display_text(cx),
7713 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7714 );
7715 assert_eq!(
7716 editor.selections.display_ranges(cx),
7717 [
7718 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7719 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7720 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7721 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7722 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7723 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7724 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7725 ]
7726 );
7727 });
7728 EditorTestContext::for_editor(editor, cx)
7729 .await
7730 .assert_editor_state(
7731 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7732 );
7733}
7734
7735#[gpui::test]
7736async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7737 init_test(cx, |_| {});
7738
7739 let mut cx = EditorTestContext::new(cx).await;
7740
7741 cx.set_state(indoc!(
7742 r#"abc
7743 defˇghi
7744
7745 jk
7746 nlmo
7747 "#
7748 ));
7749
7750 cx.update_editor(|editor, window, cx| {
7751 editor.add_selection_above(&Default::default(), window, cx);
7752 });
7753
7754 cx.assert_editor_state(indoc!(
7755 r#"abcˇ
7756 defˇghi
7757
7758 jk
7759 nlmo
7760 "#
7761 ));
7762
7763 cx.update_editor(|editor, window, cx| {
7764 editor.add_selection_above(&Default::default(), window, cx);
7765 });
7766
7767 cx.assert_editor_state(indoc!(
7768 r#"abcˇ
7769 defˇghi
7770
7771 jk
7772 nlmo
7773 "#
7774 ));
7775
7776 cx.update_editor(|editor, window, cx| {
7777 editor.add_selection_below(&Default::default(), window, cx);
7778 });
7779
7780 cx.assert_editor_state(indoc!(
7781 r#"abc
7782 defˇghi
7783
7784 jk
7785 nlmo
7786 "#
7787 ));
7788
7789 cx.update_editor(|editor, window, cx| {
7790 editor.undo_selection(&Default::default(), window, cx);
7791 });
7792
7793 cx.assert_editor_state(indoc!(
7794 r#"abcˇ
7795 defˇghi
7796
7797 jk
7798 nlmo
7799 "#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.redo_selection(&Default::default(), window, cx);
7804 });
7805
7806 cx.assert_editor_state(indoc!(
7807 r#"abc
7808 defˇghi
7809
7810 jk
7811 nlmo
7812 "#
7813 ));
7814
7815 cx.update_editor(|editor, window, cx| {
7816 editor.add_selection_below(&Default::default(), window, cx);
7817 });
7818
7819 cx.assert_editor_state(indoc!(
7820 r#"abc
7821 defˇghi
7822 ˇ
7823 jk
7824 nlmo
7825 "#
7826 ));
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.add_selection_below(&Default::default(), window, cx);
7830 });
7831
7832 cx.assert_editor_state(indoc!(
7833 r#"abc
7834 defˇghi
7835 ˇ
7836 jkˇ
7837 nlmo
7838 "#
7839 ));
7840
7841 cx.update_editor(|editor, window, cx| {
7842 editor.add_selection_below(&Default::default(), window, cx);
7843 });
7844
7845 cx.assert_editor_state(indoc!(
7846 r#"abc
7847 defˇghi
7848 ˇ
7849 jkˇ
7850 nlmˇo
7851 "#
7852 ));
7853
7854 cx.update_editor(|editor, window, cx| {
7855 editor.add_selection_below(&Default::default(), window, cx);
7856 });
7857
7858 cx.assert_editor_state(indoc!(
7859 r#"abc
7860 defˇghi
7861 ˇ
7862 jkˇ
7863 nlmˇo
7864 ˇ"#
7865 ));
7866
7867 // change selections
7868 cx.set_state(indoc!(
7869 r#"abc
7870 def«ˇg»hi
7871
7872 jk
7873 nlmo
7874 "#
7875 ));
7876
7877 cx.update_editor(|editor, window, cx| {
7878 editor.add_selection_below(&Default::default(), window, cx);
7879 });
7880
7881 cx.assert_editor_state(indoc!(
7882 r#"abc
7883 def«ˇg»hi
7884
7885 jk
7886 nlm«ˇo»
7887 "#
7888 ));
7889
7890 cx.update_editor(|editor, window, cx| {
7891 editor.add_selection_below(&Default::default(), window, cx);
7892 });
7893
7894 cx.assert_editor_state(indoc!(
7895 r#"abc
7896 def«ˇg»hi
7897
7898 jk
7899 nlm«ˇo»
7900 "#
7901 ));
7902
7903 cx.update_editor(|editor, window, cx| {
7904 editor.add_selection_above(&Default::default(), window, cx);
7905 });
7906
7907 cx.assert_editor_state(indoc!(
7908 r#"abc
7909 def«ˇg»hi
7910
7911 jk
7912 nlmo
7913 "#
7914 ));
7915
7916 cx.update_editor(|editor, window, cx| {
7917 editor.add_selection_above(&Default::default(), window, cx);
7918 });
7919
7920 cx.assert_editor_state(indoc!(
7921 r#"abc
7922 def«ˇg»hi
7923
7924 jk
7925 nlmo
7926 "#
7927 ));
7928
7929 // Change selections again
7930 cx.set_state(indoc!(
7931 r#"a«bc
7932 defgˇ»hi
7933
7934 jk
7935 nlmo
7936 "#
7937 ));
7938
7939 cx.update_editor(|editor, window, cx| {
7940 editor.add_selection_below(&Default::default(), window, cx);
7941 });
7942
7943 cx.assert_editor_state(indoc!(
7944 r#"a«bcˇ»
7945 d«efgˇ»hi
7946
7947 j«kˇ»
7948 nlmo
7949 "#
7950 ));
7951
7952 cx.update_editor(|editor, window, cx| {
7953 editor.add_selection_below(&Default::default(), window, cx);
7954 });
7955 cx.assert_editor_state(indoc!(
7956 r#"a«bcˇ»
7957 d«efgˇ»hi
7958
7959 j«kˇ»
7960 n«lmoˇ»
7961 "#
7962 ));
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_above(&Default::default(), window, cx);
7965 });
7966
7967 cx.assert_editor_state(indoc!(
7968 r#"a«bcˇ»
7969 d«efgˇ»hi
7970
7971 j«kˇ»
7972 nlmo
7973 "#
7974 ));
7975
7976 // Change selections again
7977 cx.set_state(indoc!(
7978 r#"abc
7979 d«ˇefghi
7980
7981 jk
7982 nlm»o
7983 "#
7984 ));
7985
7986 cx.update_editor(|editor, window, cx| {
7987 editor.add_selection_above(&Default::default(), window, cx);
7988 });
7989
7990 cx.assert_editor_state(indoc!(
7991 r#"a«ˇbc»
7992 d«ˇef»ghi
7993
7994 j«ˇk»
7995 n«ˇlm»o
7996 "#
7997 ));
7998
7999 cx.update_editor(|editor, window, cx| {
8000 editor.add_selection_below(&Default::default(), window, cx);
8001 });
8002
8003 cx.assert_editor_state(indoc!(
8004 r#"abc
8005 d«ˇef»ghi
8006
8007 j«ˇk»
8008 n«ˇlm»o
8009 "#
8010 ));
8011}
8012
8013#[gpui::test]
8014async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8015 init_test(cx, |_| {});
8016 let mut cx = EditorTestContext::new(cx).await;
8017
8018 cx.set_state(indoc!(
8019 r#"line onˇe
8020 liˇne two
8021 line three
8022 line four"#
8023 ));
8024
8025 cx.update_editor(|editor, window, cx| {
8026 editor.add_selection_below(&Default::default(), window, cx);
8027 });
8028
8029 // test multiple cursors expand in the same direction
8030 cx.assert_editor_state(indoc!(
8031 r#"line onˇe
8032 liˇne twˇo
8033 liˇne three
8034 line four"#
8035 ));
8036
8037 cx.update_editor(|editor, window, cx| {
8038 editor.add_selection_below(&Default::default(), window, cx);
8039 });
8040
8041 cx.update_editor(|editor, window, cx| {
8042 editor.add_selection_below(&Default::default(), window, cx);
8043 });
8044
8045 // test multiple cursors expand below overflow
8046 cx.assert_editor_state(indoc!(
8047 r#"line onˇe
8048 liˇne twˇo
8049 liˇne thˇree
8050 liˇne foˇur"#
8051 ));
8052
8053 cx.update_editor(|editor, window, cx| {
8054 editor.add_selection_above(&Default::default(), window, cx);
8055 });
8056
8057 // test multiple cursors retrieves back correctly
8058 cx.assert_editor_state(indoc!(
8059 r#"line onˇe
8060 liˇne twˇo
8061 liˇne thˇree
8062 line four"#
8063 ));
8064
8065 cx.update_editor(|editor, window, cx| {
8066 editor.add_selection_above(&Default::default(), window, cx);
8067 });
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.add_selection_above(&Default::default(), window, cx);
8071 });
8072
8073 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8074 cx.assert_editor_state(indoc!(
8075 r#"liˇne onˇe
8076 liˇne two
8077 line three
8078 line four"#
8079 ));
8080
8081 cx.update_editor(|editor, window, cx| {
8082 editor.undo_selection(&Default::default(), window, cx);
8083 });
8084
8085 // test undo
8086 cx.assert_editor_state(indoc!(
8087 r#"line onˇe
8088 liˇne twˇo
8089 line three
8090 line four"#
8091 ));
8092
8093 cx.update_editor(|editor, window, cx| {
8094 editor.redo_selection(&Default::default(), window, cx);
8095 });
8096
8097 // test redo
8098 cx.assert_editor_state(indoc!(
8099 r#"liˇne onˇe
8100 liˇne two
8101 line three
8102 line four"#
8103 ));
8104
8105 cx.set_state(indoc!(
8106 r#"abcd
8107 ef«ghˇ»
8108 ijkl
8109 «mˇ»nop"#
8110 ));
8111
8112 cx.update_editor(|editor, window, cx| {
8113 editor.add_selection_above(&Default::default(), window, cx);
8114 });
8115
8116 // test multiple selections expand in the same direction
8117 cx.assert_editor_state(indoc!(
8118 r#"ab«cdˇ»
8119 ef«ghˇ»
8120 «iˇ»jkl
8121 «mˇ»nop"#
8122 ));
8123
8124 cx.update_editor(|editor, window, cx| {
8125 editor.add_selection_above(&Default::default(), window, cx);
8126 });
8127
8128 // test multiple selection upward overflow
8129 cx.assert_editor_state(indoc!(
8130 r#"ab«cdˇ»
8131 «eˇ»f«ghˇ»
8132 «iˇ»jkl
8133 «mˇ»nop"#
8134 ));
8135
8136 cx.update_editor(|editor, window, cx| {
8137 editor.add_selection_below(&Default::default(), window, cx);
8138 });
8139
8140 // test multiple selection retrieves back correctly
8141 cx.assert_editor_state(indoc!(
8142 r#"abcd
8143 ef«ghˇ»
8144 «iˇ»jkl
8145 «mˇ»nop"#
8146 ));
8147
8148 cx.update_editor(|editor, window, cx| {
8149 editor.add_selection_below(&Default::default(), window, cx);
8150 });
8151
8152 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8153 cx.assert_editor_state(indoc!(
8154 r#"abcd
8155 ef«ghˇ»
8156 ij«klˇ»
8157 «mˇ»nop"#
8158 ));
8159
8160 cx.update_editor(|editor, window, cx| {
8161 editor.undo_selection(&Default::default(), window, cx);
8162 });
8163
8164 // test undo
8165 cx.assert_editor_state(indoc!(
8166 r#"abcd
8167 ef«ghˇ»
8168 «iˇ»jkl
8169 «mˇ»nop"#
8170 ));
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.redo_selection(&Default::default(), window, cx);
8174 });
8175
8176 // test redo
8177 cx.assert_editor_state(indoc!(
8178 r#"abcd
8179 ef«ghˇ»
8180 ij«klˇ»
8181 «mˇ»nop"#
8182 ));
8183}
8184
8185#[gpui::test]
8186async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8187 init_test(cx, |_| {});
8188 let mut cx = EditorTestContext::new(cx).await;
8189
8190 cx.set_state(indoc!(
8191 r#"line onˇe
8192 liˇne two
8193 line three
8194 line four"#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 editor.add_selection_below(&Default::default(), window, cx);
8200 editor.add_selection_below(&Default::default(), window, cx);
8201 });
8202
8203 // initial state with two multi cursor groups
8204 cx.assert_editor_state(indoc!(
8205 r#"line onˇe
8206 liˇne twˇo
8207 liˇne thˇree
8208 liˇne foˇur"#
8209 ));
8210
8211 // add single cursor in middle - simulate opt click
8212 cx.update_editor(|editor, window, cx| {
8213 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8214 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8215 editor.end_selection(window, cx);
8216 });
8217
8218 cx.assert_editor_state(indoc!(
8219 r#"line onˇe
8220 liˇne twˇo
8221 liˇneˇ thˇree
8222 liˇne foˇur"#
8223 ));
8224
8225 cx.update_editor(|editor, window, cx| {
8226 editor.add_selection_above(&Default::default(), window, cx);
8227 });
8228
8229 // test new added selection expands above and existing selection shrinks
8230 cx.assert_editor_state(indoc!(
8231 r#"line onˇe
8232 liˇneˇ twˇo
8233 liˇneˇ thˇree
8234 line four"#
8235 ));
8236
8237 cx.update_editor(|editor, window, cx| {
8238 editor.add_selection_above(&Default::default(), window, cx);
8239 });
8240
8241 // test new added selection expands above and existing selection shrinks
8242 cx.assert_editor_state(indoc!(
8243 r#"lineˇ onˇe
8244 liˇneˇ twˇo
8245 lineˇ three
8246 line four"#
8247 ));
8248
8249 // intial state with two selection groups
8250 cx.set_state(indoc!(
8251 r#"abcd
8252 ef«ghˇ»
8253 ijkl
8254 «mˇ»nop"#
8255 ));
8256
8257 cx.update_editor(|editor, window, cx| {
8258 editor.add_selection_above(&Default::default(), window, cx);
8259 editor.add_selection_above(&Default::default(), window, cx);
8260 });
8261
8262 cx.assert_editor_state(indoc!(
8263 r#"ab«cdˇ»
8264 «eˇ»f«ghˇ»
8265 «iˇ»jkl
8266 «mˇ»nop"#
8267 ));
8268
8269 // add single selection in middle - simulate opt drag
8270 cx.update_editor(|editor, window, cx| {
8271 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8272 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8273 editor.update_selection(
8274 DisplayPoint::new(DisplayRow(2), 4),
8275 0,
8276 gpui::Point::<f32>::default(),
8277 window,
8278 cx,
8279 );
8280 editor.end_selection(window, cx);
8281 });
8282
8283 cx.assert_editor_state(indoc!(
8284 r#"ab«cdˇ»
8285 «eˇ»f«ghˇ»
8286 «iˇ»jk«lˇ»
8287 «mˇ»nop"#
8288 ));
8289
8290 cx.update_editor(|editor, window, cx| {
8291 editor.add_selection_below(&Default::default(), window, cx);
8292 });
8293
8294 // test new added selection expands below, others shrinks from above
8295 cx.assert_editor_state(indoc!(
8296 r#"abcd
8297 ef«ghˇ»
8298 «iˇ»jk«lˇ»
8299 «mˇ»no«pˇ»"#
8300 ));
8301}
8302
8303#[gpui::test]
8304async fn test_select_next(cx: &mut TestAppContext) {
8305 init_test(cx, |_| {});
8306
8307 let mut cx = EditorTestContext::new(cx).await;
8308 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8309
8310 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8311 .unwrap();
8312 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8313
8314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8315 .unwrap();
8316 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8317
8318 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8319 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8320
8321 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8322 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8323
8324 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8325 .unwrap();
8326 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8327
8328 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8329 .unwrap();
8330 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8331
8332 // Test selection direction should be preserved
8333 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8334
8335 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8336 .unwrap();
8337 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8338}
8339
8340#[gpui::test]
8341async fn test_select_all_matches(cx: &mut TestAppContext) {
8342 init_test(cx, |_| {});
8343
8344 let mut cx = EditorTestContext::new(cx).await;
8345
8346 // Test caret-only selections
8347 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8348 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8349 .unwrap();
8350 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8351
8352 // Test left-to-right selections
8353 cx.set_state("abc\n«abcˇ»\nabc");
8354 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8355 .unwrap();
8356 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8357
8358 // Test right-to-left selections
8359 cx.set_state("abc\n«ˇabc»\nabc");
8360 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8361 .unwrap();
8362 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8363
8364 // Test selecting whitespace with caret selection
8365 cx.set_state("abc\nˇ abc\nabc");
8366 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8367 .unwrap();
8368 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8369
8370 // Test selecting whitespace with left-to-right selection
8371 cx.set_state("abc\n«ˇ »abc\nabc");
8372 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8373 .unwrap();
8374 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8375
8376 // Test no matches with right-to-left selection
8377 cx.set_state("abc\n« ˇ»abc\nabc");
8378 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8379 .unwrap();
8380 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8381
8382 // Test with a single word and clip_at_line_ends=true (#29823)
8383 cx.set_state("aˇbc");
8384 cx.update_editor(|e, window, cx| {
8385 e.set_clip_at_line_ends(true, cx);
8386 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8387 e.set_clip_at_line_ends(false, cx);
8388 });
8389 cx.assert_editor_state("«abcˇ»");
8390}
8391
8392#[gpui::test]
8393async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let mut cx = EditorTestContext::new(cx).await;
8397
8398 let large_body_1 = "\nd".repeat(200);
8399 let large_body_2 = "\ne".repeat(200);
8400
8401 cx.set_state(&format!(
8402 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8403 ));
8404 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8405 let scroll_position = editor.scroll_position(cx);
8406 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8407 scroll_position
8408 });
8409
8410 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8411 .unwrap();
8412 cx.assert_editor_state(&format!(
8413 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8414 ));
8415 let scroll_position_after_selection =
8416 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8417 assert_eq!(
8418 initial_scroll_position, scroll_position_after_selection,
8419 "Scroll position should not change after selecting all matches"
8420 );
8421}
8422
8423#[gpui::test]
8424async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8425 init_test(cx, |_| {});
8426
8427 let mut cx = EditorLspTestContext::new_rust(
8428 lsp::ServerCapabilities {
8429 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8430 ..Default::default()
8431 },
8432 cx,
8433 )
8434 .await;
8435
8436 cx.set_state(indoc! {"
8437 line 1
8438 line 2
8439 linˇe 3
8440 line 4
8441 line 5
8442 "});
8443
8444 // Make an edit
8445 cx.update_editor(|editor, window, cx| {
8446 editor.handle_input("X", window, cx);
8447 });
8448
8449 // Move cursor to a different position
8450 cx.update_editor(|editor, window, cx| {
8451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8452 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8453 });
8454 });
8455
8456 cx.assert_editor_state(indoc! {"
8457 line 1
8458 line 2
8459 linXe 3
8460 line 4
8461 liˇne 5
8462 "});
8463
8464 cx.lsp
8465 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8466 Ok(Some(vec![lsp::TextEdit::new(
8467 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8468 "PREFIX ".to_string(),
8469 )]))
8470 });
8471
8472 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8473 .unwrap()
8474 .await
8475 .unwrap();
8476
8477 cx.assert_editor_state(indoc! {"
8478 PREFIX line 1
8479 line 2
8480 linXe 3
8481 line 4
8482 liˇne 5
8483 "});
8484
8485 // Undo formatting
8486 cx.update_editor(|editor, window, cx| {
8487 editor.undo(&Default::default(), window, cx);
8488 });
8489
8490 // Verify cursor moved back to position after edit
8491 cx.assert_editor_state(indoc! {"
8492 line 1
8493 line 2
8494 linXˇe 3
8495 line 4
8496 line 5
8497 "});
8498}
8499
8500#[gpui::test]
8501async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8502 init_test(cx, |_| {});
8503
8504 let mut cx = EditorTestContext::new(cx).await;
8505
8506 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8507 cx.update_editor(|editor, window, cx| {
8508 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8509 });
8510
8511 cx.set_state(indoc! {"
8512 line 1
8513 line 2
8514 linˇe 3
8515 line 4
8516 line 5
8517 line 6
8518 line 7
8519 line 8
8520 line 9
8521 line 10
8522 "});
8523
8524 let snapshot = cx.buffer_snapshot();
8525 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8526
8527 cx.update(|_, cx| {
8528 provider.update(cx, |provider, _| {
8529 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8530 id: None,
8531 edits: vec![(edit_position..edit_position, "X".into())],
8532 edit_preview: None,
8533 }))
8534 })
8535 });
8536
8537 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8538 cx.update_editor(|editor, window, cx| {
8539 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8540 });
8541
8542 cx.assert_editor_state(indoc! {"
8543 line 1
8544 line 2
8545 lineXˇ 3
8546 line 4
8547 line 5
8548 line 6
8549 line 7
8550 line 8
8551 line 9
8552 line 10
8553 "});
8554
8555 cx.update_editor(|editor, window, cx| {
8556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8557 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8558 });
8559 });
8560
8561 cx.assert_editor_state(indoc! {"
8562 line 1
8563 line 2
8564 lineX 3
8565 line 4
8566 line 5
8567 line 6
8568 line 7
8569 line 8
8570 line 9
8571 liˇne 10
8572 "});
8573
8574 cx.update_editor(|editor, window, cx| {
8575 editor.undo(&Default::default(), window, cx);
8576 });
8577
8578 cx.assert_editor_state(indoc! {"
8579 line 1
8580 line 2
8581 lineˇ 3
8582 line 4
8583 line 5
8584 line 6
8585 line 7
8586 line 8
8587 line 9
8588 line 10
8589 "});
8590}
8591
8592#[gpui::test]
8593async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8594 init_test(cx, |_| {});
8595
8596 let mut cx = EditorTestContext::new(cx).await;
8597 cx.set_state(
8598 r#"let foo = 2;
8599lˇet foo = 2;
8600let fooˇ = 2;
8601let foo = 2;
8602let foo = ˇ2;"#,
8603 );
8604
8605 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8606 .unwrap();
8607 cx.assert_editor_state(
8608 r#"let foo = 2;
8609«letˇ» foo = 2;
8610let «fooˇ» = 2;
8611let foo = 2;
8612let foo = «2ˇ»;"#,
8613 );
8614
8615 // noop for multiple selections with different contents
8616 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8617 .unwrap();
8618 cx.assert_editor_state(
8619 r#"let foo = 2;
8620«letˇ» foo = 2;
8621let «fooˇ» = 2;
8622let foo = 2;
8623let foo = «2ˇ»;"#,
8624 );
8625
8626 // Test last selection direction should be preserved
8627 cx.set_state(
8628 r#"let foo = 2;
8629let foo = 2;
8630let «fooˇ» = 2;
8631let «ˇfoo» = 2;
8632let foo = 2;"#,
8633 );
8634
8635 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8636 .unwrap();
8637 cx.assert_editor_state(
8638 r#"let foo = 2;
8639let foo = 2;
8640let «fooˇ» = 2;
8641let «ˇfoo» = 2;
8642let «ˇfoo» = 2;"#,
8643 );
8644}
8645
8646#[gpui::test]
8647async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8648 init_test(cx, |_| {});
8649
8650 let mut cx =
8651 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8652
8653 cx.assert_editor_state(indoc! {"
8654 ˇbbb
8655 ccc
8656
8657 bbb
8658 ccc
8659 "});
8660 cx.dispatch_action(SelectPrevious::default());
8661 cx.assert_editor_state(indoc! {"
8662 «bbbˇ»
8663 ccc
8664
8665 bbb
8666 ccc
8667 "});
8668 cx.dispatch_action(SelectPrevious::default());
8669 cx.assert_editor_state(indoc! {"
8670 «bbbˇ»
8671 ccc
8672
8673 «bbbˇ»
8674 ccc
8675 "});
8676}
8677
8678#[gpui::test]
8679async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8680 init_test(cx, |_| {});
8681
8682 let mut cx = EditorTestContext::new(cx).await;
8683 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8684
8685 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8686 .unwrap();
8687 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8688
8689 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8690 .unwrap();
8691 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8692
8693 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8694 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8695
8696 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8697 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8698
8699 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8700 .unwrap();
8701 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8702
8703 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8704 .unwrap();
8705 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8706}
8707
8708#[gpui::test]
8709async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8710 init_test(cx, |_| {});
8711
8712 let mut cx = EditorTestContext::new(cx).await;
8713 cx.set_state("aˇ");
8714
8715 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8716 .unwrap();
8717 cx.assert_editor_state("«aˇ»");
8718 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8719 .unwrap();
8720 cx.assert_editor_state("«aˇ»");
8721}
8722
8723#[gpui::test]
8724async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728 cx.set_state(
8729 r#"let foo = 2;
8730lˇet foo = 2;
8731let fooˇ = 2;
8732let foo = 2;
8733let foo = ˇ2;"#,
8734 );
8735
8736 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8737 .unwrap();
8738 cx.assert_editor_state(
8739 r#"let foo = 2;
8740«letˇ» foo = 2;
8741let «fooˇ» = 2;
8742let foo = 2;
8743let foo = «2ˇ»;"#,
8744 );
8745
8746 // noop for multiple selections with different contents
8747 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756}
8757
8758#[gpui::test]
8759async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8760 init_test(cx, |_| {});
8761
8762 let mut cx = EditorTestContext::new(cx).await;
8763 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8764
8765 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8766 .unwrap();
8767 // selection direction is preserved
8768 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8769
8770 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8771 .unwrap();
8772 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8773
8774 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8775 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8776
8777 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8778 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8779
8780 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8783
8784 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8785 .unwrap();
8786 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8787}
8788
8789#[gpui::test]
8790async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8791 init_test(cx, |_| {});
8792
8793 let language = Arc::new(Language::new(
8794 LanguageConfig::default(),
8795 Some(tree_sitter_rust::LANGUAGE.into()),
8796 ));
8797
8798 let text = r#"
8799 use mod1::mod2::{mod3, mod4};
8800
8801 fn fn_1(param1: bool, param2: &str) {
8802 let var1 = "text";
8803 }
8804 "#
8805 .unindent();
8806
8807 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8809 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8810
8811 editor
8812 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8813 .await;
8814
8815 editor.update_in(cx, |editor, window, cx| {
8816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8817 s.select_display_ranges([
8818 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8819 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8820 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8821 ]);
8822 });
8823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8824 });
8825 editor.update(cx, |editor, cx| {
8826 assert_text_with_selections(
8827 editor,
8828 indoc! {r#"
8829 use mod1::mod2::{mod3, «mod4ˇ»};
8830
8831 fn fn_1«ˇ(param1: bool, param2: &str)» {
8832 let var1 = "«ˇtext»";
8833 }
8834 "#},
8835 cx,
8836 );
8837 });
8838
8839 editor.update_in(cx, |editor, window, cx| {
8840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8841 });
8842 editor.update(cx, |editor, cx| {
8843 assert_text_with_selections(
8844 editor,
8845 indoc! {r#"
8846 use mod1::mod2::«{mod3, mod4}ˇ»;
8847
8848 «ˇfn fn_1(param1: bool, param2: &str) {
8849 let var1 = "text";
8850 }»
8851 "#},
8852 cx,
8853 );
8854 });
8855
8856 editor.update_in(cx, |editor, window, cx| {
8857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8858 });
8859 assert_eq!(
8860 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8861 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8862 );
8863
8864 // Trying to expand the selected syntax node one more time has no effect.
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 assert_eq!(
8869 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8870 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8871 );
8872
8873 editor.update_in(cx, |editor, window, cx| {
8874 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8875 });
8876 editor.update(cx, |editor, cx| {
8877 assert_text_with_selections(
8878 editor,
8879 indoc! {r#"
8880 use mod1::mod2::«{mod3, mod4}ˇ»;
8881
8882 «ˇfn fn_1(param1: bool, param2: &str) {
8883 let var1 = "text";
8884 }»
8885 "#},
8886 cx,
8887 );
8888 });
8889
8890 editor.update_in(cx, |editor, window, cx| {
8891 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8892 });
8893 editor.update(cx, |editor, cx| {
8894 assert_text_with_selections(
8895 editor,
8896 indoc! {r#"
8897 use mod1::mod2::{mod3, «mod4ˇ»};
8898
8899 fn fn_1«ˇ(param1: bool, param2: &str)» {
8900 let var1 = "«ˇtext»";
8901 }
8902 "#},
8903 cx,
8904 );
8905 });
8906
8907 editor.update_in(cx, |editor, window, cx| {
8908 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8909 });
8910 editor.update(cx, |editor, cx| {
8911 assert_text_with_selections(
8912 editor,
8913 indoc! {r#"
8914 use mod1::mod2::{mod3, moˇd4};
8915
8916 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8917 let var1 = "teˇxt";
8918 }
8919 "#},
8920 cx,
8921 );
8922 });
8923
8924 // Trying to shrink the selected syntax node one more time has no effect.
8925 editor.update_in(cx, |editor, window, cx| {
8926 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8927 });
8928 editor.update_in(cx, |editor, _, cx| {
8929 assert_text_with_selections(
8930 editor,
8931 indoc! {r#"
8932 use mod1::mod2::{mod3, moˇd4};
8933
8934 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8935 let var1 = "teˇxt";
8936 }
8937 "#},
8938 cx,
8939 );
8940 });
8941
8942 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8943 // a fold.
8944 editor.update_in(cx, |editor, window, cx| {
8945 editor.fold_creases(
8946 vec![
8947 Crease::simple(
8948 Point::new(0, 21)..Point::new(0, 24),
8949 FoldPlaceholder::test(),
8950 ),
8951 Crease::simple(
8952 Point::new(3, 20)..Point::new(3, 22),
8953 FoldPlaceholder::test(),
8954 ),
8955 ],
8956 true,
8957 window,
8958 cx,
8959 );
8960 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8961 });
8962 editor.update(cx, |editor, cx| {
8963 assert_text_with_selections(
8964 editor,
8965 indoc! {r#"
8966 use mod1::mod2::«{mod3, mod4}ˇ»;
8967
8968 fn fn_1«ˇ(param1: bool, param2: &str)» {
8969 let var1 = "«ˇtext»";
8970 }
8971 "#},
8972 cx,
8973 );
8974 });
8975}
8976
8977#[gpui::test]
8978async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8979 init_test(cx, |_| {});
8980
8981 let language = Arc::new(Language::new(
8982 LanguageConfig::default(),
8983 Some(tree_sitter_rust::LANGUAGE.into()),
8984 ));
8985
8986 let text = "let a = 2;";
8987
8988 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8989 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8990 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8991
8992 editor
8993 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8994 .await;
8995
8996 // Test case 1: Cursor at end of word
8997 editor.update_in(cx, |editor, window, cx| {
8998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8999 s.select_display_ranges([
9000 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9001 ]);
9002 });
9003 });
9004 editor.update(cx, |editor, cx| {
9005 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9006 });
9007 editor.update_in(cx, |editor, window, cx| {
9008 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9009 });
9010 editor.update(cx, |editor, cx| {
9011 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9012 });
9013 editor.update_in(cx, |editor, window, cx| {
9014 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9015 });
9016 editor.update(cx, |editor, cx| {
9017 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9018 });
9019
9020 // Test case 2: Cursor at end of statement
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9023 s.select_display_ranges([
9024 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9025 ]);
9026 });
9027 });
9028 editor.update(cx, |editor, cx| {
9029 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9030 });
9031 editor.update_in(cx, |editor, window, cx| {
9032 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9033 });
9034 editor.update(cx, |editor, cx| {
9035 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9036 });
9037}
9038
9039#[gpui::test]
9040async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9041 init_test(cx, |_| {});
9042
9043 let language = Arc::new(Language::new(
9044 LanguageConfig {
9045 name: "JavaScript".into(),
9046 ..Default::default()
9047 },
9048 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9049 ));
9050
9051 let text = r#"
9052 let a = {
9053 key: "value",
9054 };
9055 "#
9056 .unindent();
9057
9058 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9059 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9060 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9061
9062 editor
9063 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9064 .await;
9065
9066 // Test case 1: Cursor after '{'
9067 editor.update_in(cx, |editor, window, cx| {
9068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9069 s.select_display_ranges([
9070 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9071 ]);
9072 });
9073 });
9074 editor.update(cx, |editor, cx| {
9075 assert_text_with_selections(
9076 editor,
9077 indoc! {r#"
9078 let a = {ˇ
9079 key: "value",
9080 };
9081 "#},
9082 cx,
9083 );
9084 });
9085 editor.update_in(cx, |editor, window, cx| {
9086 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9087 });
9088 editor.update(cx, |editor, cx| {
9089 assert_text_with_selections(
9090 editor,
9091 indoc! {r#"
9092 let a = «ˇ{
9093 key: "value",
9094 }»;
9095 "#},
9096 cx,
9097 );
9098 });
9099
9100 // Test case 2: Cursor after ':'
9101 editor.update_in(cx, |editor, window, cx| {
9102 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9103 s.select_display_ranges([
9104 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9105 ]);
9106 });
9107 });
9108 editor.update(cx, |editor, cx| {
9109 assert_text_with_selections(
9110 editor,
9111 indoc! {r#"
9112 let a = {
9113 key:ˇ "value",
9114 };
9115 "#},
9116 cx,
9117 );
9118 });
9119 editor.update_in(cx, |editor, window, cx| {
9120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9121 });
9122 editor.update(cx, |editor, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 let a = {
9127 «ˇkey: "value"»,
9128 };
9129 "#},
9130 cx,
9131 );
9132 });
9133 editor.update_in(cx, |editor, window, cx| {
9134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9135 });
9136 editor.update(cx, |editor, cx| {
9137 assert_text_with_selections(
9138 editor,
9139 indoc! {r#"
9140 let a = «ˇ{
9141 key: "value",
9142 }»;
9143 "#},
9144 cx,
9145 );
9146 });
9147
9148 // Test case 3: Cursor after ','
9149 editor.update_in(cx, |editor, window, cx| {
9150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9151 s.select_display_ranges([
9152 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9153 ]);
9154 });
9155 });
9156 editor.update(cx, |editor, cx| {
9157 assert_text_with_selections(
9158 editor,
9159 indoc! {r#"
9160 let a = {
9161 key: "value",ˇ
9162 };
9163 "#},
9164 cx,
9165 );
9166 });
9167 editor.update_in(cx, |editor, window, cx| {
9168 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9169 });
9170 editor.update(cx, |editor, cx| {
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 let a = «ˇ{
9175 key: "value",
9176 }»;
9177 "#},
9178 cx,
9179 );
9180 });
9181
9182 // Test case 4: Cursor after ';'
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9185 s.select_display_ranges([
9186 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9187 ]);
9188 });
9189 });
9190 editor.update(cx, |editor, cx| {
9191 assert_text_with_selections(
9192 editor,
9193 indoc! {r#"
9194 let a = {
9195 key: "value",
9196 };ˇ
9197 "#},
9198 cx,
9199 );
9200 });
9201 editor.update_in(cx, |editor, window, cx| {
9202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9203 });
9204 editor.update(cx, |editor, cx| {
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 «ˇlet a = {
9209 key: "value",
9210 };
9211 »"#},
9212 cx,
9213 );
9214 });
9215}
9216
9217#[gpui::test]
9218async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9219 init_test(cx, |_| {});
9220
9221 let language = Arc::new(Language::new(
9222 LanguageConfig::default(),
9223 Some(tree_sitter_rust::LANGUAGE.into()),
9224 ));
9225
9226 let text = r#"
9227 use mod1::mod2::{mod3, mod4};
9228
9229 fn fn_1(param1: bool, param2: &str) {
9230 let var1 = "hello world";
9231 }
9232 "#
9233 .unindent();
9234
9235 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9236 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9237 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9238
9239 editor
9240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9241 .await;
9242
9243 // Test 1: Cursor on a letter of a string word
9244 editor.update_in(cx, |editor, window, cx| {
9245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9246 s.select_display_ranges([
9247 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9248 ]);
9249 });
9250 });
9251 editor.update_in(cx, |editor, window, cx| {
9252 assert_text_with_selections(
9253 editor,
9254 indoc! {r#"
9255 use mod1::mod2::{mod3, mod4};
9256
9257 fn fn_1(param1: bool, param2: &str) {
9258 let var1 = "hˇello world";
9259 }
9260 "#},
9261 cx,
9262 );
9263 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9264 assert_text_with_selections(
9265 editor,
9266 indoc! {r#"
9267 use mod1::mod2::{mod3, mod4};
9268
9269 fn fn_1(param1: bool, param2: &str) {
9270 let var1 = "«ˇhello» world";
9271 }
9272 "#},
9273 cx,
9274 );
9275 });
9276
9277 // Test 2: Partial selection within a word
9278 editor.update_in(cx, |editor, window, cx| {
9279 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9280 s.select_display_ranges([
9281 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9282 ]);
9283 });
9284 });
9285 editor.update_in(cx, |editor, window, cx| {
9286 assert_text_with_selections(
9287 editor,
9288 indoc! {r#"
9289 use mod1::mod2::{mod3, mod4};
9290
9291 fn fn_1(param1: bool, param2: &str) {
9292 let var1 = "h«elˇ»lo world";
9293 }
9294 "#},
9295 cx,
9296 );
9297 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9298 assert_text_with_selections(
9299 editor,
9300 indoc! {r#"
9301 use mod1::mod2::{mod3, mod4};
9302
9303 fn fn_1(param1: bool, param2: &str) {
9304 let var1 = "«ˇhello» world";
9305 }
9306 "#},
9307 cx,
9308 );
9309 });
9310
9311 // Test 3: Complete word already selected
9312 editor.update_in(cx, |editor, window, cx| {
9313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9314 s.select_display_ranges([
9315 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9316 ]);
9317 });
9318 });
9319 editor.update_in(cx, |editor, window, cx| {
9320 assert_text_with_selections(
9321 editor,
9322 indoc! {r#"
9323 use mod1::mod2::{mod3, mod4};
9324
9325 fn fn_1(param1: bool, param2: &str) {
9326 let var1 = "«helloˇ» world";
9327 }
9328 "#},
9329 cx,
9330 );
9331 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9332 assert_text_with_selections(
9333 editor,
9334 indoc! {r#"
9335 use mod1::mod2::{mod3, mod4};
9336
9337 fn fn_1(param1: bool, param2: &str) {
9338 let var1 = "«hello worldˇ»";
9339 }
9340 "#},
9341 cx,
9342 );
9343 });
9344
9345 // Test 4: Selection spanning across words
9346 editor.update_in(cx, |editor, window, cx| {
9347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9348 s.select_display_ranges([
9349 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9350 ]);
9351 });
9352 });
9353 editor.update_in(cx, |editor, window, cx| {
9354 assert_text_with_selections(
9355 editor,
9356 indoc! {r#"
9357 use mod1::mod2::{mod3, mod4};
9358
9359 fn fn_1(param1: bool, param2: &str) {
9360 let var1 = "hel«lo woˇ»rld";
9361 }
9362 "#},
9363 cx,
9364 );
9365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9366 assert_text_with_selections(
9367 editor,
9368 indoc! {r#"
9369 use mod1::mod2::{mod3, mod4};
9370
9371 fn fn_1(param1: bool, param2: &str) {
9372 let var1 = "«ˇhello world»";
9373 }
9374 "#},
9375 cx,
9376 );
9377 });
9378
9379 // Test 5: Expansion beyond string
9380 editor.update_in(cx, |editor, window, cx| {
9381 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9382 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9383 assert_text_with_selections(
9384 editor,
9385 indoc! {r#"
9386 use mod1::mod2::{mod3, mod4};
9387
9388 fn fn_1(param1: bool, param2: &str) {
9389 «ˇlet var1 = "hello world";»
9390 }
9391 "#},
9392 cx,
9393 );
9394 });
9395}
9396
9397#[gpui::test]
9398async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9399 init_test(cx, |_| {});
9400
9401 let mut cx = EditorTestContext::new(cx).await;
9402
9403 let language = Arc::new(Language::new(
9404 LanguageConfig::default(),
9405 Some(tree_sitter_rust::LANGUAGE.into()),
9406 ));
9407
9408 cx.update_buffer(|buffer, cx| {
9409 buffer.set_language(Some(language), cx);
9410 });
9411
9412 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9413 cx.update_editor(|editor, window, cx| {
9414 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9415 });
9416
9417 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9418
9419 cx.set_state(indoc! { r#"fn a() {
9420 // what
9421 // a
9422 // ˇlong
9423 // method
9424 // I
9425 // sure
9426 // hope
9427 // it
9428 // works
9429 }"# });
9430
9431 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9432 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9433 cx.update(|_, cx| {
9434 multi_buffer.update(cx, |multi_buffer, cx| {
9435 multi_buffer.set_excerpts_for_path(
9436 PathKey::for_buffer(&buffer, cx),
9437 buffer,
9438 [Point::new(1, 0)..Point::new(1, 0)],
9439 3,
9440 cx,
9441 );
9442 });
9443 });
9444
9445 let editor2 = cx.new_window_entity(|window, cx| {
9446 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9447 });
9448
9449 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9450 cx.update_editor(|editor, window, cx| {
9451 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9452 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9453 })
9454 });
9455
9456 cx.assert_editor_state(indoc! { "
9457 fn a() {
9458 // what
9459 // a
9460 ˇ // long
9461 // method"});
9462
9463 cx.update_editor(|editor, window, cx| {
9464 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9465 });
9466
9467 // Although we could potentially make the action work when the syntax node
9468 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9469 // did. Maybe we could also expand the excerpt to contain the range?
9470 cx.assert_editor_state(indoc! { "
9471 fn a() {
9472 // what
9473 // a
9474 ˇ // long
9475 // method"});
9476}
9477
9478#[gpui::test]
9479async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9480 init_test(cx, |_| {});
9481
9482 let base_text = r#"
9483 impl A {
9484 // this is an uncommitted comment
9485
9486 fn b() {
9487 c();
9488 }
9489
9490 // this is another uncommitted comment
9491
9492 fn d() {
9493 // e
9494 // f
9495 }
9496 }
9497
9498 fn g() {
9499 // h
9500 }
9501 "#
9502 .unindent();
9503
9504 let text = r#"
9505 ˇimpl A {
9506
9507 fn b() {
9508 c();
9509 }
9510
9511 fn d() {
9512 // e
9513 // f
9514 }
9515 }
9516
9517 fn g() {
9518 // h
9519 }
9520 "#
9521 .unindent();
9522
9523 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9524 cx.set_state(&text);
9525 cx.set_head_text(&base_text);
9526 cx.update_editor(|editor, window, cx| {
9527 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9528 });
9529
9530 cx.assert_state_with_diff(
9531 "
9532 ˇimpl A {
9533 - // this is an uncommitted comment
9534
9535 fn b() {
9536 c();
9537 }
9538
9539 - // this is another uncommitted comment
9540 -
9541 fn d() {
9542 // e
9543 // f
9544 }
9545 }
9546
9547 fn g() {
9548 // h
9549 }
9550 "
9551 .unindent(),
9552 );
9553
9554 let expected_display_text = "
9555 impl A {
9556 // this is an uncommitted comment
9557
9558 fn b() {
9559 ⋯
9560 }
9561
9562 // this is another uncommitted comment
9563
9564 fn d() {
9565 ⋯
9566 }
9567 }
9568
9569 fn g() {
9570 ⋯
9571 }
9572 "
9573 .unindent();
9574
9575 cx.update_editor(|editor, window, cx| {
9576 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9577 assert_eq!(editor.display_text(cx), expected_display_text);
9578 });
9579}
9580
9581#[gpui::test]
9582async fn test_autoindent(cx: &mut TestAppContext) {
9583 init_test(cx, |_| {});
9584
9585 let language = Arc::new(
9586 Language::new(
9587 LanguageConfig {
9588 brackets: BracketPairConfig {
9589 pairs: vec![
9590 BracketPair {
9591 start: "{".to_string(),
9592 end: "}".to_string(),
9593 close: false,
9594 surround: false,
9595 newline: true,
9596 },
9597 BracketPair {
9598 start: "(".to_string(),
9599 end: ")".to_string(),
9600 close: false,
9601 surround: false,
9602 newline: true,
9603 },
9604 ],
9605 ..Default::default()
9606 },
9607 ..Default::default()
9608 },
9609 Some(tree_sitter_rust::LANGUAGE.into()),
9610 )
9611 .with_indents_query(
9612 r#"
9613 (_ "(" ")" @end) @indent
9614 (_ "{" "}" @end) @indent
9615 "#,
9616 )
9617 .unwrap(),
9618 );
9619
9620 let text = "fn a() {}";
9621
9622 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9623 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9625 editor
9626 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9627 .await;
9628
9629 editor.update_in(cx, |editor, window, cx| {
9630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9631 s.select_ranges([5..5, 8..8, 9..9])
9632 });
9633 editor.newline(&Newline, window, cx);
9634 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9635 assert_eq!(
9636 editor.selections.ranges(&editor.display_snapshot(cx)),
9637 &[
9638 Point::new(1, 4)..Point::new(1, 4),
9639 Point::new(3, 4)..Point::new(3, 4),
9640 Point::new(5, 0)..Point::new(5, 0)
9641 ]
9642 );
9643 });
9644}
9645
9646#[gpui::test]
9647async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9648 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9649
9650 let language = Arc::new(
9651 Language::new(
9652 LanguageConfig {
9653 brackets: BracketPairConfig {
9654 pairs: vec![
9655 BracketPair {
9656 start: "{".to_string(),
9657 end: "}".to_string(),
9658 close: false,
9659 surround: false,
9660 newline: true,
9661 },
9662 BracketPair {
9663 start: "(".to_string(),
9664 end: ")".to_string(),
9665 close: false,
9666 surround: false,
9667 newline: true,
9668 },
9669 ],
9670 ..Default::default()
9671 },
9672 ..Default::default()
9673 },
9674 Some(tree_sitter_rust::LANGUAGE.into()),
9675 )
9676 .with_indents_query(
9677 r#"
9678 (_ "(" ")" @end) @indent
9679 (_ "{" "}" @end) @indent
9680 "#,
9681 )
9682 .unwrap(),
9683 );
9684
9685 let text = "fn a() {}";
9686
9687 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9688 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9689 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9690 editor
9691 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9692 .await;
9693
9694 editor.update_in(cx, |editor, window, cx| {
9695 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9696 s.select_ranges([5..5, 8..8, 9..9])
9697 });
9698 editor.newline(&Newline, window, cx);
9699 assert_eq!(
9700 editor.text(cx),
9701 indoc!(
9702 "
9703 fn a(
9704
9705 ) {
9706
9707 }
9708 "
9709 )
9710 );
9711 assert_eq!(
9712 editor.selections.ranges(&editor.display_snapshot(cx)),
9713 &[
9714 Point::new(1, 0)..Point::new(1, 0),
9715 Point::new(3, 0)..Point::new(3, 0),
9716 Point::new(5, 0)..Point::new(5, 0)
9717 ]
9718 );
9719 });
9720}
9721
9722#[gpui::test]
9723async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9724 init_test(cx, |settings| {
9725 settings.defaults.auto_indent = Some(true);
9726 settings.languages.0.insert(
9727 "python".into(),
9728 LanguageSettingsContent {
9729 auto_indent: Some(false),
9730 ..Default::default()
9731 },
9732 );
9733 });
9734
9735 let mut cx = EditorTestContext::new(cx).await;
9736
9737 let injected_language = Arc::new(
9738 Language::new(
9739 LanguageConfig {
9740 brackets: BracketPairConfig {
9741 pairs: vec![
9742 BracketPair {
9743 start: "{".to_string(),
9744 end: "}".to_string(),
9745 close: false,
9746 surround: false,
9747 newline: true,
9748 },
9749 BracketPair {
9750 start: "(".to_string(),
9751 end: ")".to_string(),
9752 close: true,
9753 surround: false,
9754 newline: true,
9755 },
9756 ],
9757 ..Default::default()
9758 },
9759 name: "python".into(),
9760 ..Default::default()
9761 },
9762 Some(tree_sitter_python::LANGUAGE.into()),
9763 )
9764 .with_indents_query(
9765 r#"
9766 (_ "(" ")" @end) @indent
9767 (_ "{" "}" @end) @indent
9768 "#,
9769 )
9770 .unwrap(),
9771 );
9772
9773 let language = Arc::new(
9774 Language::new(
9775 LanguageConfig {
9776 brackets: BracketPairConfig {
9777 pairs: vec![
9778 BracketPair {
9779 start: "{".to_string(),
9780 end: "}".to_string(),
9781 close: false,
9782 surround: false,
9783 newline: true,
9784 },
9785 BracketPair {
9786 start: "(".to_string(),
9787 end: ")".to_string(),
9788 close: true,
9789 surround: false,
9790 newline: true,
9791 },
9792 ],
9793 ..Default::default()
9794 },
9795 name: LanguageName::new("rust"),
9796 ..Default::default()
9797 },
9798 Some(tree_sitter_rust::LANGUAGE.into()),
9799 )
9800 .with_indents_query(
9801 r#"
9802 (_ "(" ")" @end) @indent
9803 (_ "{" "}" @end) @indent
9804 "#,
9805 )
9806 .unwrap()
9807 .with_injection_query(
9808 r#"
9809 (macro_invocation
9810 macro: (identifier) @_macro_name
9811 (token_tree) @injection.content
9812 (#set! injection.language "python"))
9813 "#,
9814 )
9815 .unwrap(),
9816 );
9817
9818 cx.language_registry().add(injected_language);
9819 cx.language_registry().add(language.clone());
9820
9821 cx.update_buffer(|buffer, cx| {
9822 buffer.set_language(Some(language), cx);
9823 });
9824
9825 cx.set_state(r#"struct A {ˇ}"#);
9826
9827 cx.update_editor(|editor, window, cx| {
9828 editor.newline(&Default::default(), window, cx);
9829 });
9830
9831 cx.assert_editor_state(indoc!(
9832 "struct A {
9833 ˇ
9834 }"
9835 ));
9836
9837 cx.set_state(r#"select_biased!(ˇ)"#);
9838
9839 cx.update_editor(|editor, window, cx| {
9840 editor.newline(&Default::default(), window, cx);
9841 editor.handle_input("def ", window, cx);
9842 editor.handle_input("(", window, cx);
9843 editor.newline(&Default::default(), window, cx);
9844 editor.handle_input("a", window, cx);
9845 });
9846
9847 cx.assert_editor_state(indoc!(
9848 "select_biased!(
9849 def (
9850 aˇ
9851 )
9852 )"
9853 ));
9854}
9855
9856#[gpui::test]
9857async fn test_autoindent_selections(cx: &mut TestAppContext) {
9858 init_test(cx, |_| {});
9859
9860 {
9861 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9862 cx.set_state(indoc! {"
9863 impl A {
9864
9865 fn b() {}
9866
9867 «fn c() {
9868
9869 }ˇ»
9870 }
9871 "});
9872
9873 cx.update_editor(|editor, window, cx| {
9874 editor.autoindent(&Default::default(), window, cx);
9875 });
9876
9877 cx.assert_editor_state(indoc! {"
9878 impl A {
9879
9880 fn b() {}
9881
9882 «fn c() {
9883
9884 }ˇ»
9885 }
9886 "});
9887 }
9888
9889 {
9890 let mut cx = EditorTestContext::new_multibuffer(
9891 cx,
9892 [indoc! { "
9893 impl A {
9894 «
9895 // a
9896 fn b(){}
9897 »
9898 «
9899 }
9900 fn c(){}
9901 »
9902 "}],
9903 );
9904
9905 let buffer = cx.update_editor(|editor, _, cx| {
9906 let buffer = editor.buffer().update(cx, |buffer, _| {
9907 buffer.all_buffers().iter().next().unwrap().clone()
9908 });
9909 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9910 buffer
9911 });
9912
9913 cx.run_until_parked();
9914 cx.update_editor(|editor, window, cx| {
9915 editor.select_all(&Default::default(), window, cx);
9916 editor.autoindent(&Default::default(), window, cx)
9917 });
9918 cx.run_until_parked();
9919
9920 cx.update(|_, cx| {
9921 assert_eq!(
9922 buffer.read(cx).text(),
9923 indoc! { "
9924 impl A {
9925
9926 // a
9927 fn b(){}
9928
9929
9930 }
9931 fn c(){}
9932
9933 " }
9934 )
9935 });
9936 }
9937}
9938
9939#[gpui::test]
9940async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9941 init_test(cx, |_| {});
9942
9943 let mut cx = EditorTestContext::new(cx).await;
9944
9945 let language = Arc::new(Language::new(
9946 LanguageConfig {
9947 brackets: BracketPairConfig {
9948 pairs: vec![
9949 BracketPair {
9950 start: "{".to_string(),
9951 end: "}".to_string(),
9952 close: true,
9953 surround: true,
9954 newline: true,
9955 },
9956 BracketPair {
9957 start: "(".to_string(),
9958 end: ")".to_string(),
9959 close: true,
9960 surround: true,
9961 newline: true,
9962 },
9963 BracketPair {
9964 start: "/*".to_string(),
9965 end: " */".to_string(),
9966 close: true,
9967 surround: true,
9968 newline: true,
9969 },
9970 BracketPair {
9971 start: "[".to_string(),
9972 end: "]".to_string(),
9973 close: false,
9974 surround: false,
9975 newline: true,
9976 },
9977 BracketPair {
9978 start: "\"".to_string(),
9979 end: "\"".to_string(),
9980 close: true,
9981 surround: true,
9982 newline: false,
9983 },
9984 BracketPair {
9985 start: "<".to_string(),
9986 end: ">".to_string(),
9987 close: false,
9988 surround: true,
9989 newline: true,
9990 },
9991 ],
9992 ..Default::default()
9993 },
9994 autoclose_before: "})]".to_string(),
9995 ..Default::default()
9996 },
9997 Some(tree_sitter_rust::LANGUAGE.into()),
9998 ));
9999
10000 cx.language_registry().add(language.clone());
10001 cx.update_buffer(|buffer, cx| {
10002 buffer.set_language(Some(language), cx);
10003 });
10004
10005 cx.set_state(
10006 &r#"
10007 🏀ˇ
10008 εˇ
10009 ❤️ˇ
10010 "#
10011 .unindent(),
10012 );
10013
10014 // autoclose multiple nested brackets at multiple cursors
10015 cx.update_editor(|editor, window, cx| {
10016 editor.handle_input("{", window, cx);
10017 editor.handle_input("{", window, cx);
10018 editor.handle_input("{", window, cx);
10019 });
10020 cx.assert_editor_state(
10021 &"
10022 🏀{{{ˇ}}}
10023 ε{{{ˇ}}}
10024 ❤️{{{ˇ}}}
10025 "
10026 .unindent(),
10027 );
10028
10029 // insert a different closing bracket
10030 cx.update_editor(|editor, window, cx| {
10031 editor.handle_input(")", window, cx);
10032 });
10033 cx.assert_editor_state(
10034 &"
10035 🏀{{{)ˇ}}}
10036 ε{{{)ˇ}}}
10037 ❤️{{{)ˇ}}}
10038 "
10039 .unindent(),
10040 );
10041
10042 // skip over the auto-closed brackets when typing a closing bracket
10043 cx.update_editor(|editor, window, cx| {
10044 editor.move_right(&MoveRight, window, cx);
10045 editor.handle_input("}", window, cx);
10046 editor.handle_input("}", window, cx);
10047 editor.handle_input("}", window, cx);
10048 });
10049 cx.assert_editor_state(
10050 &"
10051 🏀{{{)}}}}ˇ
10052 ε{{{)}}}}ˇ
10053 ❤️{{{)}}}}ˇ
10054 "
10055 .unindent(),
10056 );
10057
10058 // autoclose multi-character pairs
10059 cx.set_state(
10060 &"
10061 ˇ
10062 ˇ
10063 "
10064 .unindent(),
10065 );
10066 cx.update_editor(|editor, window, cx| {
10067 editor.handle_input("/", window, cx);
10068 editor.handle_input("*", window, cx);
10069 });
10070 cx.assert_editor_state(
10071 &"
10072 /*ˇ */
10073 /*ˇ */
10074 "
10075 .unindent(),
10076 );
10077
10078 // one cursor autocloses a multi-character pair, one cursor
10079 // does not autoclose.
10080 cx.set_state(
10081 &"
10082 /ˇ
10083 ˇ
10084 "
10085 .unindent(),
10086 );
10087 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10088 cx.assert_editor_state(
10089 &"
10090 /*ˇ */
10091 *ˇ
10092 "
10093 .unindent(),
10094 );
10095
10096 // Don't autoclose if the next character isn't whitespace and isn't
10097 // listed in the language's "autoclose_before" section.
10098 cx.set_state("ˇa b");
10099 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10100 cx.assert_editor_state("{ˇa b");
10101
10102 // Don't autoclose if `close` is false for the bracket pair
10103 cx.set_state("ˇ");
10104 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10105 cx.assert_editor_state("[ˇ");
10106
10107 // Surround with brackets if text is selected
10108 cx.set_state("«aˇ» b");
10109 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10110 cx.assert_editor_state("{«aˇ»} b");
10111
10112 // Autoclose when not immediately after a word character
10113 cx.set_state("a ˇ");
10114 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10115 cx.assert_editor_state("a \"ˇ\"");
10116
10117 // Autoclose pair where the start and end characters are the same
10118 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10119 cx.assert_editor_state("a \"\"ˇ");
10120
10121 // Don't autoclose when immediately after a word character
10122 cx.set_state("aˇ");
10123 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10124 cx.assert_editor_state("a\"ˇ");
10125
10126 // Do autoclose when after a non-word character
10127 cx.set_state("{ˇ");
10128 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10129 cx.assert_editor_state("{\"ˇ\"");
10130
10131 // Non identical pairs autoclose regardless of preceding character
10132 cx.set_state("aˇ");
10133 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10134 cx.assert_editor_state("a{ˇ}");
10135
10136 // Don't autoclose pair if autoclose is disabled
10137 cx.set_state("ˇ");
10138 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10139 cx.assert_editor_state("<ˇ");
10140
10141 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10142 cx.set_state("«aˇ» b");
10143 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10144 cx.assert_editor_state("<«aˇ»> b");
10145}
10146
10147#[gpui::test]
10148async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10149 init_test(cx, |settings| {
10150 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10151 });
10152
10153 let mut cx = EditorTestContext::new(cx).await;
10154
10155 let language = Arc::new(Language::new(
10156 LanguageConfig {
10157 brackets: BracketPairConfig {
10158 pairs: vec![
10159 BracketPair {
10160 start: "{".to_string(),
10161 end: "}".to_string(),
10162 close: true,
10163 surround: true,
10164 newline: true,
10165 },
10166 BracketPair {
10167 start: "(".to_string(),
10168 end: ")".to_string(),
10169 close: true,
10170 surround: true,
10171 newline: true,
10172 },
10173 BracketPair {
10174 start: "[".to_string(),
10175 end: "]".to_string(),
10176 close: false,
10177 surround: false,
10178 newline: true,
10179 },
10180 ],
10181 ..Default::default()
10182 },
10183 autoclose_before: "})]".to_string(),
10184 ..Default::default()
10185 },
10186 Some(tree_sitter_rust::LANGUAGE.into()),
10187 ));
10188
10189 cx.language_registry().add(language.clone());
10190 cx.update_buffer(|buffer, cx| {
10191 buffer.set_language(Some(language), cx);
10192 });
10193
10194 cx.set_state(
10195 &"
10196 ˇ
10197 ˇ
10198 ˇ
10199 "
10200 .unindent(),
10201 );
10202
10203 // ensure only matching closing brackets are skipped over
10204 cx.update_editor(|editor, window, cx| {
10205 editor.handle_input("}", window, cx);
10206 editor.move_left(&MoveLeft, window, cx);
10207 editor.handle_input(")", window, cx);
10208 editor.move_left(&MoveLeft, window, cx);
10209 });
10210 cx.assert_editor_state(
10211 &"
10212 ˇ)}
10213 ˇ)}
10214 ˇ)}
10215 "
10216 .unindent(),
10217 );
10218
10219 // skip-over closing brackets at multiple cursors
10220 cx.update_editor(|editor, window, cx| {
10221 editor.handle_input(")", window, cx);
10222 editor.handle_input("}", window, cx);
10223 });
10224 cx.assert_editor_state(
10225 &"
10226 )}ˇ
10227 )}ˇ
10228 )}ˇ
10229 "
10230 .unindent(),
10231 );
10232
10233 // ignore non-close brackets
10234 cx.update_editor(|editor, window, cx| {
10235 editor.handle_input("]", window, cx);
10236 editor.move_left(&MoveLeft, window, cx);
10237 editor.handle_input("]", window, cx);
10238 });
10239 cx.assert_editor_state(
10240 &"
10241 )}]ˇ]
10242 )}]ˇ]
10243 )}]ˇ]
10244 "
10245 .unindent(),
10246 );
10247}
10248
10249#[gpui::test]
10250async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10251 init_test(cx, |_| {});
10252
10253 let mut cx = EditorTestContext::new(cx).await;
10254
10255 let html_language = Arc::new(
10256 Language::new(
10257 LanguageConfig {
10258 name: "HTML".into(),
10259 brackets: BracketPairConfig {
10260 pairs: vec![
10261 BracketPair {
10262 start: "<".into(),
10263 end: ">".into(),
10264 close: true,
10265 ..Default::default()
10266 },
10267 BracketPair {
10268 start: "{".into(),
10269 end: "}".into(),
10270 close: true,
10271 ..Default::default()
10272 },
10273 BracketPair {
10274 start: "(".into(),
10275 end: ")".into(),
10276 close: true,
10277 ..Default::default()
10278 },
10279 ],
10280 ..Default::default()
10281 },
10282 autoclose_before: "})]>".into(),
10283 ..Default::default()
10284 },
10285 Some(tree_sitter_html::LANGUAGE.into()),
10286 )
10287 .with_injection_query(
10288 r#"
10289 (script_element
10290 (raw_text) @injection.content
10291 (#set! injection.language "javascript"))
10292 "#,
10293 )
10294 .unwrap(),
10295 );
10296
10297 let javascript_language = Arc::new(Language::new(
10298 LanguageConfig {
10299 name: "JavaScript".into(),
10300 brackets: BracketPairConfig {
10301 pairs: vec![
10302 BracketPair {
10303 start: "/*".into(),
10304 end: " */".into(),
10305 close: true,
10306 ..Default::default()
10307 },
10308 BracketPair {
10309 start: "{".into(),
10310 end: "}".into(),
10311 close: true,
10312 ..Default::default()
10313 },
10314 BracketPair {
10315 start: "(".into(),
10316 end: ")".into(),
10317 close: true,
10318 ..Default::default()
10319 },
10320 ],
10321 ..Default::default()
10322 },
10323 autoclose_before: "})]>".into(),
10324 ..Default::default()
10325 },
10326 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10327 ));
10328
10329 cx.language_registry().add(html_language.clone());
10330 cx.language_registry().add(javascript_language);
10331 cx.executor().run_until_parked();
10332
10333 cx.update_buffer(|buffer, cx| {
10334 buffer.set_language(Some(html_language), cx);
10335 });
10336
10337 cx.set_state(
10338 &r#"
10339 <body>ˇ
10340 <script>
10341 var x = 1;ˇ
10342 </script>
10343 </body>ˇ
10344 "#
10345 .unindent(),
10346 );
10347
10348 // Precondition: different languages are active at different locations.
10349 cx.update_editor(|editor, window, cx| {
10350 let snapshot = editor.snapshot(window, cx);
10351 let cursors = editor
10352 .selections
10353 .ranges::<usize>(&editor.display_snapshot(cx));
10354 let languages = cursors
10355 .iter()
10356 .map(|c| snapshot.language_at(c.start).unwrap().name())
10357 .collect::<Vec<_>>();
10358 assert_eq!(
10359 languages,
10360 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10361 );
10362 });
10363
10364 // Angle brackets autoclose in HTML, but not JavaScript.
10365 cx.update_editor(|editor, window, cx| {
10366 editor.handle_input("<", window, cx);
10367 editor.handle_input("a", window, cx);
10368 });
10369 cx.assert_editor_state(
10370 &r#"
10371 <body><aˇ>
10372 <script>
10373 var x = 1;<aˇ
10374 </script>
10375 </body><aˇ>
10376 "#
10377 .unindent(),
10378 );
10379
10380 // Curly braces and parens autoclose in both HTML and JavaScript.
10381 cx.update_editor(|editor, window, cx| {
10382 editor.handle_input(" b=", window, cx);
10383 editor.handle_input("{", window, cx);
10384 editor.handle_input("c", window, cx);
10385 editor.handle_input("(", window, cx);
10386 });
10387 cx.assert_editor_state(
10388 &r#"
10389 <body><a b={c(ˇ)}>
10390 <script>
10391 var x = 1;<a b={c(ˇ)}
10392 </script>
10393 </body><a b={c(ˇ)}>
10394 "#
10395 .unindent(),
10396 );
10397
10398 // Brackets that were already autoclosed are skipped.
10399 cx.update_editor(|editor, window, cx| {
10400 editor.handle_input(")", window, cx);
10401 editor.handle_input("d", window, cx);
10402 editor.handle_input("}", window, cx);
10403 });
10404 cx.assert_editor_state(
10405 &r#"
10406 <body><a b={c()d}ˇ>
10407 <script>
10408 var x = 1;<a b={c()d}ˇ
10409 </script>
10410 </body><a b={c()d}ˇ>
10411 "#
10412 .unindent(),
10413 );
10414 cx.update_editor(|editor, window, cx| {
10415 editor.handle_input(">", window, cx);
10416 });
10417 cx.assert_editor_state(
10418 &r#"
10419 <body><a b={c()d}>ˇ
10420 <script>
10421 var x = 1;<a b={c()d}>ˇ
10422 </script>
10423 </body><a b={c()d}>ˇ
10424 "#
10425 .unindent(),
10426 );
10427
10428 // Reset
10429 cx.set_state(
10430 &r#"
10431 <body>ˇ
10432 <script>
10433 var x = 1;ˇ
10434 </script>
10435 </body>ˇ
10436 "#
10437 .unindent(),
10438 );
10439
10440 cx.update_editor(|editor, window, cx| {
10441 editor.handle_input("<", window, cx);
10442 });
10443 cx.assert_editor_state(
10444 &r#"
10445 <body><ˇ>
10446 <script>
10447 var x = 1;<ˇ
10448 </script>
10449 </body><ˇ>
10450 "#
10451 .unindent(),
10452 );
10453
10454 // When backspacing, the closing angle brackets are removed.
10455 cx.update_editor(|editor, window, cx| {
10456 editor.backspace(&Backspace, window, cx);
10457 });
10458 cx.assert_editor_state(
10459 &r#"
10460 <body>ˇ
10461 <script>
10462 var x = 1;ˇ
10463 </script>
10464 </body>ˇ
10465 "#
10466 .unindent(),
10467 );
10468
10469 // Block comments autoclose in JavaScript, but not HTML.
10470 cx.update_editor(|editor, window, cx| {
10471 editor.handle_input("/", window, cx);
10472 editor.handle_input("*", window, cx);
10473 });
10474 cx.assert_editor_state(
10475 &r#"
10476 <body>/*ˇ
10477 <script>
10478 var x = 1;/*ˇ */
10479 </script>
10480 </body>/*ˇ
10481 "#
10482 .unindent(),
10483 );
10484}
10485
10486#[gpui::test]
10487async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10488 init_test(cx, |_| {});
10489
10490 let mut cx = EditorTestContext::new(cx).await;
10491
10492 let rust_language = Arc::new(
10493 Language::new(
10494 LanguageConfig {
10495 name: "Rust".into(),
10496 brackets: serde_json::from_value(json!([
10497 { "start": "{", "end": "}", "close": true, "newline": true },
10498 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10499 ]))
10500 .unwrap(),
10501 autoclose_before: "})]>".into(),
10502 ..Default::default()
10503 },
10504 Some(tree_sitter_rust::LANGUAGE.into()),
10505 )
10506 .with_override_query("(string_literal) @string")
10507 .unwrap(),
10508 );
10509
10510 cx.language_registry().add(rust_language.clone());
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(rust_language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 let x = ˇ
10518 "#
10519 .unindent(),
10520 );
10521
10522 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10523 cx.update_editor(|editor, window, cx| {
10524 editor.handle_input("\"", window, cx);
10525 });
10526 cx.assert_editor_state(
10527 &r#"
10528 let x = "ˇ"
10529 "#
10530 .unindent(),
10531 );
10532
10533 // Inserting another quotation mark. The cursor moves across the existing
10534 // automatically-inserted quotation mark.
10535 cx.update_editor(|editor, window, cx| {
10536 editor.handle_input("\"", window, cx);
10537 });
10538 cx.assert_editor_state(
10539 &r#"
10540 let x = ""ˇ
10541 "#
10542 .unindent(),
10543 );
10544
10545 // Reset
10546 cx.set_state(
10547 &r#"
10548 let x = ˇ
10549 "#
10550 .unindent(),
10551 );
10552
10553 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10554 cx.update_editor(|editor, window, cx| {
10555 editor.handle_input("\"", window, cx);
10556 editor.handle_input(" ", window, cx);
10557 editor.move_left(&Default::default(), window, cx);
10558 editor.handle_input("\\", window, cx);
10559 editor.handle_input("\"", window, cx);
10560 });
10561 cx.assert_editor_state(
10562 &r#"
10563 let x = "\"ˇ "
10564 "#
10565 .unindent(),
10566 );
10567
10568 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10569 // mark. Nothing is inserted.
10570 cx.update_editor(|editor, window, cx| {
10571 editor.move_right(&Default::default(), window, cx);
10572 editor.handle_input("\"", window, cx);
10573 });
10574 cx.assert_editor_state(
10575 &r#"
10576 let x = "\" "ˇ
10577 "#
10578 .unindent(),
10579 );
10580}
10581
10582#[gpui::test]
10583async fn test_surround_with_pair(cx: &mut TestAppContext) {
10584 init_test(cx, |_| {});
10585
10586 let language = Arc::new(Language::new(
10587 LanguageConfig {
10588 brackets: BracketPairConfig {
10589 pairs: vec![
10590 BracketPair {
10591 start: "{".to_string(),
10592 end: "}".to_string(),
10593 close: true,
10594 surround: true,
10595 newline: true,
10596 },
10597 BracketPair {
10598 start: "/* ".to_string(),
10599 end: "*/".to_string(),
10600 close: true,
10601 surround: true,
10602 ..Default::default()
10603 },
10604 ],
10605 ..Default::default()
10606 },
10607 ..Default::default()
10608 },
10609 Some(tree_sitter_rust::LANGUAGE.into()),
10610 ));
10611
10612 let text = r#"
10613 a
10614 b
10615 c
10616 "#
10617 .unindent();
10618
10619 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10620 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10621 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10622 editor
10623 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10624 .await;
10625
10626 editor.update_in(cx, |editor, window, cx| {
10627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10628 s.select_display_ranges([
10629 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10631 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10632 ])
10633 });
10634
10635 editor.handle_input("{", window, cx);
10636 editor.handle_input("{", window, cx);
10637 editor.handle_input("{", window, cx);
10638 assert_eq!(
10639 editor.text(cx),
10640 "
10641 {{{a}}}
10642 {{{b}}}
10643 {{{c}}}
10644 "
10645 .unindent()
10646 );
10647 assert_eq!(
10648 editor.selections.display_ranges(cx),
10649 [
10650 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10651 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10652 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10653 ]
10654 );
10655
10656 editor.undo(&Undo, window, cx);
10657 editor.undo(&Undo, window, cx);
10658 editor.undo(&Undo, window, cx);
10659 assert_eq!(
10660 editor.text(cx),
10661 "
10662 a
10663 b
10664 c
10665 "
10666 .unindent()
10667 );
10668 assert_eq!(
10669 editor.selections.display_ranges(cx),
10670 [
10671 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10672 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10673 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10674 ]
10675 );
10676
10677 // Ensure inserting the first character of a multi-byte bracket pair
10678 // doesn't surround the selections with the bracket.
10679 editor.handle_input("/", window, cx);
10680 assert_eq!(
10681 editor.text(cx),
10682 "
10683 /
10684 /
10685 /
10686 "
10687 .unindent()
10688 );
10689 assert_eq!(
10690 editor.selections.display_ranges(cx),
10691 [
10692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10693 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10694 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10695 ]
10696 );
10697
10698 editor.undo(&Undo, window, cx);
10699 assert_eq!(
10700 editor.text(cx),
10701 "
10702 a
10703 b
10704 c
10705 "
10706 .unindent()
10707 );
10708 assert_eq!(
10709 editor.selections.display_ranges(cx),
10710 [
10711 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10713 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10714 ]
10715 );
10716
10717 // Ensure inserting the last character of a multi-byte bracket pair
10718 // doesn't surround the selections with the bracket.
10719 editor.handle_input("*", window, cx);
10720 assert_eq!(
10721 editor.text(cx),
10722 "
10723 *
10724 *
10725 *
10726 "
10727 .unindent()
10728 );
10729 assert_eq!(
10730 editor.selections.display_ranges(cx),
10731 [
10732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10733 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10734 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10735 ]
10736 );
10737 });
10738}
10739
10740#[gpui::test]
10741async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10742 init_test(cx, |_| {});
10743
10744 let language = Arc::new(Language::new(
10745 LanguageConfig {
10746 brackets: BracketPairConfig {
10747 pairs: vec![BracketPair {
10748 start: "{".to_string(),
10749 end: "}".to_string(),
10750 close: true,
10751 surround: true,
10752 newline: true,
10753 }],
10754 ..Default::default()
10755 },
10756 autoclose_before: "}".to_string(),
10757 ..Default::default()
10758 },
10759 Some(tree_sitter_rust::LANGUAGE.into()),
10760 ));
10761
10762 let text = r#"
10763 a
10764 b
10765 c
10766 "#
10767 .unindent();
10768
10769 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10770 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10771 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10772 editor
10773 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10774 .await;
10775
10776 editor.update_in(cx, |editor, window, cx| {
10777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10778 s.select_ranges([
10779 Point::new(0, 1)..Point::new(0, 1),
10780 Point::new(1, 1)..Point::new(1, 1),
10781 Point::new(2, 1)..Point::new(2, 1),
10782 ])
10783 });
10784
10785 editor.handle_input("{", window, cx);
10786 editor.handle_input("{", window, cx);
10787 editor.handle_input("_", window, cx);
10788 assert_eq!(
10789 editor.text(cx),
10790 "
10791 a{{_}}
10792 b{{_}}
10793 c{{_}}
10794 "
10795 .unindent()
10796 );
10797 assert_eq!(
10798 editor
10799 .selections
10800 .ranges::<Point>(&editor.display_snapshot(cx)),
10801 [
10802 Point::new(0, 4)..Point::new(0, 4),
10803 Point::new(1, 4)..Point::new(1, 4),
10804 Point::new(2, 4)..Point::new(2, 4)
10805 ]
10806 );
10807
10808 editor.backspace(&Default::default(), window, cx);
10809 editor.backspace(&Default::default(), window, cx);
10810 assert_eq!(
10811 editor.text(cx),
10812 "
10813 a{}
10814 b{}
10815 c{}
10816 "
10817 .unindent()
10818 );
10819 assert_eq!(
10820 editor
10821 .selections
10822 .ranges::<Point>(&editor.display_snapshot(cx)),
10823 [
10824 Point::new(0, 2)..Point::new(0, 2),
10825 Point::new(1, 2)..Point::new(1, 2),
10826 Point::new(2, 2)..Point::new(2, 2)
10827 ]
10828 );
10829
10830 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10831 assert_eq!(
10832 editor.text(cx),
10833 "
10834 a
10835 b
10836 c
10837 "
10838 .unindent()
10839 );
10840 assert_eq!(
10841 editor
10842 .selections
10843 .ranges::<Point>(&editor.display_snapshot(cx)),
10844 [
10845 Point::new(0, 1)..Point::new(0, 1),
10846 Point::new(1, 1)..Point::new(1, 1),
10847 Point::new(2, 1)..Point::new(2, 1)
10848 ]
10849 );
10850 });
10851}
10852
10853#[gpui::test]
10854async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10855 init_test(cx, |settings| {
10856 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10857 });
10858
10859 let mut cx = EditorTestContext::new(cx).await;
10860
10861 let language = Arc::new(Language::new(
10862 LanguageConfig {
10863 brackets: BracketPairConfig {
10864 pairs: vec![
10865 BracketPair {
10866 start: "{".to_string(),
10867 end: "}".to_string(),
10868 close: true,
10869 surround: true,
10870 newline: true,
10871 },
10872 BracketPair {
10873 start: "(".to_string(),
10874 end: ")".to_string(),
10875 close: true,
10876 surround: true,
10877 newline: true,
10878 },
10879 BracketPair {
10880 start: "[".to_string(),
10881 end: "]".to_string(),
10882 close: false,
10883 surround: true,
10884 newline: true,
10885 },
10886 ],
10887 ..Default::default()
10888 },
10889 autoclose_before: "})]".to_string(),
10890 ..Default::default()
10891 },
10892 Some(tree_sitter_rust::LANGUAGE.into()),
10893 ));
10894
10895 cx.language_registry().add(language.clone());
10896 cx.update_buffer(|buffer, cx| {
10897 buffer.set_language(Some(language), cx);
10898 });
10899
10900 cx.set_state(
10901 &"
10902 {(ˇ)}
10903 [[ˇ]]
10904 {(ˇ)}
10905 "
10906 .unindent(),
10907 );
10908
10909 cx.update_editor(|editor, window, cx| {
10910 editor.backspace(&Default::default(), window, cx);
10911 editor.backspace(&Default::default(), window, cx);
10912 });
10913
10914 cx.assert_editor_state(
10915 &"
10916 ˇ
10917 ˇ]]
10918 ˇ
10919 "
10920 .unindent(),
10921 );
10922
10923 cx.update_editor(|editor, window, cx| {
10924 editor.handle_input("{", window, cx);
10925 editor.handle_input("{", window, cx);
10926 editor.move_right(&MoveRight, window, cx);
10927 editor.move_right(&MoveRight, window, cx);
10928 editor.move_left(&MoveLeft, window, cx);
10929 editor.move_left(&MoveLeft, window, cx);
10930 editor.backspace(&Default::default(), window, cx);
10931 });
10932
10933 cx.assert_editor_state(
10934 &"
10935 {ˇ}
10936 {ˇ}]]
10937 {ˇ}
10938 "
10939 .unindent(),
10940 );
10941
10942 cx.update_editor(|editor, window, cx| {
10943 editor.backspace(&Default::default(), window, cx);
10944 });
10945
10946 cx.assert_editor_state(
10947 &"
10948 ˇ
10949 ˇ]]
10950 ˇ
10951 "
10952 .unindent(),
10953 );
10954}
10955
10956#[gpui::test]
10957async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10958 init_test(cx, |_| {});
10959
10960 let language = Arc::new(Language::new(
10961 LanguageConfig::default(),
10962 Some(tree_sitter_rust::LANGUAGE.into()),
10963 ));
10964
10965 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10968 editor
10969 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10970 .await;
10971
10972 editor.update_in(cx, |editor, window, cx| {
10973 editor.set_auto_replace_emoji_shortcode(true);
10974
10975 editor.handle_input("Hello ", window, cx);
10976 editor.handle_input(":wave", window, cx);
10977 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10978
10979 editor.handle_input(":", window, cx);
10980 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10981
10982 editor.handle_input(" :smile", window, cx);
10983 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10984
10985 editor.handle_input(":", window, cx);
10986 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10987
10988 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10989 editor.handle_input(":wave", window, cx);
10990 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10991
10992 editor.handle_input(":", window, cx);
10993 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10994
10995 editor.handle_input(":1", window, cx);
10996 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10997
10998 editor.handle_input(":", window, cx);
10999 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11000
11001 // Ensure shortcode does not get replaced when it is part of a word
11002 editor.handle_input(" Test:wave", window, cx);
11003 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11004
11005 editor.handle_input(":", window, cx);
11006 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11007
11008 editor.set_auto_replace_emoji_shortcode(false);
11009
11010 // Ensure shortcode does not get replaced when auto replace is off
11011 editor.handle_input(" :wave", window, cx);
11012 assert_eq!(
11013 editor.text(cx),
11014 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11015 );
11016
11017 editor.handle_input(":", window, cx);
11018 assert_eq!(
11019 editor.text(cx),
11020 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11021 );
11022 });
11023}
11024
11025#[gpui::test]
11026async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11027 init_test(cx, |_| {});
11028
11029 let (text, insertion_ranges) = marked_text_ranges(
11030 indoc! {"
11031 ˇ
11032 "},
11033 false,
11034 );
11035
11036 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11037 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11038
11039 _ = editor.update_in(cx, |editor, window, cx| {
11040 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11041
11042 editor
11043 .insert_snippet(&insertion_ranges, snippet, window, cx)
11044 .unwrap();
11045
11046 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11047 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11048 assert_eq!(editor.text(cx), expected_text);
11049 assert_eq!(
11050 editor
11051 .selections
11052 .ranges::<usize>(&editor.display_snapshot(cx)),
11053 selection_ranges
11054 );
11055 }
11056
11057 assert(
11058 editor,
11059 cx,
11060 indoc! {"
11061 type «» =•
11062 "},
11063 );
11064
11065 assert!(editor.context_menu_visible(), "There should be a matches");
11066 });
11067}
11068
11069#[gpui::test]
11070async fn test_snippets(cx: &mut TestAppContext) {
11071 init_test(cx, |_| {});
11072
11073 let mut cx = EditorTestContext::new(cx).await;
11074
11075 cx.set_state(indoc! {"
11076 a.ˇ b
11077 a.ˇ b
11078 a.ˇ b
11079 "});
11080
11081 cx.update_editor(|editor, window, cx| {
11082 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11083 let insertion_ranges = editor
11084 .selections
11085 .all(&editor.display_snapshot(cx))
11086 .iter()
11087 .map(|s| s.range())
11088 .collect::<Vec<_>>();
11089 editor
11090 .insert_snippet(&insertion_ranges, snippet, window, cx)
11091 .unwrap();
11092 });
11093
11094 cx.assert_editor_state(indoc! {"
11095 a.f(«oneˇ», two, «threeˇ») b
11096 a.f(«oneˇ», two, «threeˇ») b
11097 a.f(«oneˇ», two, «threeˇ») b
11098 "});
11099
11100 // Can't move earlier than the first tab stop
11101 cx.update_editor(|editor, window, cx| {
11102 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11103 });
11104 cx.assert_editor_state(indoc! {"
11105 a.f(«oneˇ», two, «threeˇ») b
11106 a.f(«oneˇ», two, «threeˇ») b
11107 a.f(«oneˇ», two, «threeˇ») b
11108 "});
11109
11110 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11111 cx.assert_editor_state(indoc! {"
11112 a.f(one, «twoˇ», three) b
11113 a.f(one, «twoˇ», three) b
11114 a.f(one, «twoˇ», three) b
11115 "});
11116
11117 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11118 cx.assert_editor_state(indoc! {"
11119 a.f(«oneˇ», two, «threeˇ») b
11120 a.f(«oneˇ», two, «threeˇ») b
11121 a.f(«oneˇ», two, «threeˇ») b
11122 "});
11123
11124 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11125 cx.assert_editor_state(indoc! {"
11126 a.f(one, «twoˇ», three) b
11127 a.f(one, «twoˇ», three) b
11128 a.f(one, «twoˇ», three) b
11129 "});
11130 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11131 cx.assert_editor_state(indoc! {"
11132 a.f(one, two, three)ˇ b
11133 a.f(one, two, three)ˇ b
11134 a.f(one, two, three)ˇ b
11135 "});
11136
11137 // As soon as the last tab stop is reached, snippet state is gone
11138 cx.update_editor(|editor, window, cx| {
11139 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11140 });
11141 cx.assert_editor_state(indoc! {"
11142 a.f(one, two, three)ˇ b
11143 a.f(one, two, three)ˇ b
11144 a.f(one, two, three)ˇ b
11145 "});
11146}
11147
11148#[gpui::test]
11149async fn test_snippet_indentation(cx: &mut TestAppContext) {
11150 init_test(cx, |_| {});
11151
11152 let mut cx = EditorTestContext::new(cx).await;
11153
11154 cx.update_editor(|editor, window, cx| {
11155 let snippet = Snippet::parse(indoc! {"
11156 /*
11157 * Multiline comment with leading indentation
11158 *
11159 * $1
11160 */
11161 $0"})
11162 .unwrap();
11163 let insertion_ranges = editor
11164 .selections
11165 .all(&editor.display_snapshot(cx))
11166 .iter()
11167 .map(|s| s.range())
11168 .collect::<Vec<_>>();
11169 editor
11170 .insert_snippet(&insertion_ranges, snippet, window, cx)
11171 .unwrap();
11172 });
11173
11174 cx.assert_editor_state(indoc! {"
11175 /*
11176 * Multiline comment with leading indentation
11177 *
11178 * ˇ
11179 */
11180 "});
11181
11182 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11183 cx.assert_editor_state(indoc! {"
11184 /*
11185 * Multiline comment with leading indentation
11186 *
11187 *•
11188 */
11189 ˇ"});
11190}
11191
11192#[gpui::test]
11193async fn test_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 cx.run_until_parked();
12619 // Set up a buffer white some trailing whitespace and no trailing newline.
12620 cx.set_state(
12621 &[
12622 "one ", //
12623 "twoˇ", //
12624 "three ", //
12625 "four", //
12626 ]
12627 .join("\n"),
12628 );
12629
12630 // Record which buffer changes have been sent to the language server
12631 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12632 cx.lsp
12633 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12634 let buffer_changes = buffer_changes.clone();
12635 move |params, _| {
12636 buffer_changes.lock().extend(
12637 params
12638 .content_changes
12639 .into_iter()
12640 .map(|e| (e.range.unwrap(), e.text)),
12641 );
12642 }
12643 });
12644 cx.run_until_parked();
12645
12646 // Handle formatting requests to the language server.
12647 cx.lsp
12648 .set_request_handler::<lsp::request::Formatting, _, _>({
12649 let buffer_changes = buffer_changes.clone();
12650 move |_, _| {
12651 let buffer_changes = buffer_changes.clone();
12652 // Insert blank lines between each line of the buffer.
12653 async move {
12654 // When formatting is requested, trailing whitespace has already been stripped,
12655 // and the trailing newline has already been added.
12656 assert_eq!(
12657 &buffer_changes.lock()[1..],
12658 &[
12659 (
12660 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12661 "".into()
12662 ),
12663 (
12664 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12665 "".into()
12666 ),
12667 (
12668 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12669 "\n".into()
12670 ),
12671 ]
12672 );
12673
12674 Ok(Some(vec![
12675 lsp::TextEdit {
12676 range: lsp::Range::new(
12677 lsp::Position::new(1, 0),
12678 lsp::Position::new(1, 0),
12679 ),
12680 new_text: "\n".into(),
12681 },
12682 lsp::TextEdit {
12683 range: lsp::Range::new(
12684 lsp::Position::new(2, 0),
12685 lsp::Position::new(2, 0),
12686 ),
12687 new_text: "\n".into(),
12688 },
12689 ]))
12690 }
12691 }
12692 });
12693
12694 // Submit a format request.
12695 let format = cx
12696 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12697 .unwrap();
12698
12699 cx.run_until_parked();
12700 // After formatting the buffer, the trailing whitespace is stripped,
12701 // a newline is appended, and the edits provided by the language server
12702 // have been applied.
12703 format.await.unwrap();
12704
12705 cx.assert_editor_state(
12706 &[
12707 "one", //
12708 "", //
12709 "twoˇ", //
12710 "", //
12711 "three", //
12712 "four", //
12713 "", //
12714 ]
12715 .join("\n"),
12716 );
12717
12718 // Undoing the formatting undoes the trailing whitespace removal, the
12719 // trailing newline, and the LSP edits.
12720 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12721 cx.assert_editor_state(
12722 &[
12723 "one ", //
12724 "twoˇ", //
12725 "three ", //
12726 "four", //
12727 ]
12728 .join("\n"),
12729 );
12730}
12731
12732#[gpui::test]
12733async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12734 cx: &mut TestAppContext,
12735) {
12736 init_test(cx, |_| {});
12737
12738 cx.update(|cx| {
12739 cx.update_global::<SettingsStore, _>(|settings, cx| {
12740 settings.update_user_settings(cx, |settings| {
12741 settings.editor.auto_signature_help = Some(true);
12742 });
12743 });
12744 });
12745
12746 let mut cx = EditorLspTestContext::new_rust(
12747 lsp::ServerCapabilities {
12748 signature_help_provider: Some(lsp::SignatureHelpOptions {
12749 ..Default::default()
12750 }),
12751 ..Default::default()
12752 },
12753 cx,
12754 )
12755 .await;
12756
12757 let language = Language::new(
12758 LanguageConfig {
12759 name: "Rust".into(),
12760 brackets: BracketPairConfig {
12761 pairs: vec![
12762 BracketPair {
12763 start: "{".to_string(),
12764 end: "}".to_string(),
12765 close: true,
12766 surround: true,
12767 newline: true,
12768 },
12769 BracketPair {
12770 start: "(".to_string(),
12771 end: ")".to_string(),
12772 close: true,
12773 surround: true,
12774 newline: true,
12775 },
12776 BracketPair {
12777 start: "/*".to_string(),
12778 end: " */".to_string(),
12779 close: true,
12780 surround: true,
12781 newline: true,
12782 },
12783 BracketPair {
12784 start: "[".to_string(),
12785 end: "]".to_string(),
12786 close: false,
12787 surround: false,
12788 newline: true,
12789 },
12790 BracketPair {
12791 start: "\"".to_string(),
12792 end: "\"".to_string(),
12793 close: true,
12794 surround: true,
12795 newline: false,
12796 },
12797 BracketPair {
12798 start: "<".to_string(),
12799 end: ">".to_string(),
12800 close: false,
12801 surround: true,
12802 newline: true,
12803 },
12804 ],
12805 ..Default::default()
12806 },
12807 autoclose_before: "})]".to_string(),
12808 ..Default::default()
12809 },
12810 Some(tree_sitter_rust::LANGUAGE.into()),
12811 );
12812 let language = Arc::new(language);
12813
12814 cx.language_registry().add(language.clone());
12815 cx.update_buffer(|buffer, cx| {
12816 buffer.set_language(Some(language), cx);
12817 });
12818
12819 cx.set_state(
12820 &r#"
12821 fn main() {
12822 sampleˇ
12823 }
12824 "#
12825 .unindent(),
12826 );
12827
12828 cx.update_editor(|editor, window, cx| {
12829 editor.handle_input("(", window, cx);
12830 });
12831 cx.assert_editor_state(
12832 &"
12833 fn main() {
12834 sample(ˇ)
12835 }
12836 "
12837 .unindent(),
12838 );
12839
12840 let mocked_response = lsp::SignatureHelp {
12841 signatures: vec![lsp::SignatureInformation {
12842 label: "fn sample(param1: u8, param2: u8)".to_string(),
12843 documentation: None,
12844 parameters: Some(vec![
12845 lsp::ParameterInformation {
12846 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12847 documentation: None,
12848 },
12849 lsp::ParameterInformation {
12850 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12851 documentation: None,
12852 },
12853 ]),
12854 active_parameter: None,
12855 }],
12856 active_signature: Some(0),
12857 active_parameter: Some(0),
12858 };
12859 handle_signature_help_request(&mut cx, mocked_response).await;
12860
12861 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12862 .await;
12863
12864 cx.editor(|editor, _, _| {
12865 let signature_help_state = editor.signature_help_state.popover().cloned();
12866 let signature = signature_help_state.unwrap();
12867 assert_eq!(
12868 signature.signatures[signature.current_signature].label,
12869 "fn sample(param1: u8, param2: u8)"
12870 );
12871 });
12872}
12873
12874#[gpui::test]
12875async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12876 init_test(cx, |_| {});
12877
12878 cx.update(|cx| {
12879 cx.update_global::<SettingsStore, _>(|settings, cx| {
12880 settings.update_user_settings(cx, |settings| {
12881 settings.editor.auto_signature_help = Some(false);
12882 settings.editor.show_signature_help_after_edits = Some(false);
12883 });
12884 });
12885 });
12886
12887 let mut cx = EditorLspTestContext::new_rust(
12888 lsp::ServerCapabilities {
12889 signature_help_provider: Some(lsp::SignatureHelpOptions {
12890 ..Default::default()
12891 }),
12892 ..Default::default()
12893 },
12894 cx,
12895 )
12896 .await;
12897
12898 let language = Language::new(
12899 LanguageConfig {
12900 name: "Rust".into(),
12901 brackets: BracketPairConfig {
12902 pairs: vec![
12903 BracketPair {
12904 start: "{".to_string(),
12905 end: "}".to_string(),
12906 close: true,
12907 surround: true,
12908 newline: true,
12909 },
12910 BracketPair {
12911 start: "(".to_string(),
12912 end: ")".to_string(),
12913 close: true,
12914 surround: true,
12915 newline: true,
12916 },
12917 BracketPair {
12918 start: "/*".to_string(),
12919 end: " */".to_string(),
12920 close: true,
12921 surround: true,
12922 newline: true,
12923 },
12924 BracketPair {
12925 start: "[".to_string(),
12926 end: "]".to_string(),
12927 close: false,
12928 surround: false,
12929 newline: true,
12930 },
12931 BracketPair {
12932 start: "\"".to_string(),
12933 end: "\"".to_string(),
12934 close: true,
12935 surround: true,
12936 newline: false,
12937 },
12938 BracketPair {
12939 start: "<".to_string(),
12940 end: ">".to_string(),
12941 close: false,
12942 surround: true,
12943 newline: true,
12944 },
12945 ],
12946 ..Default::default()
12947 },
12948 autoclose_before: "})]".to_string(),
12949 ..Default::default()
12950 },
12951 Some(tree_sitter_rust::LANGUAGE.into()),
12952 );
12953 let language = Arc::new(language);
12954
12955 cx.language_registry().add(language.clone());
12956 cx.update_buffer(|buffer, cx| {
12957 buffer.set_language(Some(language), cx);
12958 });
12959
12960 // Ensure that signature_help is not called when no signature help is enabled.
12961 cx.set_state(
12962 &r#"
12963 fn main() {
12964 sampleˇ
12965 }
12966 "#
12967 .unindent(),
12968 );
12969 cx.update_editor(|editor, window, cx| {
12970 editor.handle_input("(", window, cx);
12971 });
12972 cx.assert_editor_state(
12973 &"
12974 fn main() {
12975 sample(ˇ)
12976 }
12977 "
12978 .unindent(),
12979 );
12980 cx.editor(|editor, _, _| {
12981 assert!(editor.signature_help_state.task().is_none());
12982 });
12983
12984 let mocked_response = lsp::SignatureHelp {
12985 signatures: vec![lsp::SignatureInformation {
12986 label: "fn sample(param1: u8, param2: u8)".to_string(),
12987 documentation: None,
12988 parameters: Some(vec![
12989 lsp::ParameterInformation {
12990 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12991 documentation: None,
12992 },
12993 lsp::ParameterInformation {
12994 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12995 documentation: None,
12996 },
12997 ]),
12998 active_parameter: None,
12999 }],
13000 active_signature: Some(0),
13001 active_parameter: Some(0),
13002 };
13003
13004 // Ensure that signature_help is called when enabled afte edits
13005 cx.update(|_, cx| {
13006 cx.update_global::<SettingsStore, _>(|settings, cx| {
13007 settings.update_user_settings(cx, |settings| {
13008 settings.editor.auto_signature_help = Some(false);
13009 settings.editor.show_signature_help_after_edits = Some(true);
13010 });
13011 });
13012 });
13013 cx.set_state(
13014 &r#"
13015 fn main() {
13016 sampleˇ
13017 }
13018 "#
13019 .unindent(),
13020 );
13021 cx.update_editor(|editor, window, cx| {
13022 editor.handle_input("(", window, cx);
13023 });
13024 cx.assert_editor_state(
13025 &"
13026 fn main() {
13027 sample(ˇ)
13028 }
13029 "
13030 .unindent(),
13031 );
13032 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13033 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13034 .await;
13035 cx.update_editor(|editor, _, _| {
13036 let signature_help_state = editor.signature_help_state.popover().cloned();
13037 assert!(signature_help_state.is_some());
13038 let signature = signature_help_state.unwrap();
13039 assert_eq!(
13040 signature.signatures[signature.current_signature].label,
13041 "fn sample(param1: u8, param2: u8)"
13042 );
13043 editor.signature_help_state = SignatureHelpState::default();
13044 });
13045
13046 // Ensure that signature_help is called when auto signature help override is enabled
13047 cx.update(|_, cx| {
13048 cx.update_global::<SettingsStore, _>(|settings, cx| {
13049 settings.update_user_settings(cx, |settings| {
13050 settings.editor.auto_signature_help = Some(true);
13051 settings.editor.show_signature_help_after_edits = Some(false);
13052 });
13053 });
13054 });
13055 cx.set_state(
13056 &r#"
13057 fn main() {
13058 sampleˇ
13059 }
13060 "#
13061 .unindent(),
13062 );
13063 cx.update_editor(|editor, window, cx| {
13064 editor.handle_input("(", window, cx);
13065 });
13066 cx.assert_editor_state(
13067 &"
13068 fn main() {
13069 sample(ˇ)
13070 }
13071 "
13072 .unindent(),
13073 );
13074 handle_signature_help_request(&mut cx, mocked_response).await;
13075 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13076 .await;
13077 cx.editor(|editor, _, _| {
13078 let signature_help_state = editor.signature_help_state.popover().cloned();
13079 assert!(signature_help_state.is_some());
13080 let signature = signature_help_state.unwrap();
13081 assert_eq!(
13082 signature.signatures[signature.current_signature].label,
13083 "fn sample(param1: u8, param2: u8)"
13084 );
13085 });
13086}
13087
13088#[gpui::test]
13089async fn test_signature_help(cx: &mut TestAppContext) {
13090 init_test(cx, |_| {});
13091 cx.update(|cx| {
13092 cx.update_global::<SettingsStore, _>(|settings, cx| {
13093 settings.update_user_settings(cx, |settings| {
13094 settings.editor.auto_signature_help = Some(true);
13095 });
13096 });
13097 });
13098
13099 let mut cx = EditorLspTestContext::new_rust(
13100 lsp::ServerCapabilities {
13101 signature_help_provider: Some(lsp::SignatureHelpOptions {
13102 ..Default::default()
13103 }),
13104 ..Default::default()
13105 },
13106 cx,
13107 )
13108 .await;
13109
13110 // A test that directly calls `show_signature_help`
13111 cx.update_editor(|editor, window, cx| {
13112 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13113 });
13114
13115 let mocked_response = lsp::SignatureHelp {
13116 signatures: vec![lsp::SignatureInformation {
13117 label: "fn sample(param1: u8, param2: u8)".to_string(),
13118 documentation: None,
13119 parameters: Some(vec![
13120 lsp::ParameterInformation {
13121 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13122 documentation: None,
13123 },
13124 lsp::ParameterInformation {
13125 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13126 documentation: None,
13127 },
13128 ]),
13129 active_parameter: None,
13130 }],
13131 active_signature: Some(0),
13132 active_parameter: Some(0),
13133 };
13134 handle_signature_help_request(&mut cx, mocked_response).await;
13135
13136 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13137 .await;
13138
13139 cx.editor(|editor, _, _| {
13140 let signature_help_state = editor.signature_help_state.popover().cloned();
13141 assert!(signature_help_state.is_some());
13142 let signature = signature_help_state.unwrap();
13143 assert_eq!(
13144 signature.signatures[signature.current_signature].label,
13145 "fn sample(param1: u8, param2: u8)"
13146 );
13147 });
13148
13149 // When exiting outside from inside the brackets, `signature_help` is closed.
13150 cx.set_state(indoc! {"
13151 fn main() {
13152 sample(ˇ);
13153 }
13154
13155 fn sample(param1: u8, param2: u8) {}
13156 "});
13157
13158 cx.update_editor(|editor, window, cx| {
13159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13160 s.select_ranges([0..0])
13161 });
13162 });
13163
13164 let mocked_response = lsp::SignatureHelp {
13165 signatures: Vec::new(),
13166 active_signature: None,
13167 active_parameter: None,
13168 };
13169 handle_signature_help_request(&mut cx, mocked_response).await;
13170
13171 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13172 .await;
13173
13174 cx.editor(|editor, _, _| {
13175 assert!(!editor.signature_help_state.is_shown());
13176 });
13177
13178 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13179 cx.set_state(indoc! {"
13180 fn main() {
13181 sample(ˇ);
13182 }
13183
13184 fn sample(param1: u8, param2: u8) {}
13185 "});
13186
13187 let mocked_response = lsp::SignatureHelp {
13188 signatures: vec![lsp::SignatureInformation {
13189 label: "fn sample(param1: u8, param2: u8)".to_string(),
13190 documentation: None,
13191 parameters: Some(vec![
13192 lsp::ParameterInformation {
13193 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13194 documentation: None,
13195 },
13196 lsp::ParameterInformation {
13197 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13198 documentation: None,
13199 },
13200 ]),
13201 active_parameter: None,
13202 }],
13203 active_signature: Some(0),
13204 active_parameter: Some(0),
13205 };
13206 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13207 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13208 .await;
13209 cx.editor(|editor, _, _| {
13210 assert!(editor.signature_help_state.is_shown());
13211 });
13212
13213 // Restore the popover with more parameter input
13214 cx.set_state(indoc! {"
13215 fn main() {
13216 sample(param1, param2ˇ);
13217 }
13218
13219 fn sample(param1: u8, param2: u8) {}
13220 "});
13221
13222 let mocked_response = lsp::SignatureHelp {
13223 signatures: vec![lsp::SignatureInformation {
13224 label: "fn sample(param1: u8, param2: u8)".to_string(),
13225 documentation: None,
13226 parameters: Some(vec![
13227 lsp::ParameterInformation {
13228 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13229 documentation: None,
13230 },
13231 lsp::ParameterInformation {
13232 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13233 documentation: None,
13234 },
13235 ]),
13236 active_parameter: None,
13237 }],
13238 active_signature: Some(0),
13239 active_parameter: Some(1),
13240 };
13241 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13242 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13243 .await;
13244
13245 // When selecting a range, the popover is gone.
13246 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13247 cx.update_editor(|editor, window, cx| {
13248 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13249 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13250 })
13251 });
13252 cx.assert_editor_state(indoc! {"
13253 fn main() {
13254 sample(param1, «ˇparam2»);
13255 }
13256
13257 fn sample(param1: u8, param2: u8) {}
13258 "});
13259 cx.editor(|editor, _, _| {
13260 assert!(!editor.signature_help_state.is_shown());
13261 });
13262
13263 // When unselecting again, the popover is back if within the brackets.
13264 cx.update_editor(|editor, window, cx| {
13265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13266 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13267 })
13268 });
13269 cx.assert_editor_state(indoc! {"
13270 fn main() {
13271 sample(param1, ˇparam2);
13272 }
13273
13274 fn sample(param1: u8, param2: u8) {}
13275 "});
13276 handle_signature_help_request(&mut cx, mocked_response).await;
13277 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13278 .await;
13279 cx.editor(|editor, _, _| {
13280 assert!(editor.signature_help_state.is_shown());
13281 });
13282
13283 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13284 cx.update_editor(|editor, window, cx| {
13285 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13286 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13287 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13288 })
13289 });
13290 cx.assert_editor_state(indoc! {"
13291 fn main() {
13292 sample(param1, ˇparam2);
13293 }
13294
13295 fn sample(param1: u8, param2: u8) {}
13296 "});
13297
13298 let mocked_response = lsp::SignatureHelp {
13299 signatures: vec![lsp::SignatureInformation {
13300 label: "fn sample(param1: u8, param2: u8)".to_string(),
13301 documentation: None,
13302 parameters: Some(vec![
13303 lsp::ParameterInformation {
13304 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13305 documentation: None,
13306 },
13307 lsp::ParameterInformation {
13308 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13309 documentation: None,
13310 },
13311 ]),
13312 active_parameter: None,
13313 }],
13314 active_signature: Some(0),
13315 active_parameter: Some(1),
13316 };
13317 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13318 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13319 .await;
13320 cx.update_editor(|editor, _, cx| {
13321 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13322 });
13323 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13324 .await;
13325 cx.update_editor(|editor, window, cx| {
13326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13327 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13328 })
13329 });
13330 cx.assert_editor_state(indoc! {"
13331 fn main() {
13332 sample(param1, «ˇparam2»);
13333 }
13334
13335 fn sample(param1: u8, param2: u8) {}
13336 "});
13337 cx.update_editor(|editor, window, cx| {
13338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13339 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13340 })
13341 });
13342 cx.assert_editor_state(indoc! {"
13343 fn main() {
13344 sample(param1, ˇparam2);
13345 }
13346
13347 fn sample(param1: u8, param2: u8) {}
13348 "});
13349 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13350 .await;
13351}
13352
13353#[gpui::test]
13354async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13355 init_test(cx, |_| {});
13356
13357 let mut cx = EditorLspTestContext::new_rust(
13358 lsp::ServerCapabilities {
13359 signature_help_provider: Some(lsp::SignatureHelpOptions {
13360 ..Default::default()
13361 }),
13362 ..Default::default()
13363 },
13364 cx,
13365 )
13366 .await;
13367
13368 cx.set_state(indoc! {"
13369 fn main() {
13370 overloadedˇ
13371 }
13372 "});
13373
13374 cx.update_editor(|editor, window, cx| {
13375 editor.handle_input("(", window, cx);
13376 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13377 });
13378
13379 // Mock response with 3 signatures
13380 let mocked_response = lsp::SignatureHelp {
13381 signatures: vec![
13382 lsp::SignatureInformation {
13383 label: "fn overloaded(x: i32)".to_string(),
13384 documentation: None,
13385 parameters: Some(vec![lsp::ParameterInformation {
13386 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13387 documentation: None,
13388 }]),
13389 active_parameter: None,
13390 },
13391 lsp::SignatureInformation {
13392 label: "fn overloaded(x: i32, y: i32)".to_string(),
13393 documentation: None,
13394 parameters: Some(vec![
13395 lsp::ParameterInformation {
13396 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13397 documentation: None,
13398 },
13399 lsp::ParameterInformation {
13400 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13401 documentation: None,
13402 },
13403 ]),
13404 active_parameter: None,
13405 },
13406 lsp::SignatureInformation {
13407 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13408 documentation: None,
13409 parameters: Some(vec![
13410 lsp::ParameterInformation {
13411 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13412 documentation: None,
13413 },
13414 lsp::ParameterInformation {
13415 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13416 documentation: None,
13417 },
13418 lsp::ParameterInformation {
13419 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13420 documentation: None,
13421 },
13422 ]),
13423 active_parameter: None,
13424 },
13425 ],
13426 active_signature: Some(1),
13427 active_parameter: Some(0),
13428 };
13429 handle_signature_help_request(&mut cx, mocked_response).await;
13430
13431 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13432 .await;
13433
13434 // Verify we have multiple signatures and the right one is selected
13435 cx.editor(|editor, _, _| {
13436 let popover = editor.signature_help_state.popover().cloned().unwrap();
13437 assert_eq!(popover.signatures.len(), 3);
13438 // active_signature was 1, so that should be the current
13439 assert_eq!(popover.current_signature, 1);
13440 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13441 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13442 assert_eq!(
13443 popover.signatures[2].label,
13444 "fn overloaded(x: i32, y: i32, z: i32)"
13445 );
13446 });
13447
13448 // Test navigation functionality
13449 cx.update_editor(|editor, window, cx| {
13450 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13451 });
13452
13453 cx.editor(|editor, _, _| {
13454 let popover = editor.signature_help_state.popover().cloned().unwrap();
13455 assert_eq!(popover.current_signature, 2);
13456 });
13457
13458 // Test wrap around
13459 cx.update_editor(|editor, window, cx| {
13460 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13461 });
13462
13463 cx.editor(|editor, _, _| {
13464 let popover = editor.signature_help_state.popover().cloned().unwrap();
13465 assert_eq!(popover.current_signature, 0);
13466 });
13467
13468 // Test previous navigation
13469 cx.update_editor(|editor, window, cx| {
13470 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13471 });
13472
13473 cx.editor(|editor, _, _| {
13474 let popover = editor.signature_help_state.popover().cloned().unwrap();
13475 assert_eq!(popover.current_signature, 2);
13476 });
13477}
13478
13479#[gpui::test]
13480async fn test_completion_mode(cx: &mut TestAppContext) {
13481 init_test(cx, |_| {});
13482 let mut cx = EditorLspTestContext::new_rust(
13483 lsp::ServerCapabilities {
13484 completion_provider: Some(lsp::CompletionOptions {
13485 resolve_provider: Some(true),
13486 ..Default::default()
13487 }),
13488 ..Default::default()
13489 },
13490 cx,
13491 )
13492 .await;
13493
13494 struct Run {
13495 run_description: &'static str,
13496 initial_state: String,
13497 buffer_marked_text: String,
13498 completion_label: &'static str,
13499 completion_text: &'static str,
13500 expected_with_insert_mode: String,
13501 expected_with_replace_mode: String,
13502 expected_with_replace_subsequence_mode: String,
13503 expected_with_replace_suffix_mode: String,
13504 }
13505
13506 let runs = [
13507 Run {
13508 run_description: "Start of word matches completion text",
13509 initial_state: "before ediˇ after".into(),
13510 buffer_marked_text: "before <edi|> after".into(),
13511 completion_label: "editor",
13512 completion_text: "editor",
13513 expected_with_insert_mode: "before editorˇ after".into(),
13514 expected_with_replace_mode: "before editorˇ after".into(),
13515 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13516 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13517 },
13518 Run {
13519 run_description: "Accept same text at the middle of the word",
13520 initial_state: "before ediˇtor after".into(),
13521 buffer_marked_text: "before <edi|tor> after".into(),
13522 completion_label: "editor",
13523 completion_text: "editor",
13524 expected_with_insert_mode: "before editorˇtor after".into(),
13525 expected_with_replace_mode: "before editorˇ after".into(),
13526 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13527 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13528 },
13529 Run {
13530 run_description: "End of word matches completion text -- cursor at end",
13531 initial_state: "before torˇ after".into(),
13532 buffer_marked_text: "before <tor|> after".into(),
13533 completion_label: "editor",
13534 completion_text: "editor",
13535 expected_with_insert_mode: "before editorˇ after".into(),
13536 expected_with_replace_mode: "before editorˇ after".into(),
13537 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13538 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13539 },
13540 Run {
13541 run_description: "End of word matches completion text -- cursor at start",
13542 initial_state: "before ˇtor after".into(),
13543 buffer_marked_text: "before <|tor> after".into(),
13544 completion_label: "editor",
13545 completion_text: "editor",
13546 expected_with_insert_mode: "before editorˇtor after".into(),
13547 expected_with_replace_mode: "before editorˇ after".into(),
13548 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13549 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13550 },
13551 Run {
13552 run_description: "Prepend text containing whitespace",
13553 initial_state: "pˇfield: bool".into(),
13554 buffer_marked_text: "<p|field>: bool".into(),
13555 completion_label: "pub ",
13556 completion_text: "pub ",
13557 expected_with_insert_mode: "pub ˇfield: bool".into(),
13558 expected_with_replace_mode: "pub ˇ: bool".into(),
13559 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13560 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13561 },
13562 Run {
13563 run_description: "Add element to start of list",
13564 initial_state: "[element_ˇelement_2]".into(),
13565 buffer_marked_text: "[<element_|element_2>]".into(),
13566 completion_label: "element_1",
13567 completion_text: "element_1",
13568 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13569 expected_with_replace_mode: "[element_1ˇ]".into(),
13570 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13571 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13572 },
13573 Run {
13574 run_description: "Add element to start of list -- first and second elements are equal",
13575 initial_state: "[elˇelement]".into(),
13576 buffer_marked_text: "[<el|element>]".into(),
13577 completion_label: "element",
13578 completion_text: "element",
13579 expected_with_insert_mode: "[elementˇelement]".into(),
13580 expected_with_replace_mode: "[elementˇ]".into(),
13581 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13582 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13583 },
13584 Run {
13585 run_description: "Ends with matching suffix",
13586 initial_state: "SubˇError".into(),
13587 buffer_marked_text: "<Sub|Error>".into(),
13588 completion_label: "SubscriptionError",
13589 completion_text: "SubscriptionError",
13590 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13591 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13592 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13593 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13594 },
13595 Run {
13596 run_description: "Suffix is a subsequence -- contiguous",
13597 initial_state: "SubˇErr".into(),
13598 buffer_marked_text: "<Sub|Err>".into(),
13599 completion_label: "SubscriptionError",
13600 completion_text: "SubscriptionError",
13601 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13602 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13603 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13604 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13605 },
13606 Run {
13607 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13608 initial_state: "Suˇscrirr".into(),
13609 buffer_marked_text: "<Su|scrirr>".into(),
13610 completion_label: "SubscriptionError",
13611 completion_text: "SubscriptionError",
13612 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13613 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13614 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13615 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13616 },
13617 Run {
13618 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13619 initial_state: "foo(indˇix)".into(),
13620 buffer_marked_text: "foo(<ind|ix>)".into(),
13621 completion_label: "node_index",
13622 completion_text: "node_index",
13623 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13624 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13625 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13626 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13627 },
13628 Run {
13629 run_description: "Replace range ends before cursor - should extend to cursor",
13630 initial_state: "before editˇo after".into(),
13631 buffer_marked_text: "before <{ed}>it|o after".into(),
13632 completion_label: "editor",
13633 completion_text: "editor",
13634 expected_with_insert_mode: "before editorˇo after".into(),
13635 expected_with_replace_mode: "before editorˇo after".into(),
13636 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13637 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13638 },
13639 Run {
13640 run_description: "Uses label for suffix matching",
13641 initial_state: "before ediˇtor after".into(),
13642 buffer_marked_text: "before <edi|tor> after".into(),
13643 completion_label: "editor",
13644 completion_text: "editor()",
13645 expected_with_insert_mode: "before editor()ˇtor after".into(),
13646 expected_with_replace_mode: "before editor()ˇ after".into(),
13647 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13648 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13649 },
13650 Run {
13651 run_description: "Case insensitive subsequence and suffix matching",
13652 initial_state: "before EDiˇtoR after".into(),
13653 buffer_marked_text: "before <EDi|toR> after".into(),
13654 completion_label: "editor",
13655 completion_text: "editor",
13656 expected_with_insert_mode: "before editorˇtoR after".into(),
13657 expected_with_replace_mode: "before editorˇ after".into(),
13658 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13659 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13660 },
13661 ];
13662
13663 for run in runs {
13664 let run_variations = [
13665 (LspInsertMode::Insert, run.expected_with_insert_mode),
13666 (LspInsertMode::Replace, run.expected_with_replace_mode),
13667 (
13668 LspInsertMode::ReplaceSubsequence,
13669 run.expected_with_replace_subsequence_mode,
13670 ),
13671 (
13672 LspInsertMode::ReplaceSuffix,
13673 run.expected_with_replace_suffix_mode,
13674 ),
13675 ];
13676
13677 for (lsp_insert_mode, expected_text) in run_variations {
13678 eprintln!(
13679 "run = {:?}, mode = {lsp_insert_mode:.?}",
13680 run.run_description,
13681 );
13682
13683 update_test_language_settings(&mut cx, |settings| {
13684 settings.defaults.completions = Some(CompletionSettingsContent {
13685 lsp_insert_mode: Some(lsp_insert_mode),
13686 words: Some(WordsCompletionMode::Disabled),
13687 words_min_length: Some(0),
13688 ..Default::default()
13689 });
13690 });
13691
13692 cx.set_state(&run.initial_state);
13693 cx.update_editor(|editor, window, cx| {
13694 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13695 });
13696
13697 let counter = Arc::new(AtomicUsize::new(0));
13698 handle_completion_request_with_insert_and_replace(
13699 &mut cx,
13700 &run.buffer_marked_text,
13701 vec![(run.completion_label, run.completion_text)],
13702 counter.clone(),
13703 )
13704 .await;
13705 cx.condition(|editor, _| editor.context_menu_visible())
13706 .await;
13707 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13708
13709 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13710 editor
13711 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13712 .unwrap()
13713 });
13714 cx.assert_editor_state(&expected_text);
13715 handle_resolve_completion_request(&mut cx, None).await;
13716 apply_additional_edits.await.unwrap();
13717 }
13718 }
13719}
13720
13721#[gpui::test]
13722async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13723 init_test(cx, |_| {});
13724 let mut cx = EditorLspTestContext::new_rust(
13725 lsp::ServerCapabilities {
13726 completion_provider: Some(lsp::CompletionOptions {
13727 resolve_provider: Some(true),
13728 ..Default::default()
13729 }),
13730 ..Default::default()
13731 },
13732 cx,
13733 )
13734 .await;
13735
13736 let initial_state = "SubˇError";
13737 let buffer_marked_text = "<Sub|Error>";
13738 let completion_text = "SubscriptionError";
13739 let expected_with_insert_mode = "SubscriptionErrorˇError";
13740 let expected_with_replace_mode = "SubscriptionErrorˇ";
13741
13742 update_test_language_settings(&mut cx, |settings| {
13743 settings.defaults.completions = Some(CompletionSettingsContent {
13744 words: Some(WordsCompletionMode::Disabled),
13745 words_min_length: Some(0),
13746 // set the opposite here to ensure that the action is overriding the default behavior
13747 lsp_insert_mode: Some(LspInsertMode::Insert),
13748 ..Default::default()
13749 });
13750 });
13751
13752 cx.set_state(initial_state);
13753 cx.update_editor(|editor, window, cx| {
13754 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13755 });
13756
13757 let counter = Arc::new(AtomicUsize::new(0));
13758 handle_completion_request_with_insert_and_replace(
13759 &mut cx,
13760 buffer_marked_text,
13761 vec![(completion_text, completion_text)],
13762 counter.clone(),
13763 )
13764 .await;
13765 cx.condition(|editor, _| editor.context_menu_visible())
13766 .await;
13767 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13768
13769 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13770 editor
13771 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13772 .unwrap()
13773 });
13774 cx.assert_editor_state(expected_with_replace_mode);
13775 handle_resolve_completion_request(&mut cx, None).await;
13776 apply_additional_edits.await.unwrap();
13777
13778 update_test_language_settings(&mut cx, |settings| {
13779 settings.defaults.completions = Some(CompletionSettingsContent {
13780 words: Some(WordsCompletionMode::Disabled),
13781 words_min_length: Some(0),
13782 // set the opposite here to ensure that the action is overriding the default behavior
13783 lsp_insert_mode: Some(LspInsertMode::Replace),
13784 ..Default::default()
13785 });
13786 });
13787
13788 cx.set_state(initial_state);
13789 cx.update_editor(|editor, window, cx| {
13790 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13791 });
13792 handle_completion_request_with_insert_and_replace(
13793 &mut cx,
13794 buffer_marked_text,
13795 vec![(completion_text, completion_text)],
13796 counter.clone(),
13797 )
13798 .await;
13799 cx.condition(|editor, _| editor.context_menu_visible())
13800 .await;
13801 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13802
13803 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13804 editor
13805 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13806 .unwrap()
13807 });
13808 cx.assert_editor_state(expected_with_insert_mode);
13809 handle_resolve_completion_request(&mut cx, None).await;
13810 apply_additional_edits.await.unwrap();
13811}
13812
13813#[gpui::test]
13814async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13815 init_test(cx, |_| {});
13816 let mut cx = EditorLspTestContext::new_rust(
13817 lsp::ServerCapabilities {
13818 completion_provider: Some(lsp::CompletionOptions {
13819 resolve_provider: Some(true),
13820 ..Default::default()
13821 }),
13822 ..Default::default()
13823 },
13824 cx,
13825 )
13826 .await;
13827
13828 // scenario: surrounding text matches completion text
13829 let completion_text = "to_offset";
13830 let initial_state = indoc! {"
13831 1. buf.to_offˇsuffix
13832 2. buf.to_offˇsuf
13833 3. buf.to_offˇfix
13834 4. buf.to_offˇ
13835 5. into_offˇensive
13836 6. ˇsuffix
13837 7. let ˇ //
13838 8. aaˇzz
13839 9. buf.to_off«zzzzzˇ»suffix
13840 10. buf.«ˇzzzzz»suffix
13841 11. to_off«ˇzzzzz»
13842
13843 buf.to_offˇsuffix // newest cursor
13844 "};
13845 let completion_marked_buffer = indoc! {"
13846 1. buf.to_offsuffix
13847 2. buf.to_offsuf
13848 3. buf.to_offfix
13849 4. buf.to_off
13850 5. into_offensive
13851 6. suffix
13852 7. let //
13853 8. aazz
13854 9. buf.to_offzzzzzsuffix
13855 10. buf.zzzzzsuffix
13856 11. to_offzzzzz
13857
13858 buf.<to_off|suffix> // newest cursor
13859 "};
13860 let expected = indoc! {"
13861 1. buf.to_offsetˇ
13862 2. buf.to_offsetˇsuf
13863 3. buf.to_offsetˇfix
13864 4. buf.to_offsetˇ
13865 5. into_offsetˇensive
13866 6. to_offsetˇsuffix
13867 7. let to_offsetˇ //
13868 8. aato_offsetˇzz
13869 9. buf.to_offsetˇ
13870 10. buf.to_offsetˇsuffix
13871 11. to_offsetˇ
13872
13873 buf.to_offsetˇ // newest cursor
13874 "};
13875 cx.set_state(initial_state);
13876 cx.update_editor(|editor, window, cx| {
13877 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13878 });
13879 handle_completion_request_with_insert_and_replace(
13880 &mut cx,
13881 completion_marked_buffer,
13882 vec![(completion_text, completion_text)],
13883 Arc::new(AtomicUsize::new(0)),
13884 )
13885 .await;
13886 cx.condition(|editor, _| editor.context_menu_visible())
13887 .await;
13888 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13889 editor
13890 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13891 .unwrap()
13892 });
13893 cx.assert_editor_state(expected);
13894 handle_resolve_completion_request(&mut cx, None).await;
13895 apply_additional_edits.await.unwrap();
13896
13897 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13898 let completion_text = "foo_and_bar";
13899 let initial_state = indoc! {"
13900 1. ooanbˇ
13901 2. zooanbˇ
13902 3. ooanbˇz
13903 4. zooanbˇz
13904 5. ooanˇ
13905 6. oanbˇ
13906
13907 ooanbˇ
13908 "};
13909 let completion_marked_buffer = indoc! {"
13910 1. ooanb
13911 2. zooanb
13912 3. ooanbz
13913 4. zooanbz
13914 5. ooan
13915 6. oanb
13916
13917 <ooanb|>
13918 "};
13919 let expected = indoc! {"
13920 1. foo_and_barˇ
13921 2. zfoo_and_barˇ
13922 3. foo_and_barˇz
13923 4. zfoo_and_barˇz
13924 5. ooanfoo_and_barˇ
13925 6. oanbfoo_and_barˇ
13926
13927 foo_and_barˇ
13928 "};
13929 cx.set_state(initial_state);
13930 cx.update_editor(|editor, window, cx| {
13931 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13932 });
13933 handle_completion_request_with_insert_and_replace(
13934 &mut cx,
13935 completion_marked_buffer,
13936 vec![(completion_text, completion_text)],
13937 Arc::new(AtomicUsize::new(0)),
13938 )
13939 .await;
13940 cx.condition(|editor, _| editor.context_menu_visible())
13941 .await;
13942 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13943 editor
13944 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13945 .unwrap()
13946 });
13947 cx.assert_editor_state(expected);
13948 handle_resolve_completion_request(&mut cx, None).await;
13949 apply_additional_edits.await.unwrap();
13950
13951 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13952 // (expects the same as if it was inserted at the end)
13953 let completion_text = "foo_and_bar";
13954 let initial_state = indoc! {"
13955 1. ooˇanb
13956 2. zooˇanb
13957 3. ooˇanbz
13958 4. zooˇanbz
13959
13960 ooˇanb
13961 "};
13962 let completion_marked_buffer = indoc! {"
13963 1. ooanb
13964 2. zooanb
13965 3. ooanbz
13966 4. zooanbz
13967
13968 <oo|anb>
13969 "};
13970 let expected = indoc! {"
13971 1. foo_and_barˇ
13972 2. zfoo_and_barˇ
13973 3. foo_and_barˇz
13974 4. zfoo_and_barˇz
13975
13976 foo_and_barˇ
13977 "};
13978 cx.set_state(initial_state);
13979 cx.update_editor(|editor, window, cx| {
13980 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13981 });
13982 handle_completion_request_with_insert_and_replace(
13983 &mut cx,
13984 completion_marked_buffer,
13985 vec![(completion_text, completion_text)],
13986 Arc::new(AtomicUsize::new(0)),
13987 )
13988 .await;
13989 cx.condition(|editor, _| editor.context_menu_visible())
13990 .await;
13991 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13992 editor
13993 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13994 .unwrap()
13995 });
13996 cx.assert_editor_state(expected);
13997 handle_resolve_completion_request(&mut cx, None).await;
13998 apply_additional_edits.await.unwrap();
13999}
14000
14001// This used to crash
14002#[gpui::test]
14003async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14004 init_test(cx, |_| {});
14005
14006 let buffer_text = indoc! {"
14007 fn main() {
14008 10.satu;
14009
14010 //
14011 // separate cursors so they open in different excerpts (manually reproducible)
14012 //
14013
14014 10.satu20;
14015 }
14016 "};
14017 let multibuffer_text_with_selections = indoc! {"
14018 fn main() {
14019 10.satuˇ;
14020
14021 //
14022
14023 //
14024
14025 10.satuˇ20;
14026 }
14027 "};
14028 let expected_multibuffer = indoc! {"
14029 fn main() {
14030 10.saturating_sub()ˇ;
14031
14032 //
14033
14034 //
14035
14036 10.saturating_sub()ˇ;
14037 }
14038 "};
14039
14040 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14041 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14042
14043 let fs = FakeFs::new(cx.executor());
14044 fs.insert_tree(
14045 path!("/a"),
14046 json!({
14047 "main.rs": buffer_text,
14048 }),
14049 )
14050 .await;
14051
14052 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14053 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14054 language_registry.add(rust_lang());
14055 let mut fake_servers = language_registry.register_fake_lsp(
14056 "Rust",
14057 FakeLspAdapter {
14058 capabilities: lsp::ServerCapabilities {
14059 completion_provider: Some(lsp::CompletionOptions {
14060 resolve_provider: None,
14061 ..lsp::CompletionOptions::default()
14062 }),
14063 ..lsp::ServerCapabilities::default()
14064 },
14065 ..FakeLspAdapter::default()
14066 },
14067 );
14068 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14069 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14070 let buffer = project
14071 .update(cx, |project, cx| {
14072 project.open_local_buffer(path!("/a/main.rs"), cx)
14073 })
14074 .await
14075 .unwrap();
14076
14077 let multi_buffer = cx.new(|cx| {
14078 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14079 multi_buffer.push_excerpts(
14080 buffer.clone(),
14081 [ExcerptRange::new(0..first_excerpt_end)],
14082 cx,
14083 );
14084 multi_buffer.push_excerpts(
14085 buffer.clone(),
14086 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14087 cx,
14088 );
14089 multi_buffer
14090 });
14091
14092 let editor = workspace
14093 .update(cx, |_, window, cx| {
14094 cx.new(|cx| {
14095 Editor::new(
14096 EditorMode::Full {
14097 scale_ui_elements_with_buffer_font_size: false,
14098 show_active_line_background: false,
14099 sized_by_content: false,
14100 },
14101 multi_buffer.clone(),
14102 Some(project.clone()),
14103 window,
14104 cx,
14105 )
14106 })
14107 })
14108 .unwrap();
14109
14110 let pane = workspace
14111 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14112 .unwrap();
14113 pane.update_in(cx, |pane, window, cx| {
14114 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14115 });
14116
14117 let fake_server = fake_servers.next().await.unwrap();
14118
14119 editor.update_in(cx, |editor, window, cx| {
14120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14121 s.select_ranges([
14122 Point::new(1, 11)..Point::new(1, 11),
14123 Point::new(7, 11)..Point::new(7, 11),
14124 ])
14125 });
14126
14127 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14128 });
14129
14130 editor.update_in(cx, |editor, window, cx| {
14131 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14132 });
14133
14134 fake_server
14135 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14136 let completion_item = lsp::CompletionItem {
14137 label: "saturating_sub()".into(),
14138 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14139 lsp::InsertReplaceEdit {
14140 new_text: "saturating_sub()".to_owned(),
14141 insert: lsp::Range::new(
14142 lsp::Position::new(7, 7),
14143 lsp::Position::new(7, 11),
14144 ),
14145 replace: lsp::Range::new(
14146 lsp::Position::new(7, 7),
14147 lsp::Position::new(7, 13),
14148 ),
14149 },
14150 )),
14151 ..lsp::CompletionItem::default()
14152 };
14153
14154 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14155 })
14156 .next()
14157 .await
14158 .unwrap();
14159
14160 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14161 .await;
14162
14163 editor
14164 .update_in(cx, |editor, window, cx| {
14165 editor
14166 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14167 .unwrap()
14168 })
14169 .await
14170 .unwrap();
14171
14172 editor.update(cx, |editor, cx| {
14173 assert_text_with_selections(editor, expected_multibuffer, cx);
14174 })
14175}
14176
14177#[gpui::test]
14178async fn test_completion(cx: &mut TestAppContext) {
14179 init_test(cx, |_| {});
14180
14181 let mut cx = EditorLspTestContext::new_rust(
14182 lsp::ServerCapabilities {
14183 completion_provider: Some(lsp::CompletionOptions {
14184 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14185 resolve_provider: Some(true),
14186 ..Default::default()
14187 }),
14188 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14189 ..Default::default()
14190 },
14191 cx,
14192 )
14193 .await;
14194 let counter = Arc::new(AtomicUsize::new(0));
14195
14196 cx.set_state(indoc! {"
14197 oneˇ
14198 two
14199 three
14200 "});
14201 cx.simulate_keystroke(".");
14202 handle_completion_request(
14203 indoc! {"
14204 one.|<>
14205 two
14206 three
14207 "},
14208 vec!["first_completion", "second_completion"],
14209 true,
14210 counter.clone(),
14211 &mut cx,
14212 )
14213 .await;
14214 cx.condition(|editor, _| editor.context_menu_visible())
14215 .await;
14216 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14217
14218 let _handler = handle_signature_help_request(
14219 &mut cx,
14220 lsp::SignatureHelp {
14221 signatures: vec![lsp::SignatureInformation {
14222 label: "test signature".to_string(),
14223 documentation: None,
14224 parameters: Some(vec![lsp::ParameterInformation {
14225 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14226 documentation: None,
14227 }]),
14228 active_parameter: None,
14229 }],
14230 active_signature: None,
14231 active_parameter: None,
14232 },
14233 );
14234 cx.update_editor(|editor, window, cx| {
14235 assert!(
14236 !editor.signature_help_state.is_shown(),
14237 "No signature help was called for"
14238 );
14239 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14240 });
14241 cx.run_until_parked();
14242 cx.update_editor(|editor, _, _| {
14243 assert!(
14244 !editor.signature_help_state.is_shown(),
14245 "No signature help should be shown when completions menu is open"
14246 );
14247 });
14248
14249 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14250 editor.context_menu_next(&Default::default(), window, cx);
14251 editor
14252 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14253 .unwrap()
14254 });
14255 cx.assert_editor_state(indoc! {"
14256 one.second_completionˇ
14257 two
14258 three
14259 "});
14260
14261 handle_resolve_completion_request(
14262 &mut cx,
14263 Some(vec![
14264 (
14265 //This overlaps with the primary completion edit which is
14266 //misbehavior from the LSP spec, test that we filter it out
14267 indoc! {"
14268 one.second_ˇcompletion
14269 two
14270 threeˇ
14271 "},
14272 "overlapping additional edit",
14273 ),
14274 (
14275 indoc! {"
14276 one.second_completion
14277 two
14278 threeˇ
14279 "},
14280 "\nadditional edit",
14281 ),
14282 ]),
14283 )
14284 .await;
14285 apply_additional_edits.await.unwrap();
14286 cx.assert_editor_state(indoc! {"
14287 one.second_completionˇ
14288 two
14289 three
14290 additional edit
14291 "});
14292
14293 cx.set_state(indoc! {"
14294 one.second_completion
14295 twoˇ
14296 threeˇ
14297 additional edit
14298 "});
14299 cx.simulate_keystroke(" ");
14300 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14301 cx.simulate_keystroke("s");
14302 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14303
14304 cx.assert_editor_state(indoc! {"
14305 one.second_completion
14306 two sˇ
14307 three sˇ
14308 additional edit
14309 "});
14310 handle_completion_request(
14311 indoc! {"
14312 one.second_completion
14313 two s
14314 three <s|>
14315 additional edit
14316 "},
14317 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14318 true,
14319 counter.clone(),
14320 &mut cx,
14321 )
14322 .await;
14323 cx.condition(|editor, _| editor.context_menu_visible())
14324 .await;
14325 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14326
14327 cx.simulate_keystroke("i");
14328
14329 handle_completion_request(
14330 indoc! {"
14331 one.second_completion
14332 two si
14333 three <si|>
14334 additional edit
14335 "},
14336 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14337 true,
14338 counter.clone(),
14339 &mut cx,
14340 )
14341 .await;
14342 cx.condition(|editor, _| editor.context_menu_visible())
14343 .await;
14344 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14345
14346 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14347 editor
14348 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14349 .unwrap()
14350 });
14351 cx.assert_editor_state(indoc! {"
14352 one.second_completion
14353 two sixth_completionˇ
14354 three sixth_completionˇ
14355 additional edit
14356 "});
14357
14358 apply_additional_edits.await.unwrap();
14359
14360 update_test_language_settings(&mut cx, |settings| {
14361 settings.defaults.show_completions_on_input = Some(false);
14362 });
14363 cx.set_state("editorˇ");
14364 cx.simulate_keystroke(".");
14365 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14366 cx.simulate_keystrokes("c l o");
14367 cx.assert_editor_state("editor.cloˇ");
14368 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14369 cx.update_editor(|editor, window, cx| {
14370 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14371 });
14372 handle_completion_request(
14373 "editor.<clo|>",
14374 vec!["close", "clobber"],
14375 true,
14376 counter.clone(),
14377 &mut cx,
14378 )
14379 .await;
14380 cx.condition(|editor, _| editor.context_menu_visible())
14381 .await;
14382 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14383
14384 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14385 editor
14386 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14387 .unwrap()
14388 });
14389 cx.assert_editor_state("editor.clobberˇ");
14390 handle_resolve_completion_request(&mut cx, None).await;
14391 apply_additional_edits.await.unwrap();
14392}
14393
14394#[gpui::test]
14395async fn test_completion_reuse(cx: &mut TestAppContext) {
14396 init_test(cx, |_| {});
14397
14398 let mut cx = EditorLspTestContext::new_rust(
14399 lsp::ServerCapabilities {
14400 completion_provider: Some(lsp::CompletionOptions {
14401 trigger_characters: Some(vec![".".to_string()]),
14402 ..Default::default()
14403 }),
14404 ..Default::default()
14405 },
14406 cx,
14407 )
14408 .await;
14409
14410 let counter = Arc::new(AtomicUsize::new(0));
14411 cx.set_state("objˇ");
14412 cx.simulate_keystroke(".");
14413
14414 // Initial completion request returns complete results
14415 let is_incomplete = false;
14416 handle_completion_request(
14417 "obj.|<>",
14418 vec!["a", "ab", "abc"],
14419 is_incomplete,
14420 counter.clone(),
14421 &mut cx,
14422 )
14423 .await;
14424 cx.run_until_parked();
14425 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14426 cx.assert_editor_state("obj.ˇ");
14427 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14428
14429 // Type "a" - filters existing completions
14430 cx.simulate_keystroke("a");
14431 cx.run_until_parked();
14432 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14433 cx.assert_editor_state("obj.aˇ");
14434 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14435
14436 // Type "b" - filters existing completions
14437 cx.simulate_keystroke("b");
14438 cx.run_until_parked();
14439 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14440 cx.assert_editor_state("obj.abˇ");
14441 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14442
14443 // Type "c" - filters existing completions
14444 cx.simulate_keystroke("c");
14445 cx.run_until_parked();
14446 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14447 cx.assert_editor_state("obj.abcˇ");
14448 check_displayed_completions(vec!["abc"], &mut cx);
14449
14450 // Backspace to delete "c" - filters existing completions
14451 cx.update_editor(|editor, window, cx| {
14452 editor.backspace(&Backspace, window, cx);
14453 });
14454 cx.run_until_parked();
14455 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14456 cx.assert_editor_state("obj.abˇ");
14457 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14458
14459 // Moving cursor to the left dismisses menu.
14460 cx.update_editor(|editor, window, cx| {
14461 editor.move_left(&MoveLeft, window, cx);
14462 });
14463 cx.run_until_parked();
14464 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14465 cx.assert_editor_state("obj.aˇb");
14466 cx.update_editor(|editor, _, _| {
14467 assert_eq!(editor.context_menu_visible(), false);
14468 });
14469
14470 // Type "b" - new request
14471 cx.simulate_keystroke("b");
14472 let is_incomplete = false;
14473 handle_completion_request(
14474 "obj.<ab|>a",
14475 vec!["ab", "abc"],
14476 is_incomplete,
14477 counter.clone(),
14478 &mut cx,
14479 )
14480 .await;
14481 cx.run_until_parked();
14482 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14483 cx.assert_editor_state("obj.abˇb");
14484 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14485
14486 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14487 cx.update_editor(|editor, window, cx| {
14488 editor.backspace(&Backspace, window, cx);
14489 });
14490 let is_incomplete = false;
14491 handle_completion_request(
14492 "obj.<a|>b",
14493 vec!["a", "ab", "abc"],
14494 is_incomplete,
14495 counter.clone(),
14496 &mut cx,
14497 )
14498 .await;
14499 cx.run_until_parked();
14500 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14501 cx.assert_editor_state("obj.aˇb");
14502 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14503
14504 // Backspace to delete "a" - dismisses menu.
14505 cx.update_editor(|editor, window, cx| {
14506 editor.backspace(&Backspace, window, cx);
14507 });
14508 cx.run_until_parked();
14509 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14510 cx.assert_editor_state("obj.ˇb");
14511 cx.update_editor(|editor, _, _| {
14512 assert_eq!(editor.context_menu_visible(), false);
14513 });
14514}
14515
14516#[gpui::test]
14517async fn test_word_completion(cx: &mut TestAppContext) {
14518 let lsp_fetch_timeout_ms = 10;
14519 init_test(cx, |language_settings| {
14520 language_settings.defaults.completions = Some(CompletionSettingsContent {
14521 words_min_length: Some(0),
14522 lsp_fetch_timeout_ms: Some(10),
14523 lsp_insert_mode: Some(LspInsertMode::Insert),
14524 ..Default::default()
14525 });
14526 });
14527
14528 let mut cx = EditorLspTestContext::new_rust(
14529 lsp::ServerCapabilities {
14530 completion_provider: Some(lsp::CompletionOptions {
14531 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14532 ..lsp::CompletionOptions::default()
14533 }),
14534 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14535 ..lsp::ServerCapabilities::default()
14536 },
14537 cx,
14538 )
14539 .await;
14540
14541 let throttle_completions = Arc::new(AtomicBool::new(false));
14542
14543 let lsp_throttle_completions = throttle_completions.clone();
14544 let _completion_requests_handler =
14545 cx.lsp
14546 .server
14547 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14548 let lsp_throttle_completions = lsp_throttle_completions.clone();
14549 let cx = cx.clone();
14550 async move {
14551 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14552 cx.background_executor()
14553 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14554 .await;
14555 }
14556 Ok(Some(lsp::CompletionResponse::Array(vec![
14557 lsp::CompletionItem {
14558 label: "first".into(),
14559 ..lsp::CompletionItem::default()
14560 },
14561 lsp::CompletionItem {
14562 label: "last".into(),
14563 ..lsp::CompletionItem::default()
14564 },
14565 ])))
14566 }
14567 });
14568
14569 cx.set_state(indoc! {"
14570 oneˇ
14571 two
14572 three
14573 "});
14574 cx.simulate_keystroke(".");
14575 cx.executor().run_until_parked();
14576 cx.condition(|editor, _| editor.context_menu_visible())
14577 .await;
14578 cx.update_editor(|editor, window, cx| {
14579 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14580 {
14581 assert_eq!(
14582 completion_menu_entries(menu),
14583 &["first", "last"],
14584 "When LSP server is fast to reply, no fallback word completions are used"
14585 );
14586 } else {
14587 panic!("expected completion menu to be open");
14588 }
14589 editor.cancel(&Cancel, window, cx);
14590 });
14591 cx.executor().run_until_parked();
14592 cx.condition(|editor, _| !editor.context_menu_visible())
14593 .await;
14594
14595 throttle_completions.store(true, atomic::Ordering::Release);
14596 cx.simulate_keystroke(".");
14597 cx.executor()
14598 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14599 cx.executor().run_until_parked();
14600 cx.condition(|editor, _| editor.context_menu_visible())
14601 .await;
14602 cx.update_editor(|editor, _, _| {
14603 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14604 {
14605 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14606 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14607 } else {
14608 panic!("expected completion menu to be open");
14609 }
14610 });
14611}
14612
14613#[gpui::test]
14614async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14615 init_test(cx, |language_settings| {
14616 language_settings.defaults.completions = Some(CompletionSettingsContent {
14617 words: Some(WordsCompletionMode::Enabled),
14618 words_min_length: Some(0),
14619 lsp_insert_mode: Some(LspInsertMode::Insert),
14620 ..Default::default()
14621 });
14622 });
14623
14624 let mut cx = EditorLspTestContext::new_rust(
14625 lsp::ServerCapabilities {
14626 completion_provider: Some(lsp::CompletionOptions {
14627 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14628 ..lsp::CompletionOptions::default()
14629 }),
14630 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14631 ..lsp::ServerCapabilities::default()
14632 },
14633 cx,
14634 )
14635 .await;
14636
14637 let _completion_requests_handler =
14638 cx.lsp
14639 .server
14640 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14641 Ok(Some(lsp::CompletionResponse::Array(vec![
14642 lsp::CompletionItem {
14643 label: "first".into(),
14644 ..lsp::CompletionItem::default()
14645 },
14646 lsp::CompletionItem {
14647 label: "last".into(),
14648 ..lsp::CompletionItem::default()
14649 },
14650 ])))
14651 });
14652
14653 cx.set_state(indoc! {"ˇ
14654 first
14655 last
14656 second
14657 "});
14658 cx.simulate_keystroke(".");
14659 cx.executor().run_until_parked();
14660 cx.condition(|editor, _| editor.context_menu_visible())
14661 .await;
14662 cx.update_editor(|editor, _, _| {
14663 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14664 {
14665 assert_eq!(
14666 completion_menu_entries(menu),
14667 &["first", "last", "second"],
14668 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14669 );
14670 } else {
14671 panic!("expected completion menu to be open");
14672 }
14673 });
14674}
14675
14676#[gpui::test]
14677async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14678 init_test(cx, |language_settings| {
14679 language_settings.defaults.completions = Some(CompletionSettingsContent {
14680 words: Some(WordsCompletionMode::Disabled),
14681 words_min_length: Some(0),
14682 lsp_insert_mode: Some(LspInsertMode::Insert),
14683 ..Default::default()
14684 });
14685 });
14686
14687 let mut cx = EditorLspTestContext::new_rust(
14688 lsp::ServerCapabilities {
14689 completion_provider: Some(lsp::CompletionOptions {
14690 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14691 ..lsp::CompletionOptions::default()
14692 }),
14693 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14694 ..lsp::ServerCapabilities::default()
14695 },
14696 cx,
14697 )
14698 .await;
14699
14700 let _completion_requests_handler =
14701 cx.lsp
14702 .server
14703 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14704 panic!("LSP completions should not be queried when dealing with word completions")
14705 });
14706
14707 cx.set_state(indoc! {"ˇ
14708 first
14709 last
14710 second
14711 "});
14712 cx.update_editor(|editor, window, cx| {
14713 editor.show_word_completions(&ShowWordCompletions, window, cx);
14714 });
14715 cx.executor().run_until_parked();
14716 cx.condition(|editor, _| editor.context_menu_visible())
14717 .await;
14718 cx.update_editor(|editor, _, _| {
14719 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14720 {
14721 assert_eq!(
14722 completion_menu_entries(menu),
14723 &["first", "last", "second"],
14724 "`ShowWordCompletions` action should show word completions"
14725 );
14726 } else {
14727 panic!("expected completion menu to be open");
14728 }
14729 });
14730
14731 cx.simulate_keystroke("l");
14732 cx.executor().run_until_parked();
14733 cx.condition(|editor, _| editor.context_menu_visible())
14734 .await;
14735 cx.update_editor(|editor, _, _| {
14736 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14737 {
14738 assert_eq!(
14739 completion_menu_entries(menu),
14740 &["last"],
14741 "After showing word completions, further editing should filter them and not query the LSP"
14742 );
14743 } else {
14744 panic!("expected completion menu to be open");
14745 }
14746 });
14747}
14748
14749#[gpui::test]
14750async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14751 init_test(cx, |language_settings| {
14752 language_settings.defaults.completions = Some(CompletionSettingsContent {
14753 words_min_length: Some(0),
14754 lsp: Some(false),
14755 lsp_insert_mode: Some(LspInsertMode::Insert),
14756 ..Default::default()
14757 });
14758 });
14759
14760 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14761
14762 cx.set_state(indoc! {"ˇ
14763 0_usize
14764 let
14765 33
14766 4.5f32
14767 "});
14768 cx.update_editor(|editor, window, cx| {
14769 editor.show_completions(&ShowCompletions::default(), window, cx);
14770 });
14771 cx.executor().run_until_parked();
14772 cx.condition(|editor, _| editor.context_menu_visible())
14773 .await;
14774 cx.update_editor(|editor, window, cx| {
14775 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14776 {
14777 assert_eq!(
14778 completion_menu_entries(menu),
14779 &["let"],
14780 "With no digits in the completion query, no digits should be in the word completions"
14781 );
14782 } else {
14783 panic!("expected completion menu to be open");
14784 }
14785 editor.cancel(&Cancel, window, cx);
14786 });
14787
14788 cx.set_state(indoc! {"3ˇ
14789 0_usize
14790 let
14791 3
14792 33.35f32
14793 "});
14794 cx.update_editor(|editor, window, cx| {
14795 editor.show_completions(&ShowCompletions::default(), window, cx);
14796 });
14797 cx.executor().run_until_parked();
14798 cx.condition(|editor, _| editor.context_menu_visible())
14799 .await;
14800 cx.update_editor(|editor, _, _| {
14801 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14802 {
14803 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14804 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14805 } else {
14806 panic!("expected completion menu to be open");
14807 }
14808 });
14809}
14810
14811#[gpui::test]
14812async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14813 init_test(cx, |language_settings| {
14814 language_settings.defaults.completions = Some(CompletionSettingsContent {
14815 words: Some(WordsCompletionMode::Enabled),
14816 words_min_length: Some(3),
14817 lsp_insert_mode: Some(LspInsertMode::Insert),
14818 ..Default::default()
14819 });
14820 });
14821
14822 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14823 cx.set_state(indoc! {"ˇ
14824 wow
14825 wowen
14826 wowser
14827 "});
14828 cx.simulate_keystroke("w");
14829 cx.executor().run_until_parked();
14830 cx.update_editor(|editor, _, _| {
14831 if editor.context_menu.borrow_mut().is_some() {
14832 panic!(
14833 "expected completion menu to be hidden, as words completion threshold is not met"
14834 );
14835 }
14836 });
14837
14838 cx.update_editor(|editor, window, cx| {
14839 editor.show_word_completions(&ShowWordCompletions, window, cx);
14840 });
14841 cx.executor().run_until_parked();
14842 cx.update_editor(|editor, window, cx| {
14843 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14844 {
14845 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");
14846 } else {
14847 panic!("expected completion menu to be open after the word completions are called with an action");
14848 }
14849
14850 editor.cancel(&Cancel, window, cx);
14851 });
14852 cx.update_editor(|editor, _, _| {
14853 if editor.context_menu.borrow_mut().is_some() {
14854 panic!("expected completion menu to be hidden after canceling");
14855 }
14856 });
14857
14858 cx.simulate_keystroke("o");
14859 cx.executor().run_until_parked();
14860 cx.update_editor(|editor, _, _| {
14861 if editor.context_menu.borrow_mut().is_some() {
14862 panic!(
14863 "expected completion menu to be hidden, as words completion threshold is not met still"
14864 );
14865 }
14866 });
14867
14868 cx.simulate_keystroke("w");
14869 cx.executor().run_until_parked();
14870 cx.update_editor(|editor, _, _| {
14871 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14872 {
14873 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14874 } else {
14875 panic!("expected completion menu to be open after the word completions threshold is met");
14876 }
14877 });
14878}
14879
14880#[gpui::test]
14881async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14882 init_test(cx, |language_settings| {
14883 language_settings.defaults.completions = Some(CompletionSettingsContent {
14884 words: Some(WordsCompletionMode::Enabled),
14885 words_min_length: Some(0),
14886 lsp_insert_mode: Some(LspInsertMode::Insert),
14887 ..Default::default()
14888 });
14889 });
14890
14891 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14892 cx.update_editor(|editor, _, _| {
14893 editor.disable_word_completions();
14894 });
14895 cx.set_state(indoc! {"ˇ
14896 wow
14897 wowen
14898 wowser
14899 "});
14900 cx.simulate_keystroke("w");
14901 cx.executor().run_until_parked();
14902 cx.update_editor(|editor, _, _| {
14903 if editor.context_menu.borrow_mut().is_some() {
14904 panic!(
14905 "expected completion menu to be hidden, as words completion are disabled for this editor"
14906 );
14907 }
14908 });
14909
14910 cx.update_editor(|editor, window, cx| {
14911 editor.show_word_completions(&ShowWordCompletions, window, cx);
14912 });
14913 cx.executor().run_until_parked();
14914 cx.update_editor(|editor, _, _| {
14915 if editor.context_menu.borrow_mut().is_some() {
14916 panic!(
14917 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14918 );
14919 }
14920 });
14921}
14922
14923fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14924 let position = || lsp::Position {
14925 line: params.text_document_position.position.line,
14926 character: params.text_document_position.position.character,
14927 };
14928 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14929 range: lsp::Range {
14930 start: position(),
14931 end: position(),
14932 },
14933 new_text: text.to_string(),
14934 }))
14935}
14936
14937#[gpui::test]
14938async fn test_multiline_completion(cx: &mut TestAppContext) {
14939 init_test(cx, |_| {});
14940
14941 let fs = FakeFs::new(cx.executor());
14942 fs.insert_tree(
14943 path!("/a"),
14944 json!({
14945 "main.ts": "a",
14946 }),
14947 )
14948 .await;
14949
14950 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14951 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14952 let typescript_language = Arc::new(Language::new(
14953 LanguageConfig {
14954 name: "TypeScript".into(),
14955 matcher: LanguageMatcher {
14956 path_suffixes: vec!["ts".to_string()],
14957 ..LanguageMatcher::default()
14958 },
14959 line_comments: vec!["// ".into()],
14960 ..LanguageConfig::default()
14961 },
14962 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14963 ));
14964 language_registry.add(typescript_language.clone());
14965 let mut fake_servers = language_registry.register_fake_lsp(
14966 "TypeScript",
14967 FakeLspAdapter {
14968 capabilities: lsp::ServerCapabilities {
14969 completion_provider: Some(lsp::CompletionOptions {
14970 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14971 ..lsp::CompletionOptions::default()
14972 }),
14973 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14974 ..lsp::ServerCapabilities::default()
14975 },
14976 // Emulate vtsls label generation
14977 label_for_completion: Some(Box::new(|item, _| {
14978 let text = if let Some(description) = item
14979 .label_details
14980 .as_ref()
14981 .and_then(|label_details| label_details.description.as_ref())
14982 {
14983 format!("{} {}", item.label, description)
14984 } else if let Some(detail) = &item.detail {
14985 format!("{} {}", item.label, detail)
14986 } else {
14987 item.label.clone()
14988 };
14989 Some(language::CodeLabel::plain(text, None))
14990 })),
14991 ..FakeLspAdapter::default()
14992 },
14993 );
14994 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14995 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14996 let worktree_id = workspace
14997 .update(cx, |workspace, _window, cx| {
14998 workspace.project().update(cx, |project, cx| {
14999 project.worktrees(cx).next().unwrap().read(cx).id()
15000 })
15001 })
15002 .unwrap();
15003 let _buffer = project
15004 .update(cx, |project, cx| {
15005 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15006 })
15007 .await
15008 .unwrap();
15009 let editor = workspace
15010 .update(cx, |workspace, window, cx| {
15011 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15012 })
15013 .unwrap()
15014 .await
15015 .unwrap()
15016 .downcast::<Editor>()
15017 .unwrap();
15018 let fake_server = fake_servers.next().await.unwrap();
15019
15020 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15021 let multiline_label_2 = "a\nb\nc\n";
15022 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15023 let multiline_description = "d\ne\nf\n";
15024 let multiline_detail_2 = "g\nh\ni\n";
15025
15026 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15027 move |params, _| async move {
15028 Ok(Some(lsp::CompletionResponse::Array(vec![
15029 lsp::CompletionItem {
15030 label: multiline_label.to_string(),
15031 text_edit: gen_text_edit(¶ms, "new_text_1"),
15032 ..lsp::CompletionItem::default()
15033 },
15034 lsp::CompletionItem {
15035 label: "single line label 1".to_string(),
15036 detail: Some(multiline_detail.to_string()),
15037 text_edit: gen_text_edit(¶ms, "new_text_2"),
15038 ..lsp::CompletionItem::default()
15039 },
15040 lsp::CompletionItem {
15041 label: "single line label 2".to_string(),
15042 label_details: Some(lsp::CompletionItemLabelDetails {
15043 description: Some(multiline_description.to_string()),
15044 detail: None,
15045 }),
15046 text_edit: gen_text_edit(¶ms, "new_text_2"),
15047 ..lsp::CompletionItem::default()
15048 },
15049 lsp::CompletionItem {
15050 label: multiline_label_2.to_string(),
15051 detail: Some(multiline_detail_2.to_string()),
15052 text_edit: gen_text_edit(¶ms, "new_text_3"),
15053 ..lsp::CompletionItem::default()
15054 },
15055 lsp::CompletionItem {
15056 label: "Label with many spaces and \t but without newlines".to_string(),
15057 detail: Some(
15058 "Details with many spaces and \t but without newlines".to_string(),
15059 ),
15060 text_edit: gen_text_edit(¶ms, "new_text_4"),
15061 ..lsp::CompletionItem::default()
15062 },
15063 ])))
15064 },
15065 );
15066
15067 editor.update_in(cx, |editor, window, cx| {
15068 cx.focus_self(window);
15069 editor.move_to_end(&MoveToEnd, window, cx);
15070 editor.handle_input(".", window, cx);
15071 });
15072 cx.run_until_parked();
15073 completion_handle.next().await.unwrap();
15074
15075 editor.update(cx, |editor, _| {
15076 assert!(editor.context_menu_visible());
15077 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15078 {
15079 let completion_labels = menu
15080 .completions
15081 .borrow()
15082 .iter()
15083 .map(|c| c.label.text.clone())
15084 .collect::<Vec<_>>();
15085 assert_eq!(
15086 completion_labels,
15087 &[
15088 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15089 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15090 "single line label 2 d e f ",
15091 "a b c g h i ",
15092 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15093 ],
15094 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15095 );
15096
15097 for completion in menu
15098 .completions
15099 .borrow()
15100 .iter() {
15101 assert_eq!(
15102 completion.label.filter_range,
15103 0..completion.label.text.len(),
15104 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15105 );
15106 }
15107 } else {
15108 panic!("expected completion menu to be open");
15109 }
15110 });
15111}
15112
15113#[gpui::test]
15114async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15115 init_test(cx, |_| {});
15116 let mut cx = EditorLspTestContext::new_rust(
15117 lsp::ServerCapabilities {
15118 completion_provider: Some(lsp::CompletionOptions {
15119 trigger_characters: Some(vec![".".to_string()]),
15120 ..Default::default()
15121 }),
15122 ..Default::default()
15123 },
15124 cx,
15125 )
15126 .await;
15127 cx.lsp
15128 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15129 Ok(Some(lsp::CompletionResponse::Array(vec![
15130 lsp::CompletionItem {
15131 label: "first".into(),
15132 ..Default::default()
15133 },
15134 lsp::CompletionItem {
15135 label: "last".into(),
15136 ..Default::default()
15137 },
15138 ])))
15139 });
15140 cx.set_state("variableˇ");
15141 cx.simulate_keystroke(".");
15142 cx.executor().run_until_parked();
15143
15144 cx.update_editor(|editor, _, _| {
15145 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15146 {
15147 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15148 } else {
15149 panic!("expected completion menu to be open");
15150 }
15151 });
15152
15153 cx.update_editor(|editor, window, cx| {
15154 editor.move_page_down(&MovePageDown::default(), window, cx);
15155 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15156 {
15157 assert!(
15158 menu.selected_item == 1,
15159 "expected PageDown to select the last item from the context menu"
15160 );
15161 } else {
15162 panic!("expected completion menu to stay open after PageDown");
15163 }
15164 });
15165
15166 cx.update_editor(|editor, window, cx| {
15167 editor.move_page_up(&MovePageUp::default(), window, cx);
15168 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15169 {
15170 assert!(
15171 menu.selected_item == 0,
15172 "expected PageUp to select the first item from the context menu"
15173 );
15174 } else {
15175 panic!("expected completion menu to stay open after PageUp");
15176 }
15177 });
15178}
15179
15180#[gpui::test]
15181async fn test_as_is_completions(cx: &mut TestAppContext) {
15182 init_test(cx, |_| {});
15183 let mut cx = EditorLspTestContext::new_rust(
15184 lsp::ServerCapabilities {
15185 completion_provider: Some(lsp::CompletionOptions {
15186 ..Default::default()
15187 }),
15188 ..Default::default()
15189 },
15190 cx,
15191 )
15192 .await;
15193 cx.lsp
15194 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15195 Ok(Some(lsp::CompletionResponse::Array(vec![
15196 lsp::CompletionItem {
15197 label: "unsafe".into(),
15198 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15199 range: lsp::Range {
15200 start: lsp::Position {
15201 line: 1,
15202 character: 2,
15203 },
15204 end: lsp::Position {
15205 line: 1,
15206 character: 3,
15207 },
15208 },
15209 new_text: "unsafe".to_string(),
15210 })),
15211 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15212 ..Default::default()
15213 },
15214 ])))
15215 });
15216 cx.set_state("fn a() {}\n nˇ");
15217 cx.executor().run_until_parked();
15218 cx.update_editor(|editor, window, cx| {
15219 editor.show_completions(
15220 &ShowCompletions {
15221 trigger: Some("\n".into()),
15222 },
15223 window,
15224 cx,
15225 );
15226 });
15227 cx.executor().run_until_parked();
15228
15229 cx.update_editor(|editor, window, cx| {
15230 editor.confirm_completion(&Default::default(), window, cx)
15231 });
15232 cx.executor().run_until_parked();
15233 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15234}
15235
15236#[gpui::test]
15237async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15238 init_test(cx, |_| {});
15239 let language =
15240 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15241 let mut cx = EditorLspTestContext::new(
15242 language,
15243 lsp::ServerCapabilities {
15244 completion_provider: Some(lsp::CompletionOptions {
15245 ..lsp::CompletionOptions::default()
15246 }),
15247 ..lsp::ServerCapabilities::default()
15248 },
15249 cx,
15250 )
15251 .await;
15252
15253 cx.set_state(
15254 "#ifndef BAR_H
15255#define BAR_H
15256
15257#include <stdbool.h>
15258
15259int fn_branch(bool do_branch1, bool do_branch2);
15260
15261#endif // BAR_H
15262ˇ",
15263 );
15264 cx.executor().run_until_parked();
15265 cx.update_editor(|editor, window, cx| {
15266 editor.handle_input("#", window, cx);
15267 });
15268 cx.executor().run_until_parked();
15269 cx.update_editor(|editor, window, cx| {
15270 editor.handle_input("i", window, cx);
15271 });
15272 cx.executor().run_until_parked();
15273 cx.update_editor(|editor, window, cx| {
15274 editor.handle_input("n", window, cx);
15275 });
15276 cx.executor().run_until_parked();
15277 cx.assert_editor_state(
15278 "#ifndef BAR_H
15279#define BAR_H
15280
15281#include <stdbool.h>
15282
15283int fn_branch(bool do_branch1, bool do_branch2);
15284
15285#endif // BAR_H
15286#inˇ",
15287 );
15288
15289 cx.lsp
15290 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15291 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15292 is_incomplete: false,
15293 item_defaults: None,
15294 items: vec![lsp::CompletionItem {
15295 kind: Some(lsp::CompletionItemKind::SNIPPET),
15296 label_details: Some(lsp::CompletionItemLabelDetails {
15297 detail: Some("header".to_string()),
15298 description: None,
15299 }),
15300 label: " include".to_string(),
15301 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15302 range: lsp::Range {
15303 start: lsp::Position {
15304 line: 8,
15305 character: 1,
15306 },
15307 end: lsp::Position {
15308 line: 8,
15309 character: 1,
15310 },
15311 },
15312 new_text: "include \"$0\"".to_string(),
15313 })),
15314 sort_text: Some("40b67681include".to_string()),
15315 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15316 filter_text: Some("include".to_string()),
15317 insert_text: Some("include \"$0\"".to_string()),
15318 ..lsp::CompletionItem::default()
15319 }],
15320 })))
15321 });
15322 cx.update_editor(|editor, window, cx| {
15323 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15324 });
15325 cx.executor().run_until_parked();
15326 cx.update_editor(|editor, window, cx| {
15327 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15328 });
15329 cx.executor().run_until_parked();
15330 cx.assert_editor_state(
15331 "#ifndef BAR_H
15332#define BAR_H
15333
15334#include <stdbool.h>
15335
15336int fn_branch(bool do_branch1, bool do_branch2);
15337
15338#endif // BAR_H
15339#include \"ˇ\"",
15340 );
15341
15342 cx.lsp
15343 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15344 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15345 is_incomplete: true,
15346 item_defaults: None,
15347 items: vec![lsp::CompletionItem {
15348 kind: Some(lsp::CompletionItemKind::FILE),
15349 label: "AGL/".to_string(),
15350 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15351 range: lsp::Range {
15352 start: lsp::Position {
15353 line: 8,
15354 character: 10,
15355 },
15356 end: lsp::Position {
15357 line: 8,
15358 character: 11,
15359 },
15360 },
15361 new_text: "AGL/".to_string(),
15362 })),
15363 sort_text: Some("40b67681AGL/".to_string()),
15364 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15365 filter_text: Some("AGL/".to_string()),
15366 insert_text: Some("AGL/".to_string()),
15367 ..lsp::CompletionItem::default()
15368 }],
15369 })))
15370 });
15371 cx.update_editor(|editor, window, cx| {
15372 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15373 });
15374 cx.executor().run_until_parked();
15375 cx.update_editor(|editor, window, cx| {
15376 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15377 });
15378 cx.executor().run_until_parked();
15379 cx.assert_editor_state(
15380 r##"#ifndef BAR_H
15381#define BAR_H
15382
15383#include <stdbool.h>
15384
15385int fn_branch(bool do_branch1, bool do_branch2);
15386
15387#endif // BAR_H
15388#include "AGL/ˇ"##,
15389 );
15390
15391 cx.update_editor(|editor, window, cx| {
15392 editor.handle_input("\"", window, cx);
15393 });
15394 cx.executor().run_until_parked();
15395 cx.assert_editor_state(
15396 r##"#ifndef BAR_H
15397#define BAR_H
15398
15399#include <stdbool.h>
15400
15401int fn_branch(bool do_branch1, bool do_branch2);
15402
15403#endif // BAR_H
15404#include "AGL/"ˇ"##,
15405 );
15406}
15407
15408#[gpui::test]
15409async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15410 init_test(cx, |_| {});
15411
15412 let mut cx = EditorLspTestContext::new_rust(
15413 lsp::ServerCapabilities {
15414 completion_provider: Some(lsp::CompletionOptions {
15415 trigger_characters: Some(vec![".".to_string()]),
15416 resolve_provider: Some(true),
15417 ..Default::default()
15418 }),
15419 ..Default::default()
15420 },
15421 cx,
15422 )
15423 .await;
15424
15425 cx.set_state("fn main() { let a = 2ˇ; }");
15426 cx.simulate_keystroke(".");
15427 let completion_item = lsp::CompletionItem {
15428 label: "Some".into(),
15429 kind: Some(lsp::CompletionItemKind::SNIPPET),
15430 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15431 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15432 kind: lsp::MarkupKind::Markdown,
15433 value: "```rust\nSome(2)\n```".to_string(),
15434 })),
15435 deprecated: Some(false),
15436 sort_text: Some("Some".to_string()),
15437 filter_text: Some("Some".to_string()),
15438 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15439 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15440 range: lsp::Range {
15441 start: lsp::Position {
15442 line: 0,
15443 character: 22,
15444 },
15445 end: lsp::Position {
15446 line: 0,
15447 character: 22,
15448 },
15449 },
15450 new_text: "Some(2)".to_string(),
15451 })),
15452 additional_text_edits: Some(vec![lsp::TextEdit {
15453 range: lsp::Range {
15454 start: lsp::Position {
15455 line: 0,
15456 character: 20,
15457 },
15458 end: lsp::Position {
15459 line: 0,
15460 character: 22,
15461 },
15462 },
15463 new_text: "".to_string(),
15464 }]),
15465 ..Default::default()
15466 };
15467
15468 let closure_completion_item = completion_item.clone();
15469 let counter = Arc::new(AtomicUsize::new(0));
15470 let counter_clone = counter.clone();
15471 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15472 let task_completion_item = closure_completion_item.clone();
15473 counter_clone.fetch_add(1, atomic::Ordering::Release);
15474 async move {
15475 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15476 is_incomplete: true,
15477 item_defaults: None,
15478 items: vec![task_completion_item],
15479 })))
15480 }
15481 });
15482
15483 cx.condition(|editor, _| editor.context_menu_visible())
15484 .await;
15485 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15486 assert!(request.next().await.is_some());
15487 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15488
15489 cx.simulate_keystrokes("S o m");
15490 cx.condition(|editor, _| editor.context_menu_visible())
15491 .await;
15492 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15493 assert!(request.next().await.is_some());
15494 assert!(request.next().await.is_some());
15495 assert!(request.next().await.is_some());
15496 request.close();
15497 assert!(request.next().await.is_none());
15498 assert_eq!(
15499 counter.load(atomic::Ordering::Acquire),
15500 4,
15501 "With the completions menu open, only one LSP request should happen per input"
15502 );
15503}
15504
15505#[gpui::test]
15506async fn test_toggle_comment(cx: &mut TestAppContext) {
15507 init_test(cx, |_| {});
15508 let mut cx = EditorTestContext::new(cx).await;
15509 let language = Arc::new(Language::new(
15510 LanguageConfig {
15511 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15512 ..Default::default()
15513 },
15514 Some(tree_sitter_rust::LANGUAGE.into()),
15515 ));
15516 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15517
15518 // If multiple selections intersect a line, the line is only toggled once.
15519 cx.set_state(indoc! {"
15520 fn a() {
15521 «//b();
15522 ˇ»// «c();
15523 //ˇ» d();
15524 }
15525 "});
15526
15527 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15528
15529 cx.assert_editor_state(indoc! {"
15530 fn a() {
15531 «b();
15532 c();
15533 ˇ» d();
15534 }
15535 "});
15536
15537 // The comment prefix is inserted at the same column for every line in a
15538 // selection.
15539 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15540
15541 cx.assert_editor_state(indoc! {"
15542 fn a() {
15543 // «b();
15544 // c();
15545 ˇ»// d();
15546 }
15547 "});
15548
15549 // If a selection ends at the beginning of a line, that line is not toggled.
15550 cx.set_selections_state(indoc! {"
15551 fn a() {
15552 // b();
15553 «// c();
15554 ˇ» // d();
15555 }
15556 "});
15557
15558 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15559
15560 cx.assert_editor_state(indoc! {"
15561 fn a() {
15562 // b();
15563 «c();
15564 ˇ» // d();
15565 }
15566 "});
15567
15568 // If a selection span a single line and is empty, the line is toggled.
15569 cx.set_state(indoc! {"
15570 fn a() {
15571 a();
15572 b();
15573 ˇ
15574 }
15575 "});
15576
15577 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15578
15579 cx.assert_editor_state(indoc! {"
15580 fn a() {
15581 a();
15582 b();
15583 //•ˇ
15584 }
15585 "});
15586
15587 // If a selection span multiple lines, empty lines are not toggled.
15588 cx.set_state(indoc! {"
15589 fn a() {
15590 «a();
15591
15592 c();ˇ»
15593 }
15594 "});
15595
15596 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15597
15598 cx.assert_editor_state(indoc! {"
15599 fn a() {
15600 // «a();
15601
15602 // c();ˇ»
15603 }
15604 "});
15605
15606 // If a selection includes multiple comment prefixes, all lines are uncommented.
15607 cx.set_state(indoc! {"
15608 fn a() {
15609 «// a();
15610 /// b();
15611 //! c();ˇ»
15612 }
15613 "});
15614
15615 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15616
15617 cx.assert_editor_state(indoc! {"
15618 fn a() {
15619 «a();
15620 b();
15621 c();ˇ»
15622 }
15623 "});
15624}
15625
15626#[gpui::test]
15627async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15628 init_test(cx, |_| {});
15629 let mut cx = EditorTestContext::new(cx).await;
15630 let language = Arc::new(Language::new(
15631 LanguageConfig {
15632 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15633 ..Default::default()
15634 },
15635 Some(tree_sitter_rust::LANGUAGE.into()),
15636 ));
15637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15638
15639 let toggle_comments = &ToggleComments {
15640 advance_downwards: false,
15641 ignore_indent: true,
15642 };
15643
15644 // If multiple selections intersect a line, the line is only toggled once.
15645 cx.set_state(indoc! {"
15646 fn a() {
15647 // «b();
15648 // c();
15649 // ˇ» d();
15650 }
15651 "});
15652
15653 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15654
15655 cx.assert_editor_state(indoc! {"
15656 fn a() {
15657 «b();
15658 c();
15659 ˇ» d();
15660 }
15661 "});
15662
15663 // The comment prefix is inserted at the beginning of each line
15664 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15665
15666 cx.assert_editor_state(indoc! {"
15667 fn a() {
15668 // «b();
15669 // c();
15670 // ˇ» d();
15671 }
15672 "});
15673
15674 // If a selection ends at the beginning of a line, that line is not toggled.
15675 cx.set_selections_state(indoc! {"
15676 fn a() {
15677 // b();
15678 // «c();
15679 ˇ»// d();
15680 }
15681 "});
15682
15683 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15684
15685 cx.assert_editor_state(indoc! {"
15686 fn a() {
15687 // b();
15688 «c();
15689 ˇ»// d();
15690 }
15691 "});
15692
15693 // If a selection span a single line and is empty, the line is toggled.
15694 cx.set_state(indoc! {"
15695 fn a() {
15696 a();
15697 b();
15698 ˇ
15699 }
15700 "});
15701
15702 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15703
15704 cx.assert_editor_state(indoc! {"
15705 fn a() {
15706 a();
15707 b();
15708 //ˇ
15709 }
15710 "});
15711
15712 // If a selection span multiple lines, empty lines are not toggled.
15713 cx.set_state(indoc! {"
15714 fn a() {
15715 «a();
15716
15717 c();ˇ»
15718 }
15719 "});
15720
15721 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15722
15723 cx.assert_editor_state(indoc! {"
15724 fn a() {
15725 // «a();
15726
15727 // c();ˇ»
15728 }
15729 "});
15730
15731 // If a selection includes multiple comment prefixes, all lines are uncommented.
15732 cx.set_state(indoc! {"
15733 fn a() {
15734 // «a();
15735 /// b();
15736 //! c();ˇ»
15737 }
15738 "});
15739
15740 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15741
15742 cx.assert_editor_state(indoc! {"
15743 fn a() {
15744 «a();
15745 b();
15746 c();ˇ»
15747 }
15748 "});
15749}
15750
15751#[gpui::test]
15752async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15753 init_test(cx, |_| {});
15754
15755 let language = Arc::new(Language::new(
15756 LanguageConfig {
15757 line_comments: vec!["// ".into()],
15758 ..Default::default()
15759 },
15760 Some(tree_sitter_rust::LANGUAGE.into()),
15761 ));
15762
15763 let mut cx = EditorTestContext::new(cx).await;
15764
15765 cx.language_registry().add(language.clone());
15766 cx.update_buffer(|buffer, cx| {
15767 buffer.set_language(Some(language), cx);
15768 });
15769
15770 let toggle_comments = &ToggleComments {
15771 advance_downwards: true,
15772 ignore_indent: false,
15773 };
15774
15775 // Single cursor on one line -> advance
15776 // Cursor moves horizontally 3 characters as well on non-blank line
15777 cx.set_state(indoc!(
15778 "fn a() {
15779 ˇdog();
15780 cat();
15781 }"
15782 ));
15783 cx.update_editor(|editor, window, cx| {
15784 editor.toggle_comments(toggle_comments, window, cx);
15785 });
15786 cx.assert_editor_state(indoc!(
15787 "fn a() {
15788 // dog();
15789 catˇ();
15790 }"
15791 ));
15792
15793 // Single selection on one line -> don't advance
15794 cx.set_state(indoc!(
15795 "fn a() {
15796 «dog()ˇ»;
15797 cat();
15798 }"
15799 ));
15800 cx.update_editor(|editor, window, cx| {
15801 editor.toggle_comments(toggle_comments, window, cx);
15802 });
15803 cx.assert_editor_state(indoc!(
15804 "fn a() {
15805 // «dog()ˇ»;
15806 cat();
15807 }"
15808 ));
15809
15810 // Multiple cursors on one line -> advance
15811 cx.set_state(indoc!(
15812 "fn a() {
15813 ˇdˇog();
15814 cat();
15815 }"
15816 ));
15817 cx.update_editor(|editor, window, cx| {
15818 editor.toggle_comments(toggle_comments, window, cx);
15819 });
15820 cx.assert_editor_state(indoc!(
15821 "fn a() {
15822 // dog();
15823 catˇ(ˇ);
15824 }"
15825 ));
15826
15827 // Multiple cursors on one line, with selection -> don't advance
15828 cx.set_state(indoc!(
15829 "fn a() {
15830 ˇdˇog«()ˇ»;
15831 cat();
15832 }"
15833 ));
15834 cx.update_editor(|editor, window, cx| {
15835 editor.toggle_comments(toggle_comments, window, cx);
15836 });
15837 cx.assert_editor_state(indoc!(
15838 "fn a() {
15839 // ˇdˇog«()ˇ»;
15840 cat();
15841 }"
15842 ));
15843
15844 // Single cursor on one line -> advance
15845 // Cursor moves to column 0 on blank line
15846 cx.set_state(indoc!(
15847 "fn a() {
15848 ˇdog();
15849
15850 cat();
15851 }"
15852 ));
15853 cx.update_editor(|editor, window, cx| {
15854 editor.toggle_comments(toggle_comments, window, cx);
15855 });
15856 cx.assert_editor_state(indoc!(
15857 "fn a() {
15858 // dog();
15859 ˇ
15860 cat();
15861 }"
15862 ));
15863
15864 // Single cursor on one line -> advance
15865 // Cursor starts and ends at column 0
15866 cx.set_state(indoc!(
15867 "fn a() {
15868 ˇ dog();
15869 cat();
15870 }"
15871 ));
15872 cx.update_editor(|editor, window, cx| {
15873 editor.toggle_comments(toggle_comments, window, cx);
15874 });
15875 cx.assert_editor_state(indoc!(
15876 "fn a() {
15877 // dog();
15878 ˇ cat();
15879 }"
15880 ));
15881}
15882
15883#[gpui::test]
15884async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15885 init_test(cx, |_| {});
15886
15887 let mut cx = EditorTestContext::new(cx).await;
15888
15889 let html_language = Arc::new(
15890 Language::new(
15891 LanguageConfig {
15892 name: "HTML".into(),
15893 block_comment: Some(BlockCommentConfig {
15894 start: "<!-- ".into(),
15895 prefix: "".into(),
15896 end: " -->".into(),
15897 tab_size: 0,
15898 }),
15899 ..Default::default()
15900 },
15901 Some(tree_sitter_html::LANGUAGE.into()),
15902 )
15903 .with_injection_query(
15904 r#"
15905 (script_element
15906 (raw_text) @injection.content
15907 (#set! injection.language "javascript"))
15908 "#,
15909 )
15910 .unwrap(),
15911 );
15912
15913 let javascript_language = Arc::new(Language::new(
15914 LanguageConfig {
15915 name: "JavaScript".into(),
15916 line_comments: vec!["// ".into()],
15917 ..Default::default()
15918 },
15919 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15920 ));
15921
15922 cx.language_registry().add(html_language.clone());
15923 cx.language_registry().add(javascript_language);
15924 cx.update_buffer(|buffer, cx| {
15925 buffer.set_language(Some(html_language), cx);
15926 });
15927
15928 // Toggle comments for empty selections
15929 cx.set_state(
15930 &r#"
15931 <p>A</p>ˇ
15932 <p>B</p>ˇ
15933 <p>C</p>ˇ
15934 "#
15935 .unindent(),
15936 );
15937 cx.update_editor(|editor, window, cx| {
15938 editor.toggle_comments(&ToggleComments::default(), window, cx)
15939 });
15940 cx.assert_editor_state(
15941 &r#"
15942 <!-- <p>A</p>ˇ -->
15943 <!-- <p>B</p>ˇ -->
15944 <!-- <p>C</p>ˇ -->
15945 "#
15946 .unindent(),
15947 );
15948 cx.update_editor(|editor, window, cx| {
15949 editor.toggle_comments(&ToggleComments::default(), window, cx)
15950 });
15951 cx.assert_editor_state(
15952 &r#"
15953 <p>A</p>ˇ
15954 <p>B</p>ˇ
15955 <p>C</p>ˇ
15956 "#
15957 .unindent(),
15958 );
15959
15960 // Toggle comments for mixture of empty and non-empty selections, where
15961 // multiple selections occupy a given line.
15962 cx.set_state(
15963 &r#"
15964 <p>A«</p>
15965 <p>ˇ»B</p>ˇ
15966 <p>C«</p>
15967 <p>ˇ»D</p>ˇ
15968 "#
15969 .unindent(),
15970 );
15971
15972 cx.update_editor(|editor, window, cx| {
15973 editor.toggle_comments(&ToggleComments::default(), window, cx)
15974 });
15975 cx.assert_editor_state(
15976 &r#"
15977 <!-- <p>A«</p>
15978 <p>ˇ»B</p>ˇ -->
15979 <!-- <p>C«</p>
15980 <p>ˇ»D</p>ˇ -->
15981 "#
15982 .unindent(),
15983 );
15984 cx.update_editor(|editor, window, cx| {
15985 editor.toggle_comments(&ToggleComments::default(), window, cx)
15986 });
15987 cx.assert_editor_state(
15988 &r#"
15989 <p>A«</p>
15990 <p>ˇ»B</p>ˇ
15991 <p>C«</p>
15992 <p>ˇ»D</p>ˇ
15993 "#
15994 .unindent(),
15995 );
15996
15997 // Toggle comments when different languages are active for different
15998 // selections.
15999 cx.set_state(
16000 &r#"
16001 ˇ<script>
16002 ˇvar x = new Y();
16003 ˇ</script>
16004 "#
16005 .unindent(),
16006 );
16007 cx.executor().run_until_parked();
16008 cx.update_editor(|editor, window, cx| {
16009 editor.toggle_comments(&ToggleComments::default(), window, cx)
16010 });
16011 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16012 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16013 cx.assert_editor_state(
16014 &r#"
16015 <!-- ˇ<script> -->
16016 // ˇvar x = new Y();
16017 <!-- ˇ</script> -->
16018 "#
16019 .unindent(),
16020 );
16021}
16022
16023#[gpui::test]
16024fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16025 init_test(cx, |_| {});
16026
16027 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16028 let multibuffer = cx.new(|cx| {
16029 let mut multibuffer = MultiBuffer::new(ReadWrite);
16030 multibuffer.push_excerpts(
16031 buffer.clone(),
16032 [
16033 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16034 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16035 ],
16036 cx,
16037 );
16038 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16039 multibuffer
16040 });
16041
16042 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16043 editor.update_in(cx, |editor, window, cx| {
16044 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16045 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16046 s.select_ranges([
16047 Point::new(0, 0)..Point::new(0, 0),
16048 Point::new(1, 0)..Point::new(1, 0),
16049 ])
16050 });
16051
16052 editor.handle_input("X", window, cx);
16053 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16054 assert_eq!(
16055 editor.selections.ranges(&editor.display_snapshot(cx)),
16056 [
16057 Point::new(0, 1)..Point::new(0, 1),
16058 Point::new(1, 1)..Point::new(1, 1),
16059 ]
16060 );
16061
16062 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16063 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16064 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16065 });
16066 editor.backspace(&Default::default(), window, cx);
16067 assert_eq!(editor.text(cx), "Xa\nbbb");
16068 assert_eq!(
16069 editor.selections.ranges(&editor.display_snapshot(cx)),
16070 [Point::new(1, 0)..Point::new(1, 0)]
16071 );
16072
16073 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16074 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16075 });
16076 editor.backspace(&Default::default(), window, cx);
16077 assert_eq!(editor.text(cx), "X\nbb");
16078 assert_eq!(
16079 editor.selections.ranges(&editor.display_snapshot(cx)),
16080 [Point::new(0, 1)..Point::new(0, 1)]
16081 );
16082 });
16083}
16084
16085#[gpui::test]
16086fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16087 init_test(cx, |_| {});
16088
16089 let markers = vec![('[', ']').into(), ('(', ')').into()];
16090 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16091 indoc! {"
16092 [aaaa
16093 (bbbb]
16094 cccc)",
16095 },
16096 markers.clone(),
16097 );
16098 let excerpt_ranges = markers.into_iter().map(|marker| {
16099 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16100 ExcerptRange::new(context)
16101 });
16102 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16103 let multibuffer = cx.new(|cx| {
16104 let mut multibuffer = MultiBuffer::new(ReadWrite);
16105 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16106 multibuffer
16107 });
16108
16109 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16110 editor.update_in(cx, |editor, window, cx| {
16111 let (expected_text, selection_ranges) = marked_text_ranges(
16112 indoc! {"
16113 aaaa
16114 bˇbbb
16115 bˇbbˇb
16116 cccc"
16117 },
16118 true,
16119 );
16120 assert_eq!(editor.text(cx), expected_text);
16121 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16122 s.select_ranges(selection_ranges)
16123 });
16124
16125 editor.handle_input("X", window, cx);
16126
16127 let (expected_text, expected_selections) = marked_text_ranges(
16128 indoc! {"
16129 aaaa
16130 bXˇbbXb
16131 bXˇbbXˇb
16132 cccc"
16133 },
16134 false,
16135 );
16136 assert_eq!(editor.text(cx), expected_text);
16137 assert_eq!(
16138 editor.selections.ranges(&editor.display_snapshot(cx)),
16139 expected_selections
16140 );
16141
16142 editor.newline(&Newline, window, cx);
16143 let (expected_text, expected_selections) = marked_text_ranges(
16144 indoc! {"
16145 aaaa
16146 bX
16147 ˇbbX
16148 b
16149 bX
16150 ˇbbX
16151 ˇb
16152 cccc"
16153 },
16154 false,
16155 );
16156 assert_eq!(editor.text(cx), expected_text);
16157 assert_eq!(
16158 editor.selections.ranges(&editor.display_snapshot(cx)),
16159 expected_selections
16160 );
16161 });
16162}
16163
16164#[gpui::test]
16165fn test_refresh_selections(cx: &mut TestAppContext) {
16166 init_test(cx, |_| {});
16167
16168 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16169 let mut excerpt1_id = None;
16170 let multibuffer = cx.new(|cx| {
16171 let mut multibuffer = MultiBuffer::new(ReadWrite);
16172 excerpt1_id = multibuffer
16173 .push_excerpts(
16174 buffer.clone(),
16175 [
16176 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16177 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16178 ],
16179 cx,
16180 )
16181 .into_iter()
16182 .next();
16183 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16184 multibuffer
16185 });
16186
16187 let editor = cx.add_window(|window, cx| {
16188 let mut editor = build_editor(multibuffer.clone(), window, cx);
16189 let snapshot = editor.snapshot(window, cx);
16190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16191 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16192 });
16193 editor.begin_selection(
16194 Point::new(2, 1).to_display_point(&snapshot),
16195 true,
16196 1,
16197 window,
16198 cx,
16199 );
16200 assert_eq!(
16201 editor.selections.ranges(&editor.display_snapshot(cx)),
16202 [
16203 Point::new(1, 3)..Point::new(1, 3),
16204 Point::new(2, 1)..Point::new(2, 1),
16205 ]
16206 );
16207 editor
16208 });
16209
16210 // Refreshing selections is a no-op when excerpts haven't changed.
16211 _ = editor.update(cx, |editor, window, cx| {
16212 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16213 assert_eq!(
16214 editor.selections.ranges(&editor.display_snapshot(cx)),
16215 [
16216 Point::new(1, 3)..Point::new(1, 3),
16217 Point::new(2, 1)..Point::new(2, 1),
16218 ]
16219 );
16220 });
16221
16222 multibuffer.update(cx, |multibuffer, cx| {
16223 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16224 });
16225 _ = editor.update(cx, |editor, window, cx| {
16226 // Removing an excerpt causes the first selection to become degenerate.
16227 assert_eq!(
16228 editor.selections.ranges(&editor.display_snapshot(cx)),
16229 [
16230 Point::new(0, 0)..Point::new(0, 0),
16231 Point::new(0, 1)..Point::new(0, 1)
16232 ]
16233 );
16234
16235 // Refreshing selections will relocate the first selection to the original buffer
16236 // location.
16237 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16238 assert_eq!(
16239 editor.selections.ranges(&editor.display_snapshot(cx)),
16240 [
16241 Point::new(0, 1)..Point::new(0, 1),
16242 Point::new(0, 3)..Point::new(0, 3)
16243 ]
16244 );
16245 assert!(editor.selections.pending_anchor().is_some());
16246 });
16247}
16248
16249#[gpui::test]
16250fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16251 init_test(cx, |_| {});
16252
16253 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16254 let mut excerpt1_id = None;
16255 let multibuffer = cx.new(|cx| {
16256 let mut multibuffer = MultiBuffer::new(ReadWrite);
16257 excerpt1_id = multibuffer
16258 .push_excerpts(
16259 buffer.clone(),
16260 [
16261 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16262 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16263 ],
16264 cx,
16265 )
16266 .into_iter()
16267 .next();
16268 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16269 multibuffer
16270 });
16271
16272 let editor = cx.add_window(|window, cx| {
16273 let mut editor = build_editor(multibuffer.clone(), window, cx);
16274 let snapshot = editor.snapshot(window, cx);
16275 editor.begin_selection(
16276 Point::new(1, 3).to_display_point(&snapshot),
16277 false,
16278 1,
16279 window,
16280 cx,
16281 );
16282 assert_eq!(
16283 editor.selections.ranges(&editor.display_snapshot(cx)),
16284 [Point::new(1, 3)..Point::new(1, 3)]
16285 );
16286 editor
16287 });
16288
16289 multibuffer.update(cx, |multibuffer, cx| {
16290 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16291 });
16292 _ = editor.update(cx, |editor, window, cx| {
16293 assert_eq!(
16294 editor.selections.ranges(&editor.display_snapshot(cx)),
16295 [Point::new(0, 0)..Point::new(0, 0)]
16296 );
16297
16298 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16300 assert_eq!(
16301 editor.selections.ranges(&editor.display_snapshot(cx)),
16302 [Point::new(0, 3)..Point::new(0, 3)]
16303 );
16304 assert!(editor.selections.pending_anchor().is_some());
16305 });
16306}
16307
16308#[gpui::test]
16309async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16310 init_test(cx, |_| {});
16311
16312 let language = Arc::new(
16313 Language::new(
16314 LanguageConfig {
16315 brackets: BracketPairConfig {
16316 pairs: vec![
16317 BracketPair {
16318 start: "{".to_string(),
16319 end: "}".to_string(),
16320 close: true,
16321 surround: true,
16322 newline: true,
16323 },
16324 BracketPair {
16325 start: "/* ".to_string(),
16326 end: " */".to_string(),
16327 close: true,
16328 surround: true,
16329 newline: true,
16330 },
16331 ],
16332 ..Default::default()
16333 },
16334 ..Default::default()
16335 },
16336 Some(tree_sitter_rust::LANGUAGE.into()),
16337 )
16338 .with_indents_query("")
16339 .unwrap(),
16340 );
16341
16342 let text = concat!(
16343 "{ }\n", //
16344 " x\n", //
16345 " /* */\n", //
16346 "x\n", //
16347 "{{} }\n", //
16348 );
16349
16350 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16351 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16352 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16353 editor
16354 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16355 .await;
16356
16357 editor.update_in(cx, |editor, window, cx| {
16358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16359 s.select_display_ranges([
16360 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16361 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16362 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16363 ])
16364 });
16365 editor.newline(&Newline, window, cx);
16366
16367 assert_eq!(
16368 editor.buffer().read(cx).read(cx).text(),
16369 concat!(
16370 "{ \n", // Suppress rustfmt
16371 "\n", //
16372 "}\n", //
16373 " x\n", //
16374 " /* \n", //
16375 " \n", //
16376 " */\n", //
16377 "x\n", //
16378 "{{} \n", //
16379 "}\n", //
16380 )
16381 );
16382 });
16383}
16384
16385#[gpui::test]
16386fn test_highlighted_ranges(cx: &mut TestAppContext) {
16387 init_test(cx, |_| {});
16388
16389 let editor = cx.add_window(|window, cx| {
16390 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16391 build_editor(buffer, window, cx)
16392 });
16393
16394 _ = editor.update(cx, |editor, window, cx| {
16395 struct Type1;
16396 struct Type2;
16397
16398 let buffer = editor.buffer.read(cx).snapshot(cx);
16399
16400 let anchor_range =
16401 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16402
16403 editor.highlight_background::<Type1>(
16404 &[
16405 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16406 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16407 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16408 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16409 ],
16410 |_| Hsla::red(),
16411 cx,
16412 );
16413 editor.highlight_background::<Type2>(
16414 &[
16415 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16416 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16417 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16418 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16419 ],
16420 |_| Hsla::green(),
16421 cx,
16422 );
16423
16424 let snapshot = editor.snapshot(window, cx);
16425 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16426 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16427 &snapshot,
16428 cx.theme(),
16429 );
16430 assert_eq!(
16431 highlighted_ranges,
16432 &[
16433 (
16434 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16435 Hsla::green(),
16436 ),
16437 (
16438 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16439 Hsla::red(),
16440 ),
16441 (
16442 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16443 Hsla::green(),
16444 ),
16445 (
16446 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16447 Hsla::red(),
16448 ),
16449 ]
16450 );
16451 assert_eq!(
16452 editor.sorted_background_highlights_in_range(
16453 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16454 &snapshot,
16455 cx.theme(),
16456 ),
16457 &[(
16458 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16459 Hsla::red(),
16460 )]
16461 );
16462 });
16463}
16464
16465#[gpui::test]
16466async fn test_following(cx: &mut TestAppContext) {
16467 init_test(cx, |_| {});
16468
16469 let fs = FakeFs::new(cx.executor());
16470 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16471
16472 let buffer = project.update(cx, |project, cx| {
16473 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16474 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16475 });
16476 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16477 let follower = cx.update(|cx| {
16478 cx.open_window(
16479 WindowOptions {
16480 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16481 gpui::Point::new(px(0.), px(0.)),
16482 gpui::Point::new(px(10.), px(80.)),
16483 ))),
16484 ..Default::default()
16485 },
16486 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16487 )
16488 .unwrap()
16489 });
16490
16491 let is_still_following = Rc::new(RefCell::new(true));
16492 let follower_edit_event_count = Rc::new(RefCell::new(0));
16493 let pending_update = Rc::new(RefCell::new(None));
16494 let leader_entity = leader.root(cx).unwrap();
16495 let follower_entity = follower.root(cx).unwrap();
16496 _ = follower.update(cx, {
16497 let update = pending_update.clone();
16498 let is_still_following = is_still_following.clone();
16499 let follower_edit_event_count = follower_edit_event_count.clone();
16500 |_, window, cx| {
16501 cx.subscribe_in(
16502 &leader_entity,
16503 window,
16504 move |_, leader, event, window, cx| {
16505 leader.read(cx).add_event_to_update_proto(
16506 event,
16507 &mut update.borrow_mut(),
16508 window,
16509 cx,
16510 );
16511 },
16512 )
16513 .detach();
16514
16515 cx.subscribe_in(
16516 &follower_entity,
16517 window,
16518 move |_, _, event: &EditorEvent, _window, _cx| {
16519 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16520 *is_still_following.borrow_mut() = false;
16521 }
16522
16523 if let EditorEvent::BufferEdited = event {
16524 *follower_edit_event_count.borrow_mut() += 1;
16525 }
16526 },
16527 )
16528 .detach();
16529 }
16530 });
16531
16532 // Update the selections only
16533 _ = leader.update(cx, |leader, window, cx| {
16534 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16535 s.select_ranges([1..1])
16536 });
16537 });
16538 follower
16539 .update(cx, |follower, window, cx| {
16540 follower.apply_update_proto(
16541 &project,
16542 pending_update.borrow_mut().take().unwrap(),
16543 window,
16544 cx,
16545 )
16546 })
16547 .unwrap()
16548 .await
16549 .unwrap();
16550 _ = follower.update(cx, |follower, _, cx| {
16551 assert_eq!(
16552 follower.selections.ranges(&follower.display_snapshot(cx)),
16553 vec![1..1]
16554 );
16555 });
16556 assert!(*is_still_following.borrow());
16557 assert_eq!(*follower_edit_event_count.borrow(), 0);
16558
16559 // Update the scroll position only
16560 _ = leader.update(cx, |leader, window, cx| {
16561 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16562 });
16563 follower
16564 .update(cx, |follower, window, cx| {
16565 follower.apply_update_proto(
16566 &project,
16567 pending_update.borrow_mut().take().unwrap(),
16568 window,
16569 cx,
16570 )
16571 })
16572 .unwrap()
16573 .await
16574 .unwrap();
16575 assert_eq!(
16576 follower
16577 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16578 .unwrap(),
16579 gpui::Point::new(1.5, 3.5)
16580 );
16581 assert!(*is_still_following.borrow());
16582 assert_eq!(*follower_edit_event_count.borrow(), 0);
16583
16584 // Update the selections and scroll position. The follower's scroll position is updated
16585 // via autoscroll, not via the leader's exact scroll position.
16586 _ = leader.update(cx, |leader, window, cx| {
16587 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16588 s.select_ranges([0..0])
16589 });
16590 leader.request_autoscroll(Autoscroll::newest(), cx);
16591 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16592 });
16593 follower
16594 .update(cx, |follower, window, cx| {
16595 follower.apply_update_proto(
16596 &project,
16597 pending_update.borrow_mut().take().unwrap(),
16598 window,
16599 cx,
16600 )
16601 })
16602 .unwrap()
16603 .await
16604 .unwrap();
16605 _ = follower.update(cx, |follower, _, cx| {
16606 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16607 assert_eq!(
16608 follower.selections.ranges(&follower.display_snapshot(cx)),
16609 vec![0..0]
16610 );
16611 });
16612 assert!(*is_still_following.borrow());
16613
16614 // Creating a pending selection that precedes another selection
16615 _ = leader.update(cx, |leader, window, cx| {
16616 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16617 s.select_ranges([1..1])
16618 });
16619 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16620 });
16621 follower
16622 .update(cx, |follower, window, cx| {
16623 follower.apply_update_proto(
16624 &project,
16625 pending_update.borrow_mut().take().unwrap(),
16626 window,
16627 cx,
16628 )
16629 })
16630 .unwrap()
16631 .await
16632 .unwrap();
16633 _ = follower.update(cx, |follower, _, cx| {
16634 assert_eq!(
16635 follower.selections.ranges(&follower.display_snapshot(cx)),
16636 vec![0..0, 1..1]
16637 );
16638 });
16639 assert!(*is_still_following.borrow());
16640
16641 // Extend the pending selection so that it surrounds another selection
16642 _ = leader.update(cx, |leader, window, cx| {
16643 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16644 });
16645 follower
16646 .update(cx, |follower, window, cx| {
16647 follower.apply_update_proto(
16648 &project,
16649 pending_update.borrow_mut().take().unwrap(),
16650 window,
16651 cx,
16652 )
16653 })
16654 .unwrap()
16655 .await
16656 .unwrap();
16657 _ = follower.update(cx, |follower, _, cx| {
16658 assert_eq!(
16659 follower.selections.ranges(&follower.display_snapshot(cx)),
16660 vec![0..2]
16661 );
16662 });
16663
16664 // Scrolling locally breaks the follow
16665 _ = follower.update(cx, |follower, window, cx| {
16666 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16667 follower.set_scroll_anchor(
16668 ScrollAnchor {
16669 anchor: top_anchor,
16670 offset: gpui::Point::new(0.0, 0.5),
16671 },
16672 window,
16673 cx,
16674 );
16675 });
16676 assert!(!(*is_still_following.borrow()));
16677}
16678
16679#[gpui::test]
16680async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16681 init_test(cx, |_| {});
16682
16683 let fs = FakeFs::new(cx.executor());
16684 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16685 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16686 let pane = workspace
16687 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16688 .unwrap();
16689
16690 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16691
16692 let leader = pane.update_in(cx, |_, window, cx| {
16693 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16694 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16695 });
16696
16697 // Start following the editor when it has no excerpts.
16698 let mut state_message =
16699 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16700 let workspace_entity = workspace.root(cx).unwrap();
16701 let follower_1 = cx
16702 .update_window(*workspace.deref(), |_, window, cx| {
16703 Editor::from_state_proto(
16704 workspace_entity,
16705 ViewId {
16706 creator: CollaboratorId::PeerId(PeerId::default()),
16707 id: 0,
16708 },
16709 &mut state_message,
16710 window,
16711 cx,
16712 )
16713 })
16714 .unwrap()
16715 .unwrap()
16716 .await
16717 .unwrap();
16718
16719 let update_message = Rc::new(RefCell::new(None));
16720 follower_1.update_in(cx, {
16721 let update = update_message.clone();
16722 |_, window, cx| {
16723 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16724 leader.read(cx).add_event_to_update_proto(
16725 event,
16726 &mut update.borrow_mut(),
16727 window,
16728 cx,
16729 );
16730 })
16731 .detach();
16732 }
16733 });
16734
16735 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16736 (
16737 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16738 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16739 )
16740 });
16741
16742 // Insert some excerpts.
16743 leader.update(cx, |leader, cx| {
16744 leader.buffer.update(cx, |multibuffer, cx| {
16745 multibuffer.set_excerpts_for_path(
16746 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16747 buffer_1.clone(),
16748 vec![
16749 Point::row_range(0..3),
16750 Point::row_range(1..6),
16751 Point::row_range(12..15),
16752 ],
16753 0,
16754 cx,
16755 );
16756 multibuffer.set_excerpts_for_path(
16757 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16758 buffer_2.clone(),
16759 vec![Point::row_range(0..6), Point::row_range(8..12)],
16760 0,
16761 cx,
16762 );
16763 });
16764 });
16765
16766 // Apply the update of adding the excerpts.
16767 follower_1
16768 .update_in(cx, |follower, window, cx| {
16769 follower.apply_update_proto(
16770 &project,
16771 update_message.borrow().clone().unwrap(),
16772 window,
16773 cx,
16774 )
16775 })
16776 .await
16777 .unwrap();
16778 assert_eq!(
16779 follower_1.update(cx, |editor, cx| editor.text(cx)),
16780 leader.update(cx, |editor, cx| editor.text(cx))
16781 );
16782 update_message.borrow_mut().take();
16783
16784 // Start following separately after it already has excerpts.
16785 let mut state_message =
16786 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16787 let workspace_entity = workspace.root(cx).unwrap();
16788 let follower_2 = cx
16789 .update_window(*workspace.deref(), |_, window, cx| {
16790 Editor::from_state_proto(
16791 workspace_entity,
16792 ViewId {
16793 creator: CollaboratorId::PeerId(PeerId::default()),
16794 id: 0,
16795 },
16796 &mut state_message,
16797 window,
16798 cx,
16799 )
16800 })
16801 .unwrap()
16802 .unwrap()
16803 .await
16804 .unwrap();
16805 assert_eq!(
16806 follower_2.update(cx, |editor, cx| editor.text(cx)),
16807 leader.update(cx, |editor, cx| editor.text(cx))
16808 );
16809
16810 // Remove some excerpts.
16811 leader.update(cx, |leader, cx| {
16812 leader.buffer.update(cx, |multibuffer, cx| {
16813 let excerpt_ids = multibuffer.excerpt_ids();
16814 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16815 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16816 });
16817 });
16818
16819 // Apply the update of removing the excerpts.
16820 follower_1
16821 .update_in(cx, |follower, window, cx| {
16822 follower.apply_update_proto(
16823 &project,
16824 update_message.borrow().clone().unwrap(),
16825 window,
16826 cx,
16827 )
16828 })
16829 .await
16830 .unwrap();
16831 follower_2
16832 .update_in(cx, |follower, window, cx| {
16833 follower.apply_update_proto(
16834 &project,
16835 update_message.borrow().clone().unwrap(),
16836 window,
16837 cx,
16838 )
16839 })
16840 .await
16841 .unwrap();
16842 update_message.borrow_mut().take();
16843 assert_eq!(
16844 follower_1.update(cx, |editor, cx| editor.text(cx)),
16845 leader.update(cx, |editor, cx| editor.text(cx))
16846 );
16847}
16848
16849#[gpui::test]
16850async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16851 init_test(cx, |_| {});
16852
16853 let mut cx = EditorTestContext::new(cx).await;
16854 let lsp_store =
16855 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16856
16857 cx.set_state(indoc! {"
16858 ˇfn func(abc def: i32) -> u32 {
16859 }
16860 "});
16861
16862 cx.update(|_, cx| {
16863 lsp_store.update(cx, |lsp_store, cx| {
16864 lsp_store
16865 .update_diagnostics(
16866 LanguageServerId(0),
16867 lsp::PublishDiagnosticsParams {
16868 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16869 version: None,
16870 diagnostics: vec![
16871 lsp::Diagnostic {
16872 range: lsp::Range::new(
16873 lsp::Position::new(0, 11),
16874 lsp::Position::new(0, 12),
16875 ),
16876 severity: Some(lsp::DiagnosticSeverity::ERROR),
16877 ..Default::default()
16878 },
16879 lsp::Diagnostic {
16880 range: lsp::Range::new(
16881 lsp::Position::new(0, 12),
16882 lsp::Position::new(0, 15),
16883 ),
16884 severity: Some(lsp::DiagnosticSeverity::ERROR),
16885 ..Default::default()
16886 },
16887 lsp::Diagnostic {
16888 range: lsp::Range::new(
16889 lsp::Position::new(0, 25),
16890 lsp::Position::new(0, 28),
16891 ),
16892 severity: Some(lsp::DiagnosticSeverity::ERROR),
16893 ..Default::default()
16894 },
16895 ],
16896 },
16897 None,
16898 DiagnosticSourceKind::Pushed,
16899 &[],
16900 cx,
16901 )
16902 .unwrap()
16903 });
16904 });
16905
16906 executor.run_until_parked();
16907
16908 cx.update_editor(|editor, window, cx| {
16909 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16910 });
16911
16912 cx.assert_editor_state(indoc! {"
16913 fn func(abc def: i32) -> ˇu32 {
16914 }
16915 "});
16916
16917 cx.update_editor(|editor, window, cx| {
16918 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16919 });
16920
16921 cx.assert_editor_state(indoc! {"
16922 fn func(abc ˇdef: i32) -> u32 {
16923 }
16924 "});
16925
16926 cx.update_editor(|editor, window, cx| {
16927 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16928 });
16929
16930 cx.assert_editor_state(indoc! {"
16931 fn func(abcˇ def: i32) -> u32 {
16932 }
16933 "});
16934
16935 cx.update_editor(|editor, window, cx| {
16936 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16937 });
16938
16939 cx.assert_editor_state(indoc! {"
16940 fn func(abc def: i32) -> ˇu32 {
16941 }
16942 "});
16943}
16944
16945#[gpui::test]
16946async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16947 init_test(cx, |_| {});
16948
16949 let mut cx = EditorTestContext::new(cx).await;
16950
16951 let diff_base = r#"
16952 use some::mod;
16953
16954 const A: u32 = 42;
16955
16956 fn main() {
16957 println!("hello");
16958
16959 println!("world");
16960 }
16961 "#
16962 .unindent();
16963
16964 // Edits are modified, removed, modified, added
16965 cx.set_state(
16966 &r#"
16967 use some::modified;
16968
16969 ˇ
16970 fn main() {
16971 println!("hello there");
16972
16973 println!("around the");
16974 println!("world");
16975 }
16976 "#
16977 .unindent(),
16978 );
16979
16980 cx.set_head_text(&diff_base);
16981 executor.run_until_parked();
16982
16983 cx.update_editor(|editor, window, cx| {
16984 //Wrap around the bottom of the buffer
16985 for _ in 0..3 {
16986 editor.go_to_next_hunk(&GoToHunk, window, cx);
16987 }
16988 });
16989
16990 cx.assert_editor_state(
16991 &r#"
16992 ˇuse some::modified;
16993
16994
16995 fn main() {
16996 println!("hello there");
16997
16998 println!("around the");
16999 println!("world");
17000 }
17001 "#
17002 .unindent(),
17003 );
17004
17005 cx.update_editor(|editor, window, cx| {
17006 //Wrap around the top of the buffer
17007 for _ in 0..2 {
17008 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17009 }
17010 });
17011
17012 cx.assert_editor_state(
17013 &r#"
17014 use some::modified;
17015
17016
17017 fn main() {
17018 ˇ println!("hello there");
17019
17020 println!("around the");
17021 println!("world");
17022 }
17023 "#
17024 .unindent(),
17025 );
17026
17027 cx.update_editor(|editor, window, cx| {
17028 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17029 });
17030
17031 cx.assert_editor_state(
17032 &r#"
17033 use some::modified;
17034
17035 ˇ
17036 fn main() {
17037 println!("hello there");
17038
17039 println!("around the");
17040 println!("world");
17041 }
17042 "#
17043 .unindent(),
17044 );
17045
17046 cx.update_editor(|editor, window, cx| {
17047 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17048 });
17049
17050 cx.assert_editor_state(
17051 &r#"
17052 ˇuse some::modified;
17053
17054
17055 fn main() {
17056 println!("hello there");
17057
17058 println!("around the");
17059 println!("world");
17060 }
17061 "#
17062 .unindent(),
17063 );
17064
17065 cx.update_editor(|editor, window, cx| {
17066 for _ in 0..2 {
17067 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17068 }
17069 });
17070
17071 cx.assert_editor_state(
17072 &r#"
17073 use some::modified;
17074
17075
17076 fn main() {
17077 ˇ println!("hello there");
17078
17079 println!("around the");
17080 println!("world");
17081 }
17082 "#
17083 .unindent(),
17084 );
17085
17086 cx.update_editor(|editor, window, cx| {
17087 editor.fold(&Fold, window, cx);
17088 });
17089
17090 cx.update_editor(|editor, window, cx| {
17091 editor.go_to_next_hunk(&GoToHunk, window, cx);
17092 });
17093
17094 cx.assert_editor_state(
17095 &r#"
17096 ˇuse some::modified;
17097
17098
17099 fn main() {
17100 println!("hello there");
17101
17102 println!("around the");
17103 println!("world");
17104 }
17105 "#
17106 .unindent(),
17107 );
17108}
17109
17110#[test]
17111fn test_split_words() {
17112 fn split(text: &str) -> Vec<&str> {
17113 split_words(text).collect()
17114 }
17115
17116 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17117 assert_eq!(split("hello_world"), &["hello_", "world"]);
17118 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17119 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17120 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17121 assert_eq!(split("helloworld"), &["helloworld"]);
17122
17123 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17124}
17125
17126#[gpui::test]
17127async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17128 init_test(cx, |_| {});
17129
17130 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17131 let mut assert = |before, after| {
17132 let _state_context = cx.set_state(before);
17133 cx.run_until_parked();
17134 cx.update_editor(|editor, window, cx| {
17135 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17136 });
17137 cx.run_until_parked();
17138 cx.assert_editor_state(after);
17139 };
17140
17141 // Outside bracket jumps to outside of matching bracket
17142 assert("console.logˇ(var);", "console.log(var)ˇ;");
17143 assert("console.log(var)ˇ;", "console.logˇ(var);");
17144
17145 // Inside bracket jumps to inside of matching bracket
17146 assert("console.log(ˇvar);", "console.log(varˇ);");
17147 assert("console.log(varˇ);", "console.log(ˇvar);");
17148
17149 // When outside a bracket and inside, favor jumping to the inside bracket
17150 assert(
17151 "console.log('foo', [1, 2, 3]ˇ);",
17152 "console.log(ˇ'foo', [1, 2, 3]);",
17153 );
17154 assert(
17155 "console.log(ˇ'foo', [1, 2, 3]);",
17156 "console.log('foo', [1, 2, 3]ˇ);",
17157 );
17158
17159 // Bias forward if two options are equally likely
17160 assert(
17161 "let result = curried_fun()ˇ();",
17162 "let result = curried_fun()()ˇ;",
17163 );
17164
17165 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17166 assert(
17167 indoc! {"
17168 function test() {
17169 console.log('test')ˇ
17170 }"},
17171 indoc! {"
17172 function test() {
17173 console.logˇ('test')
17174 }"},
17175 );
17176}
17177
17178#[gpui::test]
17179async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17180 init_test(cx, |_| {});
17181
17182 let fs = FakeFs::new(cx.executor());
17183 fs.insert_tree(
17184 path!("/a"),
17185 json!({
17186 "main.rs": "fn main() { let a = 5; }",
17187 "other.rs": "// Test file",
17188 }),
17189 )
17190 .await;
17191 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17192
17193 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17194 language_registry.add(Arc::new(Language::new(
17195 LanguageConfig {
17196 name: "Rust".into(),
17197 matcher: LanguageMatcher {
17198 path_suffixes: vec!["rs".to_string()],
17199 ..Default::default()
17200 },
17201 brackets: BracketPairConfig {
17202 pairs: vec![BracketPair {
17203 start: "{".to_string(),
17204 end: "}".to_string(),
17205 close: true,
17206 surround: true,
17207 newline: true,
17208 }],
17209 disabled_scopes_by_bracket_ix: Vec::new(),
17210 },
17211 ..Default::default()
17212 },
17213 Some(tree_sitter_rust::LANGUAGE.into()),
17214 )));
17215 let mut fake_servers = language_registry.register_fake_lsp(
17216 "Rust",
17217 FakeLspAdapter {
17218 capabilities: lsp::ServerCapabilities {
17219 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17220 first_trigger_character: "{".to_string(),
17221 more_trigger_character: None,
17222 }),
17223 ..Default::default()
17224 },
17225 ..Default::default()
17226 },
17227 );
17228
17229 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17230
17231 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17232
17233 let worktree_id = workspace
17234 .update(cx, |workspace, _, cx| {
17235 workspace.project().update(cx, |project, cx| {
17236 project.worktrees(cx).next().unwrap().read(cx).id()
17237 })
17238 })
17239 .unwrap();
17240
17241 let buffer = project
17242 .update(cx, |project, cx| {
17243 project.open_local_buffer(path!("/a/main.rs"), cx)
17244 })
17245 .await
17246 .unwrap();
17247 let editor_handle = workspace
17248 .update(cx, |workspace, window, cx| {
17249 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17250 })
17251 .unwrap()
17252 .await
17253 .unwrap()
17254 .downcast::<Editor>()
17255 .unwrap();
17256
17257 cx.executor().start_waiting();
17258 let fake_server = fake_servers.next().await.unwrap();
17259
17260 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17261 |params, _| async move {
17262 assert_eq!(
17263 params.text_document_position.text_document.uri,
17264 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17265 );
17266 assert_eq!(
17267 params.text_document_position.position,
17268 lsp::Position::new(0, 21),
17269 );
17270
17271 Ok(Some(vec![lsp::TextEdit {
17272 new_text: "]".to_string(),
17273 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17274 }]))
17275 },
17276 );
17277
17278 editor_handle.update_in(cx, |editor, window, cx| {
17279 window.focus(&editor.focus_handle(cx));
17280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17281 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17282 });
17283 editor.handle_input("{", window, cx);
17284 });
17285
17286 cx.executor().run_until_parked();
17287
17288 buffer.update(cx, |buffer, _| {
17289 assert_eq!(
17290 buffer.text(),
17291 "fn main() { let a = {5}; }",
17292 "No extra braces from on type formatting should appear in the buffer"
17293 )
17294 });
17295}
17296
17297#[gpui::test(iterations = 20, seeds(31))]
17298async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17299 init_test(cx, |_| {});
17300
17301 let mut cx = EditorLspTestContext::new_rust(
17302 lsp::ServerCapabilities {
17303 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17304 first_trigger_character: ".".to_string(),
17305 more_trigger_character: None,
17306 }),
17307 ..Default::default()
17308 },
17309 cx,
17310 )
17311 .await;
17312
17313 cx.update_buffer(|buffer, _| {
17314 // This causes autoindent to be async.
17315 buffer.set_sync_parse_timeout(Duration::ZERO)
17316 });
17317
17318 cx.set_state("fn c() {\n d()ˇ\n}\n");
17319 cx.simulate_keystroke("\n");
17320 cx.run_until_parked();
17321
17322 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17323 let mut request =
17324 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17325 let buffer_cloned = buffer_cloned.clone();
17326 async move {
17327 buffer_cloned.update(&mut cx, |buffer, _| {
17328 assert_eq!(
17329 buffer.text(),
17330 "fn c() {\n d()\n .\n}\n",
17331 "OnTypeFormatting should triggered after autoindent applied"
17332 )
17333 })?;
17334
17335 Ok(Some(vec![]))
17336 }
17337 });
17338
17339 cx.simulate_keystroke(".");
17340 cx.run_until_parked();
17341
17342 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17343 assert!(request.next().await.is_some());
17344 request.close();
17345 assert!(request.next().await.is_none());
17346}
17347
17348#[gpui::test]
17349async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17350 init_test(cx, |_| {});
17351
17352 let fs = FakeFs::new(cx.executor());
17353 fs.insert_tree(
17354 path!("/a"),
17355 json!({
17356 "main.rs": "fn main() { let a = 5; }",
17357 "other.rs": "// Test file",
17358 }),
17359 )
17360 .await;
17361
17362 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17363
17364 let server_restarts = Arc::new(AtomicUsize::new(0));
17365 let closure_restarts = Arc::clone(&server_restarts);
17366 let language_server_name = "test language server";
17367 let language_name: LanguageName = "Rust".into();
17368
17369 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17370 language_registry.add(Arc::new(Language::new(
17371 LanguageConfig {
17372 name: language_name.clone(),
17373 matcher: LanguageMatcher {
17374 path_suffixes: vec!["rs".to_string()],
17375 ..Default::default()
17376 },
17377 ..Default::default()
17378 },
17379 Some(tree_sitter_rust::LANGUAGE.into()),
17380 )));
17381 let mut fake_servers = language_registry.register_fake_lsp(
17382 "Rust",
17383 FakeLspAdapter {
17384 name: language_server_name,
17385 initialization_options: Some(json!({
17386 "testOptionValue": true
17387 })),
17388 initializer: Some(Box::new(move |fake_server| {
17389 let task_restarts = Arc::clone(&closure_restarts);
17390 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17391 task_restarts.fetch_add(1, atomic::Ordering::Release);
17392 futures::future::ready(Ok(()))
17393 });
17394 })),
17395 ..Default::default()
17396 },
17397 );
17398
17399 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17400 let _buffer = project
17401 .update(cx, |project, cx| {
17402 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17403 })
17404 .await
17405 .unwrap();
17406 let _fake_server = fake_servers.next().await.unwrap();
17407 update_test_language_settings(cx, |language_settings| {
17408 language_settings.languages.0.insert(
17409 language_name.clone().0,
17410 LanguageSettingsContent {
17411 tab_size: NonZeroU32::new(8),
17412 ..Default::default()
17413 },
17414 );
17415 });
17416 cx.executor().run_until_parked();
17417 assert_eq!(
17418 server_restarts.load(atomic::Ordering::Acquire),
17419 0,
17420 "Should not restart LSP server on an unrelated change"
17421 );
17422
17423 update_test_project_settings(cx, |project_settings| {
17424 project_settings.lsp.insert(
17425 "Some other server name".into(),
17426 LspSettings {
17427 binary: None,
17428 settings: None,
17429 initialization_options: Some(json!({
17430 "some other init value": false
17431 })),
17432 enable_lsp_tasks: false,
17433 fetch: None,
17434 },
17435 );
17436 });
17437 cx.executor().run_until_parked();
17438 assert_eq!(
17439 server_restarts.load(atomic::Ordering::Acquire),
17440 0,
17441 "Should not restart LSP server on an unrelated LSP settings change"
17442 );
17443
17444 update_test_project_settings(cx, |project_settings| {
17445 project_settings.lsp.insert(
17446 language_server_name.into(),
17447 LspSettings {
17448 binary: None,
17449 settings: None,
17450 initialization_options: Some(json!({
17451 "anotherInitValue": false
17452 })),
17453 enable_lsp_tasks: false,
17454 fetch: None,
17455 },
17456 );
17457 });
17458 cx.executor().run_until_parked();
17459 assert_eq!(
17460 server_restarts.load(atomic::Ordering::Acquire),
17461 1,
17462 "Should restart LSP server on a related LSP settings change"
17463 );
17464
17465 update_test_project_settings(cx, |project_settings| {
17466 project_settings.lsp.insert(
17467 language_server_name.into(),
17468 LspSettings {
17469 binary: None,
17470 settings: None,
17471 initialization_options: Some(json!({
17472 "anotherInitValue": false
17473 })),
17474 enable_lsp_tasks: false,
17475 fetch: None,
17476 },
17477 );
17478 });
17479 cx.executor().run_until_parked();
17480 assert_eq!(
17481 server_restarts.load(atomic::Ordering::Acquire),
17482 1,
17483 "Should not restart LSP server on a related LSP settings change that is the same"
17484 );
17485
17486 update_test_project_settings(cx, |project_settings| {
17487 project_settings.lsp.insert(
17488 language_server_name.into(),
17489 LspSettings {
17490 binary: None,
17491 settings: None,
17492 initialization_options: None,
17493 enable_lsp_tasks: false,
17494 fetch: None,
17495 },
17496 );
17497 });
17498 cx.executor().run_until_parked();
17499 assert_eq!(
17500 server_restarts.load(atomic::Ordering::Acquire),
17501 2,
17502 "Should restart LSP server on another related LSP settings change"
17503 );
17504}
17505
17506#[gpui::test]
17507async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17508 init_test(cx, |_| {});
17509
17510 let mut cx = EditorLspTestContext::new_rust(
17511 lsp::ServerCapabilities {
17512 completion_provider: Some(lsp::CompletionOptions {
17513 trigger_characters: Some(vec![".".to_string()]),
17514 resolve_provider: Some(true),
17515 ..Default::default()
17516 }),
17517 ..Default::default()
17518 },
17519 cx,
17520 )
17521 .await;
17522
17523 cx.set_state("fn main() { let a = 2ˇ; }");
17524 cx.simulate_keystroke(".");
17525 let completion_item = lsp::CompletionItem {
17526 label: "some".into(),
17527 kind: Some(lsp::CompletionItemKind::SNIPPET),
17528 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17529 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17530 kind: lsp::MarkupKind::Markdown,
17531 value: "```rust\nSome(2)\n```".to_string(),
17532 })),
17533 deprecated: Some(false),
17534 sort_text: Some("fffffff2".to_string()),
17535 filter_text: Some("some".to_string()),
17536 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17537 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17538 range: lsp::Range {
17539 start: lsp::Position {
17540 line: 0,
17541 character: 22,
17542 },
17543 end: lsp::Position {
17544 line: 0,
17545 character: 22,
17546 },
17547 },
17548 new_text: "Some(2)".to_string(),
17549 })),
17550 additional_text_edits: Some(vec![lsp::TextEdit {
17551 range: lsp::Range {
17552 start: lsp::Position {
17553 line: 0,
17554 character: 20,
17555 },
17556 end: lsp::Position {
17557 line: 0,
17558 character: 22,
17559 },
17560 },
17561 new_text: "".to_string(),
17562 }]),
17563 ..Default::default()
17564 };
17565
17566 let closure_completion_item = completion_item.clone();
17567 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17568 let task_completion_item = closure_completion_item.clone();
17569 async move {
17570 Ok(Some(lsp::CompletionResponse::Array(vec![
17571 task_completion_item,
17572 ])))
17573 }
17574 });
17575
17576 request.next().await;
17577
17578 cx.condition(|editor, _| editor.context_menu_visible())
17579 .await;
17580 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17581 editor
17582 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17583 .unwrap()
17584 });
17585 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17586
17587 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17588 let task_completion_item = completion_item.clone();
17589 async move { Ok(task_completion_item) }
17590 })
17591 .next()
17592 .await
17593 .unwrap();
17594 apply_additional_edits.await.unwrap();
17595 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17596}
17597
17598#[gpui::test]
17599async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17600 init_test(cx, |_| {});
17601
17602 let mut cx = EditorLspTestContext::new_rust(
17603 lsp::ServerCapabilities {
17604 completion_provider: Some(lsp::CompletionOptions {
17605 trigger_characters: Some(vec![".".to_string()]),
17606 resolve_provider: Some(true),
17607 ..Default::default()
17608 }),
17609 ..Default::default()
17610 },
17611 cx,
17612 )
17613 .await;
17614
17615 cx.set_state("fn main() { let a = 2ˇ; }");
17616 cx.simulate_keystroke(".");
17617
17618 let item1 = lsp::CompletionItem {
17619 label: "method id()".to_string(),
17620 filter_text: Some("id".to_string()),
17621 detail: None,
17622 documentation: None,
17623 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17624 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17625 new_text: ".id".to_string(),
17626 })),
17627 ..lsp::CompletionItem::default()
17628 };
17629
17630 let item2 = lsp::CompletionItem {
17631 label: "other".to_string(),
17632 filter_text: Some("other".to_string()),
17633 detail: None,
17634 documentation: None,
17635 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17636 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17637 new_text: ".other".to_string(),
17638 })),
17639 ..lsp::CompletionItem::default()
17640 };
17641
17642 let item1 = item1.clone();
17643 cx.set_request_handler::<lsp::request::Completion, _, _>({
17644 let item1 = item1.clone();
17645 move |_, _, _| {
17646 let item1 = item1.clone();
17647 let item2 = item2.clone();
17648 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17649 }
17650 })
17651 .next()
17652 .await;
17653
17654 cx.condition(|editor, _| editor.context_menu_visible())
17655 .await;
17656 cx.update_editor(|editor, _, _| {
17657 let context_menu = editor.context_menu.borrow_mut();
17658 let context_menu = context_menu
17659 .as_ref()
17660 .expect("Should have the context menu deployed");
17661 match context_menu {
17662 CodeContextMenu::Completions(completions_menu) => {
17663 let completions = completions_menu.completions.borrow_mut();
17664 assert_eq!(
17665 completions
17666 .iter()
17667 .map(|completion| &completion.label.text)
17668 .collect::<Vec<_>>(),
17669 vec!["method id()", "other"]
17670 )
17671 }
17672 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17673 }
17674 });
17675
17676 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17677 let item1 = item1.clone();
17678 move |_, item_to_resolve, _| {
17679 let item1 = item1.clone();
17680 async move {
17681 if item1 == item_to_resolve {
17682 Ok(lsp::CompletionItem {
17683 label: "method id()".to_string(),
17684 filter_text: Some("id".to_string()),
17685 detail: Some("Now resolved!".to_string()),
17686 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17687 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17688 range: lsp::Range::new(
17689 lsp::Position::new(0, 22),
17690 lsp::Position::new(0, 22),
17691 ),
17692 new_text: ".id".to_string(),
17693 })),
17694 ..lsp::CompletionItem::default()
17695 })
17696 } else {
17697 Ok(item_to_resolve)
17698 }
17699 }
17700 }
17701 })
17702 .next()
17703 .await
17704 .unwrap();
17705 cx.run_until_parked();
17706
17707 cx.update_editor(|editor, window, cx| {
17708 editor.context_menu_next(&Default::default(), window, cx);
17709 });
17710
17711 cx.update_editor(|editor, _, _| {
17712 let context_menu = editor.context_menu.borrow_mut();
17713 let context_menu = context_menu
17714 .as_ref()
17715 .expect("Should have the context menu deployed");
17716 match context_menu {
17717 CodeContextMenu::Completions(completions_menu) => {
17718 let completions = completions_menu.completions.borrow_mut();
17719 assert_eq!(
17720 completions
17721 .iter()
17722 .map(|completion| &completion.label.text)
17723 .collect::<Vec<_>>(),
17724 vec!["method id() Now resolved!", "other"],
17725 "Should update first completion label, but not second as the filter text did not match."
17726 );
17727 }
17728 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17729 }
17730 });
17731}
17732
17733#[gpui::test]
17734async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17735 init_test(cx, |_| {});
17736 let mut cx = EditorLspTestContext::new_rust(
17737 lsp::ServerCapabilities {
17738 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17739 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17740 completion_provider: Some(lsp::CompletionOptions {
17741 resolve_provider: Some(true),
17742 ..Default::default()
17743 }),
17744 ..Default::default()
17745 },
17746 cx,
17747 )
17748 .await;
17749 cx.set_state(indoc! {"
17750 struct TestStruct {
17751 field: i32
17752 }
17753
17754 fn mainˇ() {
17755 let unused_var = 42;
17756 let test_struct = TestStruct { field: 42 };
17757 }
17758 "});
17759 let symbol_range = cx.lsp_range(indoc! {"
17760 struct TestStruct {
17761 field: i32
17762 }
17763
17764 «fn main»() {
17765 let unused_var = 42;
17766 let test_struct = TestStruct { field: 42 };
17767 }
17768 "});
17769 let mut hover_requests =
17770 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17771 Ok(Some(lsp::Hover {
17772 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17773 kind: lsp::MarkupKind::Markdown,
17774 value: "Function documentation".to_string(),
17775 }),
17776 range: Some(symbol_range),
17777 }))
17778 });
17779
17780 // Case 1: Test that code action menu hide hover popover
17781 cx.dispatch_action(Hover);
17782 hover_requests.next().await;
17783 cx.condition(|editor, _| editor.hover_state.visible()).await;
17784 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17785 move |_, _, _| async move {
17786 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17787 lsp::CodeAction {
17788 title: "Remove unused variable".to_string(),
17789 kind: Some(CodeActionKind::QUICKFIX),
17790 edit: Some(lsp::WorkspaceEdit {
17791 changes: Some(
17792 [(
17793 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17794 vec![lsp::TextEdit {
17795 range: lsp::Range::new(
17796 lsp::Position::new(5, 4),
17797 lsp::Position::new(5, 27),
17798 ),
17799 new_text: "".to_string(),
17800 }],
17801 )]
17802 .into_iter()
17803 .collect(),
17804 ),
17805 ..Default::default()
17806 }),
17807 ..Default::default()
17808 },
17809 )]))
17810 },
17811 );
17812 cx.update_editor(|editor, window, cx| {
17813 editor.toggle_code_actions(
17814 &ToggleCodeActions {
17815 deployed_from: None,
17816 quick_launch: false,
17817 },
17818 window,
17819 cx,
17820 );
17821 });
17822 code_action_requests.next().await;
17823 cx.run_until_parked();
17824 cx.condition(|editor, _| editor.context_menu_visible())
17825 .await;
17826 cx.update_editor(|editor, _, _| {
17827 assert!(
17828 !editor.hover_state.visible(),
17829 "Hover popover should be hidden when code action menu is shown"
17830 );
17831 // Hide code actions
17832 editor.context_menu.take();
17833 });
17834
17835 // Case 2: Test that code completions hide hover popover
17836 cx.dispatch_action(Hover);
17837 hover_requests.next().await;
17838 cx.condition(|editor, _| editor.hover_state.visible()).await;
17839 let counter = Arc::new(AtomicUsize::new(0));
17840 let mut completion_requests =
17841 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17842 let counter = counter.clone();
17843 async move {
17844 counter.fetch_add(1, atomic::Ordering::Release);
17845 Ok(Some(lsp::CompletionResponse::Array(vec![
17846 lsp::CompletionItem {
17847 label: "main".into(),
17848 kind: Some(lsp::CompletionItemKind::FUNCTION),
17849 detail: Some("() -> ()".to_string()),
17850 ..Default::default()
17851 },
17852 lsp::CompletionItem {
17853 label: "TestStruct".into(),
17854 kind: Some(lsp::CompletionItemKind::STRUCT),
17855 detail: Some("struct TestStruct".to_string()),
17856 ..Default::default()
17857 },
17858 ])))
17859 }
17860 });
17861 cx.update_editor(|editor, window, cx| {
17862 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17863 });
17864 completion_requests.next().await;
17865 cx.condition(|editor, _| editor.context_menu_visible())
17866 .await;
17867 cx.update_editor(|editor, _, _| {
17868 assert!(
17869 !editor.hover_state.visible(),
17870 "Hover popover should be hidden when completion menu is shown"
17871 );
17872 });
17873}
17874
17875#[gpui::test]
17876async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17877 init_test(cx, |_| {});
17878
17879 let mut cx = EditorLspTestContext::new_rust(
17880 lsp::ServerCapabilities {
17881 completion_provider: Some(lsp::CompletionOptions {
17882 trigger_characters: Some(vec![".".to_string()]),
17883 resolve_provider: Some(true),
17884 ..Default::default()
17885 }),
17886 ..Default::default()
17887 },
17888 cx,
17889 )
17890 .await;
17891
17892 cx.set_state("fn main() { let a = 2ˇ; }");
17893 cx.simulate_keystroke(".");
17894
17895 let unresolved_item_1 = lsp::CompletionItem {
17896 label: "id".to_string(),
17897 filter_text: Some("id".to_string()),
17898 detail: None,
17899 documentation: None,
17900 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17901 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17902 new_text: ".id".to_string(),
17903 })),
17904 ..lsp::CompletionItem::default()
17905 };
17906 let resolved_item_1 = lsp::CompletionItem {
17907 additional_text_edits: Some(vec![lsp::TextEdit {
17908 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17909 new_text: "!!".to_string(),
17910 }]),
17911 ..unresolved_item_1.clone()
17912 };
17913 let unresolved_item_2 = lsp::CompletionItem {
17914 label: "other".to_string(),
17915 filter_text: Some("other".to_string()),
17916 detail: None,
17917 documentation: None,
17918 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17919 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17920 new_text: ".other".to_string(),
17921 })),
17922 ..lsp::CompletionItem::default()
17923 };
17924 let resolved_item_2 = lsp::CompletionItem {
17925 additional_text_edits: Some(vec![lsp::TextEdit {
17926 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17927 new_text: "??".to_string(),
17928 }]),
17929 ..unresolved_item_2.clone()
17930 };
17931
17932 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17933 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17934 cx.lsp
17935 .server
17936 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17937 let unresolved_item_1 = unresolved_item_1.clone();
17938 let resolved_item_1 = resolved_item_1.clone();
17939 let unresolved_item_2 = unresolved_item_2.clone();
17940 let resolved_item_2 = resolved_item_2.clone();
17941 let resolve_requests_1 = resolve_requests_1.clone();
17942 let resolve_requests_2 = resolve_requests_2.clone();
17943 move |unresolved_request, _| {
17944 let unresolved_item_1 = unresolved_item_1.clone();
17945 let resolved_item_1 = resolved_item_1.clone();
17946 let unresolved_item_2 = unresolved_item_2.clone();
17947 let resolved_item_2 = resolved_item_2.clone();
17948 let resolve_requests_1 = resolve_requests_1.clone();
17949 let resolve_requests_2 = resolve_requests_2.clone();
17950 async move {
17951 if unresolved_request == unresolved_item_1 {
17952 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17953 Ok(resolved_item_1.clone())
17954 } else if unresolved_request == unresolved_item_2 {
17955 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17956 Ok(resolved_item_2.clone())
17957 } else {
17958 panic!("Unexpected completion item {unresolved_request:?}")
17959 }
17960 }
17961 }
17962 })
17963 .detach();
17964
17965 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17966 let unresolved_item_1 = unresolved_item_1.clone();
17967 let unresolved_item_2 = unresolved_item_2.clone();
17968 async move {
17969 Ok(Some(lsp::CompletionResponse::Array(vec![
17970 unresolved_item_1,
17971 unresolved_item_2,
17972 ])))
17973 }
17974 })
17975 .next()
17976 .await;
17977
17978 cx.condition(|editor, _| editor.context_menu_visible())
17979 .await;
17980 cx.update_editor(|editor, _, _| {
17981 let context_menu = editor.context_menu.borrow_mut();
17982 let context_menu = context_menu
17983 .as_ref()
17984 .expect("Should have the context menu deployed");
17985 match context_menu {
17986 CodeContextMenu::Completions(completions_menu) => {
17987 let completions = completions_menu.completions.borrow_mut();
17988 assert_eq!(
17989 completions
17990 .iter()
17991 .map(|completion| &completion.label.text)
17992 .collect::<Vec<_>>(),
17993 vec!["id", "other"]
17994 )
17995 }
17996 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17997 }
17998 });
17999 cx.run_until_parked();
18000
18001 cx.update_editor(|editor, window, cx| {
18002 editor.context_menu_next(&ContextMenuNext, window, cx);
18003 });
18004 cx.run_until_parked();
18005 cx.update_editor(|editor, window, cx| {
18006 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18007 });
18008 cx.run_until_parked();
18009 cx.update_editor(|editor, window, cx| {
18010 editor.context_menu_next(&ContextMenuNext, window, cx);
18011 });
18012 cx.run_until_parked();
18013 cx.update_editor(|editor, window, cx| {
18014 editor
18015 .compose_completion(&ComposeCompletion::default(), window, cx)
18016 .expect("No task returned")
18017 })
18018 .await
18019 .expect("Completion failed");
18020 cx.run_until_parked();
18021
18022 cx.update_editor(|editor, _, cx| {
18023 assert_eq!(
18024 resolve_requests_1.load(atomic::Ordering::Acquire),
18025 1,
18026 "Should always resolve once despite multiple selections"
18027 );
18028 assert_eq!(
18029 resolve_requests_2.load(atomic::Ordering::Acquire),
18030 1,
18031 "Should always resolve once after multiple selections and applying the completion"
18032 );
18033 assert_eq!(
18034 editor.text(cx),
18035 "fn main() { let a = ??.other; }",
18036 "Should use resolved data when applying the completion"
18037 );
18038 });
18039}
18040
18041#[gpui::test]
18042async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18043 init_test(cx, |_| {});
18044
18045 let item_0 = lsp::CompletionItem {
18046 label: "abs".into(),
18047 insert_text: Some("abs".into()),
18048 data: Some(json!({ "very": "special"})),
18049 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18050 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18051 lsp::InsertReplaceEdit {
18052 new_text: "abs".to_string(),
18053 insert: lsp::Range::default(),
18054 replace: lsp::Range::default(),
18055 },
18056 )),
18057 ..lsp::CompletionItem::default()
18058 };
18059 let items = iter::once(item_0.clone())
18060 .chain((11..51).map(|i| lsp::CompletionItem {
18061 label: format!("item_{}", i),
18062 insert_text: Some(format!("item_{}", i)),
18063 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18064 ..lsp::CompletionItem::default()
18065 }))
18066 .collect::<Vec<_>>();
18067
18068 let default_commit_characters = vec!["?".to_string()];
18069 let default_data = json!({ "default": "data"});
18070 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18071 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18072 let default_edit_range = lsp::Range {
18073 start: lsp::Position {
18074 line: 0,
18075 character: 5,
18076 },
18077 end: lsp::Position {
18078 line: 0,
18079 character: 5,
18080 },
18081 };
18082
18083 let mut cx = EditorLspTestContext::new_rust(
18084 lsp::ServerCapabilities {
18085 completion_provider: Some(lsp::CompletionOptions {
18086 trigger_characters: Some(vec![".".to_string()]),
18087 resolve_provider: Some(true),
18088 ..Default::default()
18089 }),
18090 ..Default::default()
18091 },
18092 cx,
18093 )
18094 .await;
18095
18096 cx.set_state("fn main() { let a = 2ˇ; }");
18097 cx.simulate_keystroke(".");
18098
18099 let completion_data = default_data.clone();
18100 let completion_characters = default_commit_characters.clone();
18101 let completion_items = items.clone();
18102 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18103 let default_data = completion_data.clone();
18104 let default_commit_characters = completion_characters.clone();
18105 let items = completion_items.clone();
18106 async move {
18107 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18108 items,
18109 item_defaults: Some(lsp::CompletionListItemDefaults {
18110 data: Some(default_data.clone()),
18111 commit_characters: Some(default_commit_characters.clone()),
18112 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18113 default_edit_range,
18114 )),
18115 insert_text_format: Some(default_insert_text_format),
18116 insert_text_mode: Some(default_insert_text_mode),
18117 }),
18118 ..lsp::CompletionList::default()
18119 })))
18120 }
18121 })
18122 .next()
18123 .await;
18124
18125 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18126 cx.lsp
18127 .server
18128 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18129 let closure_resolved_items = resolved_items.clone();
18130 move |item_to_resolve, _| {
18131 let closure_resolved_items = closure_resolved_items.clone();
18132 async move {
18133 closure_resolved_items.lock().push(item_to_resolve.clone());
18134 Ok(item_to_resolve)
18135 }
18136 }
18137 })
18138 .detach();
18139
18140 cx.condition(|editor, _| editor.context_menu_visible())
18141 .await;
18142 cx.run_until_parked();
18143 cx.update_editor(|editor, _, _| {
18144 let menu = editor.context_menu.borrow_mut();
18145 match menu.as_ref().expect("should have the completions menu") {
18146 CodeContextMenu::Completions(completions_menu) => {
18147 assert_eq!(
18148 completions_menu
18149 .entries
18150 .borrow()
18151 .iter()
18152 .map(|mat| mat.string.clone())
18153 .collect::<Vec<String>>(),
18154 items
18155 .iter()
18156 .map(|completion| completion.label.clone())
18157 .collect::<Vec<String>>()
18158 );
18159 }
18160 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18161 }
18162 });
18163 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18164 // with 4 from the end.
18165 assert_eq!(
18166 *resolved_items.lock(),
18167 [&items[0..16], &items[items.len() - 4..items.len()]]
18168 .concat()
18169 .iter()
18170 .cloned()
18171 .map(|mut item| {
18172 if item.data.is_none() {
18173 item.data = Some(default_data.clone());
18174 }
18175 item
18176 })
18177 .collect::<Vec<lsp::CompletionItem>>(),
18178 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18179 );
18180 resolved_items.lock().clear();
18181
18182 cx.update_editor(|editor, window, cx| {
18183 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18184 });
18185 cx.run_until_parked();
18186 // Completions that have already been resolved are skipped.
18187 assert_eq!(
18188 *resolved_items.lock(),
18189 items[items.len() - 17..items.len() - 4]
18190 .iter()
18191 .cloned()
18192 .map(|mut item| {
18193 if item.data.is_none() {
18194 item.data = Some(default_data.clone());
18195 }
18196 item
18197 })
18198 .collect::<Vec<lsp::CompletionItem>>()
18199 );
18200 resolved_items.lock().clear();
18201}
18202
18203#[gpui::test]
18204async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18205 init_test(cx, |_| {});
18206
18207 let mut cx = EditorLspTestContext::new(
18208 Language::new(
18209 LanguageConfig {
18210 matcher: LanguageMatcher {
18211 path_suffixes: vec!["jsx".into()],
18212 ..Default::default()
18213 },
18214 overrides: [(
18215 "element".into(),
18216 LanguageConfigOverride {
18217 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18218 ..Default::default()
18219 },
18220 )]
18221 .into_iter()
18222 .collect(),
18223 ..Default::default()
18224 },
18225 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18226 )
18227 .with_override_query("(jsx_self_closing_element) @element")
18228 .unwrap(),
18229 lsp::ServerCapabilities {
18230 completion_provider: Some(lsp::CompletionOptions {
18231 trigger_characters: Some(vec![":".to_string()]),
18232 ..Default::default()
18233 }),
18234 ..Default::default()
18235 },
18236 cx,
18237 )
18238 .await;
18239
18240 cx.lsp
18241 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18242 Ok(Some(lsp::CompletionResponse::Array(vec![
18243 lsp::CompletionItem {
18244 label: "bg-blue".into(),
18245 ..Default::default()
18246 },
18247 lsp::CompletionItem {
18248 label: "bg-red".into(),
18249 ..Default::default()
18250 },
18251 lsp::CompletionItem {
18252 label: "bg-yellow".into(),
18253 ..Default::default()
18254 },
18255 ])))
18256 });
18257
18258 cx.set_state(r#"<p class="bgˇ" />"#);
18259
18260 // Trigger completion when typing a dash, because the dash is an extra
18261 // word character in the 'element' scope, which contains the cursor.
18262 cx.simulate_keystroke("-");
18263 cx.executor().run_until_parked();
18264 cx.update_editor(|editor, _, _| {
18265 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18266 {
18267 assert_eq!(
18268 completion_menu_entries(menu),
18269 &["bg-blue", "bg-red", "bg-yellow"]
18270 );
18271 } else {
18272 panic!("expected completion menu to be open");
18273 }
18274 });
18275
18276 cx.simulate_keystroke("l");
18277 cx.executor().run_until_parked();
18278 cx.update_editor(|editor, _, _| {
18279 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18280 {
18281 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18282 } else {
18283 panic!("expected completion menu to be open");
18284 }
18285 });
18286
18287 // When filtering completions, consider the character after the '-' to
18288 // be the start of a subword.
18289 cx.set_state(r#"<p class="yelˇ" />"#);
18290 cx.simulate_keystroke("l");
18291 cx.executor().run_until_parked();
18292 cx.update_editor(|editor, _, _| {
18293 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18294 {
18295 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18296 } else {
18297 panic!("expected completion menu to be open");
18298 }
18299 });
18300}
18301
18302fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18303 let entries = menu.entries.borrow();
18304 entries.iter().map(|mat| mat.string.clone()).collect()
18305}
18306
18307#[gpui::test]
18308async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18309 init_test(cx, |settings| {
18310 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18311 });
18312
18313 let fs = FakeFs::new(cx.executor());
18314 fs.insert_file(path!("/file.ts"), Default::default()).await;
18315
18316 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18317 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18318
18319 language_registry.add(Arc::new(Language::new(
18320 LanguageConfig {
18321 name: "TypeScript".into(),
18322 matcher: LanguageMatcher {
18323 path_suffixes: vec!["ts".to_string()],
18324 ..Default::default()
18325 },
18326 ..Default::default()
18327 },
18328 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18329 )));
18330 update_test_language_settings(cx, |settings| {
18331 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18332 });
18333
18334 let test_plugin = "test_plugin";
18335 let _ = language_registry.register_fake_lsp(
18336 "TypeScript",
18337 FakeLspAdapter {
18338 prettier_plugins: vec![test_plugin],
18339 ..Default::default()
18340 },
18341 );
18342
18343 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18344 let buffer = project
18345 .update(cx, |project, cx| {
18346 project.open_local_buffer(path!("/file.ts"), cx)
18347 })
18348 .await
18349 .unwrap();
18350
18351 let buffer_text = "one\ntwo\nthree\n";
18352 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18353 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18354 editor.update_in(cx, |editor, window, cx| {
18355 editor.set_text(buffer_text, window, cx)
18356 });
18357
18358 editor
18359 .update_in(cx, |editor, window, cx| {
18360 editor.perform_format(
18361 project.clone(),
18362 FormatTrigger::Manual,
18363 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18364 window,
18365 cx,
18366 )
18367 })
18368 .unwrap()
18369 .await;
18370 assert_eq!(
18371 editor.update(cx, |editor, cx| editor.text(cx)),
18372 buffer_text.to_string() + prettier_format_suffix,
18373 "Test prettier formatting was not applied to the original buffer text",
18374 );
18375
18376 update_test_language_settings(cx, |settings| {
18377 settings.defaults.formatter = Some(FormatterList::default())
18378 });
18379 let format = editor.update_in(cx, |editor, window, cx| {
18380 editor.perform_format(
18381 project.clone(),
18382 FormatTrigger::Manual,
18383 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18384 window,
18385 cx,
18386 )
18387 });
18388 format.await.unwrap();
18389 assert_eq!(
18390 editor.update(cx, |editor, cx| editor.text(cx)),
18391 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18392 "Autoformatting (via test prettier) was not applied to the original buffer text",
18393 );
18394}
18395
18396#[gpui::test]
18397async fn test_addition_reverts(cx: &mut TestAppContext) {
18398 init_test(cx, |_| {});
18399 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18400 let base_text = indoc! {r#"
18401 struct Row;
18402 struct Row1;
18403 struct Row2;
18404
18405 struct Row4;
18406 struct Row5;
18407 struct Row6;
18408
18409 struct Row8;
18410 struct Row9;
18411 struct Row10;"#};
18412
18413 // When addition hunks are not adjacent to carets, no hunk revert is performed
18414 assert_hunk_revert(
18415 indoc! {r#"struct Row;
18416 struct Row1;
18417 struct Row1.1;
18418 struct Row1.2;
18419 struct Row2;ˇ
18420
18421 struct Row4;
18422 struct Row5;
18423 struct Row6;
18424
18425 struct Row8;
18426 ˇstruct Row9;
18427 struct Row9.1;
18428 struct Row9.2;
18429 struct Row9.3;
18430 struct Row10;"#},
18431 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18432 indoc! {r#"struct Row;
18433 struct Row1;
18434 struct Row1.1;
18435 struct Row1.2;
18436 struct Row2;ˇ
18437
18438 struct Row4;
18439 struct Row5;
18440 struct Row6;
18441
18442 struct Row8;
18443 ˇstruct Row9;
18444 struct Row9.1;
18445 struct Row9.2;
18446 struct Row9.3;
18447 struct Row10;"#},
18448 base_text,
18449 &mut cx,
18450 );
18451 // Same for selections
18452 assert_hunk_revert(
18453 indoc! {r#"struct Row;
18454 struct Row1;
18455 struct Row2;
18456 struct Row2.1;
18457 struct Row2.2;
18458 «ˇ
18459 struct Row4;
18460 struct» Row5;
18461 «struct Row6;
18462 ˇ»
18463 struct Row9.1;
18464 struct Row9.2;
18465 struct Row9.3;
18466 struct Row8;
18467 struct Row9;
18468 struct Row10;"#},
18469 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18470 indoc! {r#"struct Row;
18471 struct Row1;
18472 struct Row2;
18473 struct Row2.1;
18474 struct Row2.2;
18475 «ˇ
18476 struct Row4;
18477 struct» Row5;
18478 «struct Row6;
18479 ˇ»
18480 struct Row9.1;
18481 struct Row9.2;
18482 struct Row9.3;
18483 struct Row8;
18484 struct Row9;
18485 struct Row10;"#},
18486 base_text,
18487 &mut cx,
18488 );
18489
18490 // When carets and selections intersect the addition hunks, those are reverted.
18491 // Adjacent carets got merged.
18492 assert_hunk_revert(
18493 indoc! {r#"struct Row;
18494 ˇ// something on the top
18495 struct Row1;
18496 struct Row2;
18497 struct Roˇw3.1;
18498 struct Row2.2;
18499 struct Row2.3;ˇ
18500
18501 struct Row4;
18502 struct ˇRow5.1;
18503 struct Row5.2;
18504 struct «Rowˇ»5.3;
18505 struct Row5;
18506 struct Row6;
18507 ˇ
18508 struct Row9.1;
18509 struct «Rowˇ»9.2;
18510 struct «ˇRow»9.3;
18511 struct Row8;
18512 struct Row9;
18513 «ˇ// something on bottom»
18514 struct Row10;"#},
18515 vec![
18516 DiffHunkStatusKind::Added,
18517 DiffHunkStatusKind::Added,
18518 DiffHunkStatusKind::Added,
18519 DiffHunkStatusKind::Added,
18520 DiffHunkStatusKind::Added,
18521 ],
18522 indoc! {r#"struct Row;
18523 ˇstruct Row1;
18524 struct Row2;
18525 ˇ
18526 struct Row4;
18527 ˇstruct Row5;
18528 struct Row6;
18529 ˇ
18530 ˇstruct Row8;
18531 struct Row9;
18532 ˇstruct Row10;"#},
18533 base_text,
18534 &mut cx,
18535 );
18536}
18537
18538#[gpui::test]
18539async fn test_modification_reverts(cx: &mut TestAppContext) {
18540 init_test(cx, |_| {});
18541 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18542 let base_text = indoc! {r#"
18543 struct Row;
18544 struct Row1;
18545 struct Row2;
18546
18547 struct Row4;
18548 struct Row5;
18549 struct Row6;
18550
18551 struct Row8;
18552 struct Row9;
18553 struct Row10;"#};
18554
18555 // Modification hunks behave the same as the addition ones.
18556 assert_hunk_revert(
18557 indoc! {r#"struct Row;
18558 struct Row1;
18559 struct Row33;
18560 ˇ
18561 struct Row4;
18562 struct Row5;
18563 struct Row6;
18564 ˇ
18565 struct Row99;
18566 struct Row9;
18567 struct Row10;"#},
18568 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18569 indoc! {r#"struct Row;
18570 struct Row1;
18571 struct Row33;
18572 ˇ
18573 struct Row4;
18574 struct Row5;
18575 struct Row6;
18576 ˇ
18577 struct Row99;
18578 struct Row9;
18579 struct Row10;"#},
18580 base_text,
18581 &mut cx,
18582 );
18583 assert_hunk_revert(
18584 indoc! {r#"struct Row;
18585 struct Row1;
18586 struct Row33;
18587 «ˇ
18588 struct Row4;
18589 struct» Row5;
18590 «struct Row6;
18591 ˇ»
18592 struct Row99;
18593 struct Row9;
18594 struct Row10;"#},
18595 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18596 indoc! {r#"struct Row;
18597 struct Row1;
18598 struct Row33;
18599 «ˇ
18600 struct Row4;
18601 struct» Row5;
18602 «struct Row6;
18603 ˇ»
18604 struct Row99;
18605 struct Row9;
18606 struct Row10;"#},
18607 base_text,
18608 &mut cx,
18609 );
18610
18611 assert_hunk_revert(
18612 indoc! {r#"ˇstruct Row1.1;
18613 struct Row1;
18614 «ˇstr»uct Row22;
18615
18616 struct ˇRow44;
18617 struct Row5;
18618 struct «Rˇ»ow66;ˇ
18619
18620 «struˇ»ct Row88;
18621 struct Row9;
18622 struct Row1011;ˇ"#},
18623 vec![
18624 DiffHunkStatusKind::Modified,
18625 DiffHunkStatusKind::Modified,
18626 DiffHunkStatusKind::Modified,
18627 DiffHunkStatusKind::Modified,
18628 DiffHunkStatusKind::Modified,
18629 DiffHunkStatusKind::Modified,
18630 ],
18631 indoc! {r#"struct Row;
18632 ˇstruct Row1;
18633 struct Row2;
18634 ˇ
18635 struct Row4;
18636 ˇstruct Row5;
18637 struct Row6;
18638 ˇ
18639 struct Row8;
18640 ˇstruct Row9;
18641 struct Row10;ˇ"#},
18642 base_text,
18643 &mut cx,
18644 );
18645}
18646
18647#[gpui::test]
18648async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18649 init_test(cx, |_| {});
18650 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18651 let base_text = indoc! {r#"
18652 one
18653
18654 two
18655 three
18656 "#};
18657
18658 cx.set_head_text(base_text);
18659 cx.set_state("\nˇ\n");
18660 cx.executor().run_until_parked();
18661 cx.update_editor(|editor, _window, cx| {
18662 editor.expand_selected_diff_hunks(cx);
18663 });
18664 cx.executor().run_until_parked();
18665 cx.update_editor(|editor, window, cx| {
18666 editor.backspace(&Default::default(), window, cx);
18667 });
18668 cx.run_until_parked();
18669 cx.assert_state_with_diff(
18670 indoc! {r#"
18671
18672 - two
18673 - threeˇ
18674 +
18675 "#}
18676 .to_string(),
18677 );
18678}
18679
18680#[gpui::test]
18681async fn test_deletion_reverts(cx: &mut TestAppContext) {
18682 init_test(cx, |_| {});
18683 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18684 let base_text = indoc! {r#"struct Row;
18685struct Row1;
18686struct Row2;
18687
18688struct Row4;
18689struct Row5;
18690struct Row6;
18691
18692struct Row8;
18693struct Row9;
18694struct Row10;"#};
18695
18696 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18697 assert_hunk_revert(
18698 indoc! {r#"struct Row;
18699 struct Row2;
18700
18701 ˇstruct Row4;
18702 struct Row5;
18703 struct Row6;
18704 ˇ
18705 struct Row8;
18706 struct Row10;"#},
18707 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18708 indoc! {r#"struct Row;
18709 struct Row2;
18710
18711 ˇstruct Row4;
18712 struct Row5;
18713 struct Row6;
18714 ˇ
18715 struct Row8;
18716 struct Row10;"#},
18717 base_text,
18718 &mut cx,
18719 );
18720 assert_hunk_revert(
18721 indoc! {r#"struct Row;
18722 struct Row2;
18723
18724 «ˇstruct Row4;
18725 struct» Row5;
18726 «struct Row6;
18727 ˇ»
18728 struct Row8;
18729 struct Row10;"#},
18730 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18731 indoc! {r#"struct Row;
18732 struct Row2;
18733
18734 «ˇstruct Row4;
18735 struct» Row5;
18736 «struct Row6;
18737 ˇ»
18738 struct Row8;
18739 struct Row10;"#},
18740 base_text,
18741 &mut cx,
18742 );
18743
18744 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18745 assert_hunk_revert(
18746 indoc! {r#"struct Row;
18747 ˇstruct Row2;
18748
18749 struct Row4;
18750 struct Row5;
18751 struct Row6;
18752
18753 struct Row8;ˇ
18754 struct Row10;"#},
18755 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18756 indoc! {r#"struct Row;
18757 struct Row1;
18758 ˇstruct Row2;
18759
18760 struct Row4;
18761 struct Row5;
18762 struct Row6;
18763
18764 struct Row8;ˇ
18765 struct Row9;
18766 struct Row10;"#},
18767 base_text,
18768 &mut cx,
18769 );
18770 assert_hunk_revert(
18771 indoc! {r#"struct Row;
18772 struct Row2«ˇ;
18773 struct Row4;
18774 struct» Row5;
18775 «struct Row6;
18776
18777 struct Row8;ˇ»
18778 struct Row10;"#},
18779 vec![
18780 DiffHunkStatusKind::Deleted,
18781 DiffHunkStatusKind::Deleted,
18782 DiffHunkStatusKind::Deleted,
18783 ],
18784 indoc! {r#"struct Row;
18785 struct Row1;
18786 struct Row2«ˇ;
18787
18788 struct Row4;
18789 struct» Row5;
18790 «struct Row6;
18791
18792 struct Row8;ˇ»
18793 struct Row9;
18794 struct Row10;"#},
18795 base_text,
18796 &mut cx,
18797 );
18798}
18799
18800#[gpui::test]
18801async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18802 init_test(cx, |_| {});
18803
18804 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18805 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18806 let base_text_3 =
18807 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18808
18809 let text_1 = edit_first_char_of_every_line(base_text_1);
18810 let text_2 = edit_first_char_of_every_line(base_text_2);
18811 let text_3 = edit_first_char_of_every_line(base_text_3);
18812
18813 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18814 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18815 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18816
18817 let multibuffer = cx.new(|cx| {
18818 let mut multibuffer = MultiBuffer::new(ReadWrite);
18819 multibuffer.push_excerpts(
18820 buffer_1.clone(),
18821 [
18822 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18823 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18824 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18825 ],
18826 cx,
18827 );
18828 multibuffer.push_excerpts(
18829 buffer_2.clone(),
18830 [
18831 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18832 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18833 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18834 ],
18835 cx,
18836 );
18837 multibuffer.push_excerpts(
18838 buffer_3.clone(),
18839 [
18840 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18841 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18842 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18843 ],
18844 cx,
18845 );
18846 multibuffer
18847 });
18848
18849 let fs = FakeFs::new(cx.executor());
18850 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18851 let (editor, cx) = cx
18852 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18853 editor.update_in(cx, |editor, _window, cx| {
18854 for (buffer, diff_base) in [
18855 (buffer_1.clone(), base_text_1),
18856 (buffer_2.clone(), base_text_2),
18857 (buffer_3.clone(), base_text_3),
18858 ] {
18859 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18860 editor
18861 .buffer
18862 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18863 }
18864 });
18865 cx.executor().run_until_parked();
18866
18867 editor.update_in(cx, |editor, window, cx| {
18868 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}");
18869 editor.select_all(&SelectAll, window, cx);
18870 editor.git_restore(&Default::default(), window, cx);
18871 });
18872 cx.executor().run_until_parked();
18873
18874 // When all ranges are selected, all buffer hunks are reverted.
18875 editor.update(cx, |editor, cx| {
18876 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");
18877 });
18878 buffer_1.update(cx, |buffer, _| {
18879 assert_eq!(buffer.text(), base_text_1);
18880 });
18881 buffer_2.update(cx, |buffer, _| {
18882 assert_eq!(buffer.text(), base_text_2);
18883 });
18884 buffer_3.update(cx, |buffer, _| {
18885 assert_eq!(buffer.text(), base_text_3);
18886 });
18887
18888 editor.update_in(cx, |editor, window, cx| {
18889 editor.undo(&Default::default(), window, cx);
18890 });
18891
18892 editor.update_in(cx, |editor, window, cx| {
18893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18894 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18895 });
18896 editor.git_restore(&Default::default(), window, cx);
18897 });
18898
18899 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18900 // but not affect buffer_2 and its related excerpts.
18901 editor.update(cx, |editor, cx| {
18902 assert_eq!(
18903 editor.text(cx),
18904 "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}"
18905 );
18906 });
18907 buffer_1.update(cx, |buffer, _| {
18908 assert_eq!(buffer.text(), base_text_1);
18909 });
18910 buffer_2.update(cx, |buffer, _| {
18911 assert_eq!(
18912 buffer.text(),
18913 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18914 );
18915 });
18916 buffer_3.update(cx, |buffer, _| {
18917 assert_eq!(
18918 buffer.text(),
18919 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18920 );
18921 });
18922
18923 fn edit_first_char_of_every_line(text: &str) -> String {
18924 text.split('\n')
18925 .map(|line| format!("X{}", &line[1..]))
18926 .collect::<Vec<_>>()
18927 .join("\n")
18928 }
18929}
18930
18931#[gpui::test]
18932async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18933 init_test(cx, |_| {});
18934
18935 let cols = 4;
18936 let rows = 10;
18937 let sample_text_1 = sample_text(rows, cols, 'a');
18938 assert_eq!(
18939 sample_text_1,
18940 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18941 );
18942 let sample_text_2 = sample_text(rows, cols, 'l');
18943 assert_eq!(
18944 sample_text_2,
18945 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18946 );
18947 let sample_text_3 = sample_text(rows, cols, 'v');
18948 assert_eq!(
18949 sample_text_3,
18950 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18951 );
18952
18953 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18954 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18955 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18956
18957 let multi_buffer = cx.new(|cx| {
18958 let mut multibuffer = MultiBuffer::new(ReadWrite);
18959 multibuffer.push_excerpts(
18960 buffer_1.clone(),
18961 [
18962 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18963 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18964 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18965 ],
18966 cx,
18967 );
18968 multibuffer.push_excerpts(
18969 buffer_2.clone(),
18970 [
18971 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18972 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18973 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18974 ],
18975 cx,
18976 );
18977 multibuffer.push_excerpts(
18978 buffer_3.clone(),
18979 [
18980 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18981 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18982 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18983 ],
18984 cx,
18985 );
18986 multibuffer
18987 });
18988
18989 let fs = FakeFs::new(cx.executor());
18990 fs.insert_tree(
18991 "/a",
18992 json!({
18993 "main.rs": sample_text_1,
18994 "other.rs": sample_text_2,
18995 "lib.rs": sample_text_3,
18996 }),
18997 )
18998 .await;
18999 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19000 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19001 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19002 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19003 Editor::new(
19004 EditorMode::full(),
19005 multi_buffer,
19006 Some(project.clone()),
19007 window,
19008 cx,
19009 )
19010 });
19011 let multibuffer_item_id = workspace
19012 .update(cx, |workspace, window, cx| {
19013 assert!(
19014 workspace.active_item(cx).is_none(),
19015 "active item should be None before the first item is added"
19016 );
19017 workspace.add_item_to_active_pane(
19018 Box::new(multi_buffer_editor.clone()),
19019 None,
19020 true,
19021 window,
19022 cx,
19023 );
19024 let active_item = workspace
19025 .active_item(cx)
19026 .expect("should have an active item after adding the multi buffer");
19027 assert_eq!(
19028 active_item.buffer_kind(cx),
19029 ItemBufferKind::Multibuffer,
19030 "A multi buffer was expected to active after adding"
19031 );
19032 active_item.item_id()
19033 })
19034 .unwrap();
19035 cx.executor().run_until_parked();
19036
19037 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19038 editor.change_selections(
19039 SelectionEffects::scroll(Autoscroll::Next),
19040 window,
19041 cx,
19042 |s| s.select_ranges(Some(1..2)),
19043 );
19044 editor.open_excerpts(&OpenExcerpts, window, cx);
19045 });
19046 cx.executor().run_until_parked();
19047 let first_item_id = workspace
19048 .update(cx, |workspace, window, cx| {
19049 let active_item = workspace
19050 .active_item(cx)
19051 .expect("should have an active item after navigating into the 1st buffer");
19052 let first_item_id = active_item.item_id();
19053 assert_ne!(
19054 first_item_id, multibuffer_item_id,
19055 "Should navigate into the 1st buffer and activate it"
19056 );
19057 assert_eq!(
19058 active_item.buffer_kind(cx),
19059 ItemBufferKind::Singleton,
19060 "New active item should be a singleton buffer"
19061 );
19062 assert_eq!(
19063 active_item
19064 .act_as::<Editor>(cx)
19065 .expect("should have navigated into an editor for the 1st buffer")
19066 .read(cx)
19067 .text(cx),
19068 sample_text_1
19069 );
19070
19071 workspace
19072 .go_back(workspace.active_pane().downgrade(), window, cx)
19073 .detach_and_log_err(cx);
19074
19075 first_item_id
19076 })
19077 .unwrap();
19078 cx.executor().run_until_parked();
19079 workspace
19080 .update(cx, |workspace, _, cx| {
19081 let active_item = workspace
19082 .active_item(cx)
19083 .expect("should have an active item after navigating back");
19084 assert_eq!(
19085 active_item.item_id(),
19086 multibuffer_item_id,
19087 "Should navigate back to the multi buffer"
19088 );
19089 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19090 })
19091 .unwrap();
19092
19093 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19094 editor.change_selections(
19095 SelectionEffects::scroll(Autoscroll::Next),
19096 window,
19097 cx,
19098 |s| s.select_ranges(Some(39..40)),
19099 );
19100 editor.open_excerpts(&OpenExcerpts, window, cx);
19101 });
19102 cx.executor().run_until_parked();
19103 let second_item_id = workspace
19104 .update(cx, |workspace, window, cx| {
19105 let active_item = workspace
19106 .active_item(cx)
19107 .expect("should have an active item after navigating into the 2nd buffer");
19108 let second_item_id = active_item.item_id();
19109 assert_ne!(
19110 second_item_id, multibuffer_item_id,
19111 "Should navigate away from the multibuffer"
19112 );
19113 assert_ne!(
19114 second_item_id, first_item_id,
19115 "Should navigate into the 2nd buffer and activate it"
19116 );
19117 assert_eq!(
19118 active_item.buffer_kind(cx),
19119 ItemBufferKind::Singleton,
19120 "New active item should be a singleton buffer"
19121 );
19122 assert_eq!(
19123 active_item
19124 .act_as::<Editor>(cx)
19125 .expect("should have navigated into an editor")
19126 .read(cx)
19127 .text(cx),
19128 sample_text_2
19129 );
19130
19131 workspace
19132 .go_back(workspace.active_pane().downgrade(), window, cx)
19133 .detach_and_log_err(cx);
19134
19135 second_item_id
19136 })
19137 .unwrap();
19138 cx.executor().run_until_parked();
19139 workspace
19140 .update(cx, |workspace, _, cx| {
19141 let active_item = workspace
19142 .active_item(cx)
19143 .expect("should have an active item after navigating back from the 2nd buffer");
19144 assert_eq!(
19145 active_item.item_id(),
19146 multibuffer_item_id,
19147 "Should navigate back from the 2nd buffer to the multi buffer"
19148 );
19149 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19150 })
19151 .unwrap();
19152
19153 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19154 editor.change_selections(
19155 SelectionEffects::scroll(Autoscroll::Next),
19156 window,
19157 cx,
19158 |s| s.select_ranges(Some(70..70)),
19159 );
19160 editor.open_excerpts(&OpenExcerpts, window, cx);
19161 });
19162 cx.executor().run_until_parked();
19163 workspace
19164 .update(cx, |workspace, window, cx| {
19165 let active_item = workspace
19166 .active_item(cx)
19167 .expect("should have an active item after navigating into the 3rd buffer");
19168 let third_item_id = active_item.item_id();
19169 assert_ne!(
19170 third_item_id, multibuffer_item_id,
19171 "Should navigate into the 3rd buffer and activate it"
19172 );
19173 assert_ne!(third_item_id, first_item_id);
19174 assert_ne!(third_item_id, second_item_id);
19175 assert_eq!(
19176 active_item.buffer_kind(cx),
19177 ItemBufferKind::Singleton,
19178 "New active item should be a singleton buffer"
19179 );
19180 assert_eq!(
19181 active_item
19182 .act_as::<Editor>(cx)
19183 .expect("should have navigated into an editor")
19184 .read(cx)
19185 .text(cx),
19186 sample_text_3
19187 );
19188
19189 workspace
19190 .go_back(workspace.active_pane().downgrade(), window, cx)
19191 .detach_and_log_err(cx);
19192 })
19193 .unwrap();
19194 cx.executor().run_until_parked();
19195 workspace
19196 .update(cx, |workspace, _, cx| {
19197 let active_item = workspace
19198 .active_item(cx)
19199 .expect("should have an active item after navigating back from the 3rd buffer");
19200 assert_eq!(
19201 active_item.item_id(),
19202 multibuffer_item_id,
19203 "Should navigate back from the 3rd buffer to the multi buffer"
19204 );
19205 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19206 })
19207 .unwrap();
19208}
19209
19210#[gpui::test]
19211async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19212 init_test(cx, |_| {});
19213
19214 let mut cx = EditorTestContext::new(cx).await;
19215
19216 let diff_base = r#"
19217 use some::mod;
19218
19219 const A: u32 = 42;
19220
19221 fn main() {
19222 println!("hello");
19223
19224 println!("world");
19225 }
19226 "#
19227 .unindent();
19228
19229 cx.set_state(
19230 &r#"
19231 use some::modified;
19232
19233 ˇ
19234 fn main() {
19235 println!("hello there");
19236
19237 println!("around the");
19238 println!("world");
19239 }
19240 "#
19241 .unindent(),
19242 );
19243
19244 cx.set_head_text(&diff_base);
19245 executor.run_until_parked();
19246
19247 cx.update_editor(|editor, window, cx| {
19248 editor.go_to_next_hunk(&GoToHunk, window, cx);
19249 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19250 });
19251 executor.run_until_parked();
19252 cx.assert_state_with_diff(
19253 r#"
19254 use some::modified;
19255
19256
19257 fn main() {
19258 - println!("hello");
19259 + ˇ println!("hello there");
19260
19261 println!("around the");
19262 println!("world");
19263 }
19264 "#
19265 .unindent(),
19266 );
19267
19268 cx.update_editor(|editor, window, cx| {
19269 for _ in 0..2 {
19270 editor.go_to_next_hunk(&GoToHunk, window, cx);
19271 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19272 }
19273 });
19274 executor.run_until_parked();
19275 cx.assert_state_with_diff(
19276 r#"
19277 - use some::mod;
19278 + ˇuse some::modified;
19279
19280
19281 fn main() {
19282 - println!("hello");
19283 + println!("hello there");
19284
19285 + println!("around the");
19286 println!("world");
19287 }
19288 "#
19289 .unindent(),
19290 );
19291
19292 cx.update_editor(|editor, window, cx| {
19293 editor.go_to_next_hunk(&GoToHunk, window, cx);
19294 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19295 });
19296 executor.run_until_parked();
19297 cx.assert_state_with_diff(
19298 r#"
19299 - use some::mod;
19300 + use some::modified;
19301
19302 - const A: u32 = 42;
19303 ˇ
19304 fn main() {
19305 - println!("hello");
19306 + println!("hello there");
19307
19308 + println!("around the");
19309 println!("world");
19310 }
19311 "#
19312 .unindent(),
19313 );
19314
19315 cx.update_editor(|editor, window, cx| {
19316 editor.cancel(&Cancel, window, cx);
19317 });
19318
19319 cx.assert_state_with_diff(
19320 r#"
19321 use some::modified;
19322
19323 ˇ
19324 fn main() {
19325 println!("hello there");
19326
19327 println!("around the");
19328 println!("world");
19329 }
19330 "#
19331 .unindent(),
19332 );
19333}
19334
19335#[gpui::test]
19336async fn test_diff_base_change_with_expanded_diff_hunks(
19337 executor: BackgroundExecutor,
19338 cx: &mut TestAppContext,
19339) {
19340 init_test(cx, |_| {});
19341
19342 let mut cx = EditorTestContext::new(cx).await;
19343
19344 let diff_base = r#"
19345 use some::mod1;
19346 use some::mod2;
19347
19348 const A: u32 = 42;
19349 const B: u32 = 42;
19350 const C: u32 = 42;
19351
19352 fn main() {
19353 println!("hello");
19354
19355 println!("world");
19356 }
19357 "#
19358 .unindent();
19359
19360 cx.set_state(
19361 &r#"
19362 use some::mod2;
19363
19364 const A: u32 = 42;
19365 const C: u32 = 42;
19366
19367 fn main(ˇ) {
19368 //println!("hello");
19369
19370 println!("world");
19371 //
19372 //
19373 }
19374 "#
19375 .unindent(),
19376 );
19377
19378 cx.set_head_text(&diff_base);
19379 executor.run_until_parked();
19380
19381 cx.update_editor(|editor, window, cx| {
19382 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19383 });
19384 executor.run_until_parked();
19385 cx.assert_state_with_diff(
19386 r#"
19387 - use some::mod1;
19388 use some::mod2;
19389
19390 const A: u32 = 42;
19391 - const B: u32 = 42;
19392 const C: u32 = 42;
19393
19394 fn main(ˇ) {
19395 - println!("hello");
19396 + //println!("hello");
19397
19398 println!("world");
19399 + //
19400 + //
19401 }
19402 "#
19403 .unindent(),
19404 );
19405
19406 cx.set_head_text("new diff base!");
19407 executor.run_until_parked();
19408 cx.assert_state_with_diff(
19409 r#"
19410 - new diff base!
19411 + use some::mod2;
19412 +
19413 + const A: u32 = 42;
19414 + const C: u32 = 42;
19415 +
19416 + fn main(ˇ) {
19417 + //println!("hello");
19418 +
19419 + println!("world");
19420 + //
19421 + //
19422 + }
19423 "#
19424 .unindent(),
19425 );
19426}
19427
19428#[gpui::test]
19429async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19430 init_test(cx, |_| {});
19431
19432 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19433 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19434 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19435 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19436 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19437 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19438
19439 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19440 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19441 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19442
19443 let multi_buffer = cx.new(|cx| {
19444 let mut multibuffer = MultiBuffer::new(ReadWrite);
19445 multibuffer.push_excerpts(
19446 buffer_1.clone(),
19447 [
19448 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19449 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19450 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19451 ],
19452 cx,
19453 );
19454 multibuffer.push_excerpts(
19455 buffer_2.clone(),
19456 [
19457 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19458 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19459 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19460 ],
19461 cx,
19462 );
19463 multibuffer.push_excerpts(
19464 buffer_3.clone(),
19465 [
19466 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19467 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19468 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19469 ],
19470 cx,
19471 );
19472 multibuffer
19473 });
19474
19475 let editor =
19476 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19477 editor
19478 .update(cx, |editor, _window, cx| {
19479 for (buffer, diff_base) in [
19480 (buffer_1.clone(), file_1_old),
19481 (buffer_2.clone(), file_2_old),
19482 (buffer_3.clone(), file_3_old),
19483 ] {
19484 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19485 editor
19486 .buffer
19487 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19488 }
19489 })
19490 .unwrap();
19491
19492 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19493 cx.run_until_parked();
19494
19495 cx.assert_editor_state(
19496 &"
19497 ˇaaa
19498 ccc
19499 ddd
19500
19501 ggg
19502 hhh
19503
19504
19505 lll
19506 mmm
19507 NNN
19508
19509 qqq
19510 rrr
19511
19512 uuu
19513 111
19514 222
19515 333
19516
19517 666
19518 777
19519
19520 000
19521 !!!"
19522 .unindent(),
19523 );
19524
19525 cx.update_editor(|editor, window, cx| {
19526 editor.select_all(&SelectAll, window, cx);
19527 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19528 });
19529 cx.executor().run_until_parked();
19530
19531 cx.assert_state_with_diff(
19532 "
19533 «aaa
19534 - bbb
19535 ccc
19536 ddd
19537
19538 ggg
19539 hhh
19540
19541
19542 lll
19543 mmm
19544 - nnn
19545 + NNN
19546
19547 qqq
19548 rrr
19549
19550 uuu
19551 111
19552 222
19553 333
19554
19555 + 666
19556 777
19557
19558 000
19559 !!!ˇ»"
19560 .unindent(),
19561 );
19562}
19563
19564#[gpui::test]
19565async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19566 init_test(cx, |_| {});
19567
19568 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19569 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19570
19571 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19572 let multi_buffer = cx.new(|cx| {
19573 let mut multibuffer = MultiBuffer::new(ReadWrite);
19574 multibuffer.push_excerpts(
19575 buffer.clone(),
19576 [
19577 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19578 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19579 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19580 ],
19581 cx,
19582 );
19583 multibuffer
19584 });
19585
19586 let editor =
19587 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19588 editor
19589 .update(cx, |editor, _window, cx| {
19590 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19591 editor
19592 .buffer
19593 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19594 })
19595 .unwrap();
19596
19597 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19598 cx.run_until_parked();
19599
19600 cx.update_editor(|editor, window, cx| {
19601 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19602 });
19603 cx.executor().run_until_parked();
19604
19605 // When the start of a hunk coincides with the start of its excerpt,
19606 // the hunk is expanded. When the start of a hunk is earlier than
19607 // the start of its excerpt, the hunk is not expanded.
19608 cx.assert_state_with_diff(
19609 "
19610 ˇaaa
19611 - bbb
19612 + BBB
19613
19614 - ddd
19615 - eee
19616 + DDD
19617 + EEE
19618 fff
19619
19620 iii
19621 "
19622 .unindent(),
19623 );
19624}
19625
19626#[gpui::test]
19627async fn test_edits_around_expanded_insertion_hunks(
19628 executor: BackgroundExecutor,
19629 cx: &mut TestAppContext,
19630) {
19631 init_test(cx, |_| {});
19632
19633 let mut cx = EditorTestContext::new(cx).await;
19634
19635 let diff_base = r#"
19636 use some::mod1;
19637 use some::mod2;
19638
19639 const A: u32 = 42;
19640
19641 fn main() {
19642 println!("hello");
19643
19644 println!("world");
19645 }
19646 "#
19647 .unindent();
19648 executor.run_until_parked();
19649 cx.set_state(
19650 &r#"
19651 use some::mod1;
19652 use some::mod2;
19653
19654 const A: u32 = 42;
19655 const B: u32 = 42;
19656 const C: u32 = 42;
19657 ˇ
19658
19659 fn main() {
19660 println!("hello");
19661
19662 println!("world");
19663 }
19664 "#
19665 .unindent(),
19666 );
19667
19668 cx.set_head_text(&diff_base);
19669 executor.run_until_parked();
19670
19671 cx.update_editor(|editor, window, cx| {
19672 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19673 });
19674 executor.run_until_parked();
19675
19676 cx.assert_state_with_diff(
19677 r#"
19678 use some::mod1;
19679 use some::mod2;
19680
19681 const A: u32 = 42;
19682 + const B: u32 = 42;
19683 + const C: u32 = 42;
19684 + ˇ
19685
19686 fn main() {
19687 println!("hello");
19688
19689 println!("world");
19690 }
19691 "#
19692 .unindent(),
19693 );
19694
19695 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19696 executor.run_until_parked();
19697
19698 cx.assert_state_with_diff(
19699 r#"
19700 use some::mod1;
19701 use some::mod2;
19702
19703 const A: u32 = 42;
19704 + const B: u32 = 42;
19705 + const C: u32 = 42;
19706 + const D: u32 = 42;
19707 + ˇ
19708
19709 fn main() {
19710 println!("hello");
19711
19712 println!("world");
19713 }
19714 "#
19715 .unindent(),
19716 );
19717
19718 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19719 executor.run_until_parked();
19720
19721 cx.assert_state_with_diff(
19722 r#"
19723 use some::mod1;
19724 use some::mod2;
19725
19726 const A: u32 = 42;
19727 + const B: u32 = 42;
19728 + const C: u32 = 42;
19729 + const D: u32 = 42;
19730 + const E: u32 = 42;
19731 + ˇ
19732
19733 fn main() {
19734 println!("hello");
19735
19736 println!("world");
19737 }
19738 "#
19739 .unindent(),
19740 );
19741
19742 cx.update_editor(|editor, window, cx| {
19743 editor.delete_line(&DeleteLine, window, cx);
19744 });
19745 executor.run_until_parked();
19746
19747 cx.assert_state_with_diff(
19748 r#"
19749 use some::mod1;
19750 use some::mod2;
19751
19752 const A: u32 = 42;
19753 + const B: u32 = 42;
19754 + const C: u32 = 42;
19755 + const D: u32 = 42;
19756 + const E: u32 = 42;
19757 ˇ
19758 fn main() {
19759 println!("hello");
19760
19761 println!("world");
19762 }
19763 "#
19764 .unindent(),
19765 );
19766
19767 cx.update_editor(|editor, window, cx| {
19768 editor.move_up(&MoveUp, window, cx);
19769 editor.delete_line(&DeleteLine, window, cx);
19770 editor.move_up(&MoveUp, window, cx);
19771 editor.delete_line(&DeleteLine, window, cx);
19772 editor.move_up(&MoveUp, window, cx);
19773 editor.delete_line(&DeleteLine, window, cx);
19774 });
19775 executor.run_until_parked();
19776 cx.assert_state_with_diff(
19777 r#"
19778 use some::mod1;
19779 use some::mod2;
19780
19781 const A: u32 = 42;
19782 + const B: u32 = 42;
19783 ˇ
19784 fn main() {
19785 println!("hello");
19786
19787 println!("world");
19788 }
19789 "#
19790 .unindent(),
19791 );
19792
19793 cx.update_editor(|editor, window, cx| {
19794 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19795 editor.delete_line(&DeleteLine, window, cx);
19796 });
19797 executor.run_until_parked();
19798 cx.assert_state_with_diff(
19799 r#"
19800 ˇ
19801 fn main() {
19802 println!("hello");
19803
19804 println!("world");
19805 }
19806 "#
19807 .unindent(),
19808 );
19809}
19810
19811#[gpui::test]
19812async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19813 init_test(cx, |_| {});
19814
19815 let mut cx = EditorTestContext::new(cx).await;
19816 cx.set_head_text(indoc! { "
19817 one
19818 two
19819 three
19820 four
19821 five
19822 "
19823 });
19824 cx.set_state(indoc! { "
19825 one
19826 ˇthree
19827 five
19828 "});
19829 cx.run_until_parked();
19830 cx.update_editor(|editor, window, cx| {
19831 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19832 });
19833 cx.assert_state_with_diff(
19834 indoc! { "
19835 one
19836 - two
19837 ˇthree
19838 - four
19839 five
19840 "}
19841 .to_string(),
19842 );
19843 cx.update_editor(|editor, window, cx| {
19844 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19845 });
19846
19847 cx.assert_state_with_diff(
19848 indoc! { "
19849 one
19850 ˇthree
19851 five
19852 "}
19853 .to_string(),
19854 );
19855
19856 cx.set_state(indoc! { "
19857 one
19858 ˇTWO
19859 three
19860 four
19861 five
19862 "});
19863 cx.run_until_parked();
19864 cx.update_editor(|editor, window, cx| {
19865 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19866 });
19867
19868 cx.assert_state_with_diff(
19869 indoc! { "
19870 one
19871 - two
19872 + ˇTWO
19873 three
19874 four
19875 five
19876 "}
19877 .to_string(),
19878 );
19879 cx.update_editor(|editor, window, cx| {
19880 editor.move_up(&Default::default(), window, cx);
19881 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19882 });
19883 cx.assert_state_with_diff(
19884 indoc! { "
19885 one
19886 ˇTWO
19887 three
19888 four
19889 five
19890 "}
19891 .to_string(),
19892 );
19893}
19894
19895#[gpui::test]
19896async fn test_edits_around_expanded_deletion_hunks(
19897 executor: BackgroundExecutor,
19898 cx: &mut TestAppContext,
19899) {
19900 init_test(cx, |_| {});
19901
19902 let mut cx = EditorTestContext::new(cx).await;
19903
19904 let diff_base = r#"
19905 use some::mod1;
19906 use some::mod2;
19907
19908 const A: u32 = 42;
19909 const B: u32 = 42;
19910 const C: u32 = 42;
19911
19912
19913 fn main() {
19914 println!("hello");
19915
19916 println!("world");
19917 }
19918 "#
19919 .unindent();
19920 executor.run_until_parked();
19921 cx.set_state(
19922 &r#"
19923 use some::mod1;
19924 use some::mod2;
19925
19926 ˇconst B: u32 = 42;
19927 const C: u32 = 42;
19928
19929
19930 fn main() {
19931 println!("hello");
19932
19933 println!("world");
19934 }
19935 "#
19936 .unindent(),
19937 );
19938
19939 cx.set_head_text(&diff_base);
19940 executor.run_until_parked();
19941
19942 cx.update_editor(|editor, window, cx| {
19943 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19944 });
19945 executor.run_until_parked();
19946
19947 cx.assert_state_with_diff(
19948 r#"
19949 use some::mod1;
19950 use some::mod2;
19951
19952 - const A: u32 = 42;
19953 ˇconst B: u32 = 42;
19954 const C: u32 = 42;
19955
19956
19957 fn main() {
19958 println!("hello");
19959
19960 println!("world");
19961 }
19962 "#
19963 .unindent(),
19964 );
19965
19966 cx.update_editor(|editor, window, cx| {
19967 editor.delete_line(&DeleteLine, window, cx);
19968 });
19969 executor.run_until_parked();
19970 cx.assert_state_with_diff(
19971 r#"
19972 use some::mod1;
19973 use some::mod2;
19974
19975 - const A: u32 = 42;
19976 - const B: u32 = 42;
19977 ˇconst C: u32 = 42;
19978
19979
19980 fn main() {
19981 println!("hello");
19982
19983 println!("world");
19984 }
19985 "#
19986 .unindent(),
19987 );
19988
19989 cx.update_editor(|editor, window, cx| {
19990 editor.delete_line(&DeleteLine, window, cx);
19991 });
19992 executor.run_until_parked();
19993 cx.assert_state_with_diff(
19994 r#"
19995 use some::mod1;
19996 use some::mod2;
19997
19998 - const A: u32 = 42;
19999 - const B: u32 = 42;
20000 - const C: u32 = 42;
20001 ˇ
20002
20003 fn main() {
20004 println!("hello");
20005
20006 println!("world");
20007 }
20008 "#
20009 .unindent(),
20010 );
20011
20012 cx.update_editor(|editor, window, cx| {
20013 editor.handle_input("replacement", window, cx);
20014 });
20015 executor.run_until_parked();
20016 cx.assert_state_with_diff(
20017 r#"
20018 use some::mod1;
20019 use some::mod2;
20020
20021 - const A: u32 = 42;
20022 - const B: u32 = 42;
20023 - const C: u32 = 42;
20024 -
20025 + replacementˇ
20026
20027 fn main() {
20028 println!("hello");
20029
20030 println!("world");
20031 }
20032 "#
20033 .unindent(),
20034 );
20035}
20036
20037#[gpui::test]
20038async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20039 init_test(cx, |_| {});
20040
20041 let mut cx = EditorTestContext::new(cx).await;
20042
20043 let base_text = r#"
20044 one
20045 two
20046 three
20047 four
20048 five
20049 "#
20050 .unindent();
20051 executor.run_until_parked();
20052 cx.set_state(
20053 &r#"
20054 one
20055 two
20056 fˇour
20057 five
20058 "#
20059 .unindent(),
20060 );
20061
20062 cx.set_head_text(&base_text);
20063 executor.run_until_parked();
20064
20065 cx.update_editor(|editor, window, cx| {
20066 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20067 });
20068 executor.run_until_parked();
20069
20070 cx.assert_state_with_diff(
20071 r#"
20072 one
20073 two
20074 - three
20075 fˇour
20076 five
20077 "#
20078 .unindent(),
20079 );
20080
20081 cx.update_editor(|editor, window, cx| {
20082 editor.backspace(&Backspace, window, cx);
20083 editor.backspace(&Backspace, window, cx);
20084 });
20085 executor.run_until_parked();
20086 cx.assert_state_with_diff(
20087 r#"
20088 one
20089 two
20090 - threeˇ
20091 - four
20092 + our
20093 five
20094 "#
20095 .unindent(),
20096 );
20097}
20098
20099#[gpui::test]
20100async fn test_edit_after_expanded_modification_hunk(
20101 executor: BackgroundExecutor,
20102 cx: &mut TestAppContext,
20103) {
20104 init_test(cx, |_| {});
20105
20106 let mut cx = EditorTestContext::new(cx).await;
20107
20108 let diff_base = r#"
20109 use some::mod1;
20110 use some::mod2;
20111
20112 const A: u32 = 42;
20113 const B: u32 = 42;
20114 const C: u32 = 42;
20115 const D: u32 = 42;
20116
20117
20118 fn main() {
20119 println!("hello");
20120
20121 println!("world");
20122 }"#
20123 .unindent();
20124
20125 cx.set_state(
20126 &r#"
20127 use some::mod1;
20128 use some::mod2;
20129
20130 const A: u32 = 42;
20131 const B: u32 = 42;
20132 const C: u32 = 43ˇ
20133 const D: u32 = 42;
20134
20135
20136 fn main() {
20137 println!("hello");
20138
20139 println!("world");
20140 }"#
20141 .unindent(),
20142 );
20143
20144 cx.set_head_text(&diff_base);
20145 executor.run_until_parked();
20146 cx.update_editor(|editor, window, cx| {
20147 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20148 });
20149 executor.run_until_parked();
20150
20151 cx.assert_state_with_diff(
20152 r#"
20153 use some::mod1;
20154 use some::mod2;
20155
20156 const A: u32 = 42;
20157 const B: u32 = 42;
20158 - const C: u32 = 42;
20159 + const C: u32 = 43ˇ
20160 const D: u32 = 42;
20161
20162
20163 fn main() {
20164 println!("hello");
20165
20166 println!("world");
20167 }"#
20168 .unindent(),
20169 );
20170
20171 cx.update_editor(|editor, window, cx| {
20172 editor.handle_input("\nnew_line\n", window, cx);
20173 });
20174 executor.run_until_parked();
20175
20176 cx.assert_state_with_diff(
20177 r#"
20178 use some::mod1;
20179 use some::mod2;
20180
20181 const A: u32 = 42;
20182 const B: u32 = 42;
20183 - const C: u32 = 42;
20184 + const C: u32 = 43
20185 + new_line
20186 + ˇ
20187 const D: u32 = 42;
20188
20189
20190 fn main() {
20191 println!("hello");
20192
20193 println!("world");
20194 }"#
20195 .unindent(),
20196 );
20197}
20198
20199#[gpui::test]
20200async fn test_stage_and_unstage_added_file_hunk(
20201 executor: BackgroundExecutor,
20202 cx: &mut TestAppContext,
20203) {
20204 init_test(cx, |_| {});
20205
20206 let mut cx = EditorTestContext::new(cx).await;
20207 cx.update_editor(|editor, _, cx| {
20208 editor.set_expand_all_diff_hunks(cx);
20209 });
20210
20211 let working_copy = r#"
20212 ˇfn main() {
20213 println!("hello, world!");
20214 }
20215 "#
20216 .unindent();
20217
20218 cx.set_state(&working_copy);
20219 executor.run_until_parked();
20220
20221 cx.assert_state_with_diff(
20222 r#"
20223 + ˇfn main() {
20224 + println!("hello, world!");
20225 + }
20226 "#
20227 .unindent(),
20228 );
20229 cx.assert_index_text(None);
20230
20231 cx.update_editor(|editor, window, cx| {
20232 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20233 });
20234 executor.run_until_parked();
20235 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20236 cx.assert_state_with_diff(
20237 r#"
20238 + ˇfn main() {
20239 + println!("hello, world!");
20240 + }
20241 "#
20242 .unindent(),
20243 );
20244
20245 cx.update_editor(|editor, window, cx| {
20246 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20247 });
20248 executor.run_until_parked();
20249 cx.assert_index_text(None);
20250}
20251
20252async fn setup_indent_guides_editor(
20253 text: &str,
20254 cx: &mut TestAppContext,
20255) -> (BufferId, EditorTestContext) {
20256 init_test(cx, |_| {});
20257
20258 let mut cx = EditorTestContext::new(cx).await;
20259
20260 let buffer_id = cx.update_editor(|editor, window, cx| {
20261 editor.set_text(text, window, cx);
20262 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20263
20264 buffer_ids[0]
20265 });
20266
20267 (buffer_id, cx)
20268}
20269
20270fn assert_indent_guides(
20271 range: Range<u32>,
20272 expected: Vec<IndentGuide>,
20273 active_indices: Option<Vec<usize>>,
20274 cx: &mut EditorTestContext,
20275) {
20276 let indent_guides = cx.update_editor(|editor, window, cx| {
20277 let snapshot = editor.snapshot(window, cx).display_snapshot;
20278 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20279 editor,
20280 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20281 true,
20282 &snapshot,
20283 cx,
20284 );
20285
20286 indent_guides.sort_by(|a, b| {
20287 a.depth.cmp(&b.depth).then(
20288 a.start_row
20289 .cmp(&b.start_row)
20290 .then(a.end_row.cmp(&b.end_row)),
20291 )
20292 });
20293 indent_guides
20294 });
20295
20296 if let Some(expected) = active_indices {
20297 let active_indices = cx.update_editor(|editor, window, cx| {
20298 let snapshot = editor.snapshot(window, cx).display_snapshot;
20299 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20300 });
20301
20302 assert_eq!(
20303 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20304 expected,
20305 "Active indent guide indices do not match"
20306 );
20307 }
20308
20309 assert_eq!(indent_guides, expected, "Indent guides do not match");
20310}
20311
20312fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20313 IndentGuide {
20314 buffer_id,
20315 start_row: MultiBufferRow(start_row),
20316 end_row: MultiBufferRow(end_row),
20317 depth,
20318 tab_size: 4,
20319 settings: IndentGuideSettings {
20320 enabled: true,
20321 line_width: 1,
20322 active_line_width: 1,
20323 coloring: IndentGuideColoring::default(),
20324 background_coloring: IndentGuideBackgroundColoring::default(),
20325 },
20326 }
20327}
20328
20329#[gpui::test]
20330async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20331 let (buffer_id, mut cx) = setup_indent_guides_editor(
20332 &"
20333 fn main() {
20334 let a = 1;
20335 }"
20336 .unindent(),
20337 cx,
20338 )
20339 .await;
20340
20341 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20342}
20343
20344#[gpui::test]
20345async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20346 let (buffer_id, mut cx) = setup_indent_guides_editor(
20347 &"
20348 fn main() {
20349 let a = 1;
20350 let b = 2;
20351 }"
20352 .unindent(),
20353 cx,
20354 )
20355 .await;
20356
20357 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20358}
20359
20360#[gpui::test]
20361async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20362 let (buffer_id, mut cx) = setup_indent_guides_editor(
20363 &"
20364 fn main() {
20365 let a = 1;
20366 if a == 3 {
20367 let b = 2;
20368 } else {
20369 let c = 3;
20370 }
20371 }"
20372 .unindent(),
20373 cx,
20374 )
20375 .await;
20376
20377 assert_indent_guides(
20378 0..8,
20379 vec![
20380 indent_guide(buffer_id, 1, 6, 0),
20381 indent_guide(buffer_id, 3, 3, 1),
20382 indent_guide(buffer_id, 5, 5, 1),
20383 ],
20384 None,
20385 &mut cx,
20386 );
20387}
20388
20389#[gpui::test]
20390async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20391 let (buffer_id, mut cx) = setup_indent_guides_editor(
20392 &"
20393 fn main() {
20394 let a = 1;
20395 let b = 2;
20396 let c = 3;
20397 }"
20398 .unindent(),
20399 cx,
20400 )
20401 .await;
20402
20403 assert_indent_guides(
20404 0..5,
20405 vec![
20406 indent_guide(buffer_id, 1, 3, 0),
20407 indent_guide(buffer_id, 2, 2, 1),
20408 ],
20409 None,
20410 &mut cx,
20411 );
20412}
20413
20414#[gpui::test]
20415async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20416 let (buffer_id, mut cx) = setup_indent_guides_editor(
20417 &"
20418 fn main() {
20419 let a = 1;
20420
20421 let c = 3;
20422 }"
20423 .unindent(),
20424 cx,
20425 )
20426 .await;
20427
20428 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20429}
20430
20431#[gpui::test]
20432async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20433 let (buffer_id, mut cx) = setup_indent_guides_editor(
20434 &"
20435 fn main() {
20436 let a = 1;
20437
20438 let c = 3;
20439
20440 if a == 3 {
20441 let b = 2;
20442 } else {
20443 let c = 3;
20444 }
20445 }"
20446 .unindent(),
20447 cx,
20448 )
20449 .await;
20450
20451 assert_indent_guides(
20452 0..11,
20453 vec![
20454 indent_guide(buffer_id, 1, 9, 0),
20455 indent_guide(buffer_id, 6, 6, 1),
20456 indent_guide(buffer_id, 8, 8, 1),
20457 ],
20458 None,
20459 &mut cx,
20460 );
20461}
20462
20463#[gpui::test]
20464async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20465 let (buffer_id, mut cx) = setup_indent_guides_editor(
20466 &"
20467 fn main() {
20468 let a = 1;
20469
20470 let c = 3;
20471
20472 if a == 3 {
20473 let b = 2;
20474 } else {
20475 let c = 3;
20476 }
20477 }"
20478 .unindent(),
20479 cx,
20480 )
20481 .await;
20482
20483 assert_indent_guides(
20484 1..11,
20485 vec![
20486 indent_guide(buffer_id, 1, 9, 0),
20487 indent_guide(buffer_id, 6, 6, 1),
20488 indent_guide(buffer_id, 8, 8, 1),
20489 ],
20490 None,
20491 &mut cx,
20492 );
20493}
20494
20495#[gpui::test]
20496async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20497 let (buffer_id, mut cx) = setup_indent_guides_editor(
20498 &"
20499 fn main() {
20500 let a = 1;
20501
20502 let c = 3;
20503
20504 if a == 3 {
20505 let b = 2;
20506 } else {
20507 let c = 3;
20508 }
20509 }"
20510 .unindent(),
20511 cx,
20512 )
20513 .await;
20514
20515 assert_indent_guides(
20516 1..10,
20517 vec![
20518 indent_guide(buffer_id, 1, 9, 0),
20519 indent_guide(buffer_id, 6, 6, 1),
20520 indent_guide(buffer_id, 8, 8, 1),
20521 ],
20522 None,
20523 &mut cx,
20524 );
20525}
20526
20527#[gpui::test]
20528async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20529 let (buffer_id, mut cx) = setup_indent_guides_editor(
20530 &"
20531 fn main() {
20532 if a {
20533 b(
20534 c,
20535 d,
20536 )
20537 } else {
20538 e(
20539 f
20540 )
20541 }
20542 }"
20543 .unindent(),
20544 cx,
20545 )
20546 .await;
20547
20548 assert_indent_guides(
20549 0..11,
20550 vec![
20551 indent_guide(buffer_id, 1, 10, 0),
20552 indent_guide(buffer_id, 2, 5, 1),
20553 indent_guide(buffer_id, 7, 9, 1),
20554 indent_guide(buffer_id, 3, 4, 2),
20555 indent_guide(buffer_id, 8, 8, 2),
20556 ],
20557 None,
20558 &mut cx,
20559 );
20560
20561 cx.update_editor(|editor, window, cx| {
20562 editor.fold_at(MultiBufferRow(2), window, cx);
20563 assert_eq!(
20564 editor.display_text(cx),
20565 "
20566 fn main() {
20567 if a {
20568 b(⋯
20569 )
20570 } else {
20571 e(
20572 f
20573 )
20574 }
20575 }"
20576 .unindent()
20577 );
20578 });
20579
20580 assert_indent_guides(
20581 0..11,
20582 vec![
20583 indent_guide(buffer_id, 1, 10, 0),
20584 indent_guide(buffer_id, 2, 5, 1),
20585 indent_guide(buffer_id, 7, 9, 1),
20586 indent_guide(buffer_id, 8, 8, 2),
20587 ],
20588 None,
20589 &mut cx,
20590 );
20591}
20592
20593#[gpui::test]
20594async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20595 let (buffer_id, mut cx) = setup_indent_guides_editor(
20596 &"
20597 block1
20598 block2
20599 block3
20600 block4
20601 block2
20602 block1
20603 block1"
20604 .unindent(),
20605 cx,
20606 )
20607 .await;
20608
20609 assert_indent_guides(
20610 1..10,
20611 vec![
20612 indent_guide(buffer_id, 1, 4, 0),
20613 indent_guide(buffer_id, 2, 3, 1),
20614 indent_guide(buffer_id, 3, 3, 2),
20615 ],
20616 None,
20617 &mut cx,
20618 );
20619}
20620
20621#[gpui::test]
20622async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20623 let (buffer_id, mut cx) = setup_indent_guides_editor(
20624 &"
20625 block1
20626 block2
20627 block3
20628
20629 block1
20630 block1"
20631 .unindent(),
20632 cx,
20633 )
20634 .await;
20635
20636 assert_indent_guides(
20637 0..6,
20638 vec![
20639 indent_guide(buffer_id, 1, 2, 0),
20640 indent_guide(buffer_id, 2, 2, 1),
20641 ],
20642 None,
20643 &mut cx,
20644 );
20645}
20646
20647#[gpui::test]
20648async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20649 let (buffer_id, mut cx) = setup_indent_guides_editor(
20650 &"
20651 function component() {
20652 \treturn (
20653 \t\t\t
20654 \t\t<div>
20655 \t\t\t<abc></abc>
20656 \t\t</div>
20657 \t)
20658 }"
20659 .unindent(),
20660 cx,
20661 )
20662 .await;
20663
20664 assert_indent_guides(
20665 0..8,
20666 vec![
20667 indent_guide(buffer_id, 1, 6, 0),
20668 indent_guide(buffer_id, 2, 5, 1),
20669 indent_guide(buffer_id, 4, 4, 2),
20670 ],
20671 None,
20672 &mut cx,
20673 );
20674}
20675
20676#[gpui::test]
20677async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20678 let (buffer_id, mut cx) = setup_indent_guides_editor(
20679 &"
20680 function component() {
20681 \treturn (
20682 \t
20683 \t\t<div>
20684 \t\t\t<abc></abc>
20685 \t\t</div>
20686 \t)
20687 }"
20688 .unindent(),
20689 cx,
20690 )
20691 .await;
20692
20693 assert_indent_guides(
20694 0..8,
20695 vec![
20696 indent_guide(buffer_id, 1, 6, 0),
20697 indent_guide(buffer_id, 2, 5, 1),
20698 indent_guide(buffer_id, 4, 4, 2),
20699 ],
20700 None,
20701 &mut cx,
20702 );
20703}
20704
20705#[gpui::test]
20706async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20707 let (buffer_id, mut cx) = setup_indent_guides_editor(
20708 &"
20709 block1
20710
20711
20712
20713 block2
20714 "
20715 .unindent(),
20716 cx,
20717 )
20718 .await;
20719
20720 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20721}
20722
20723#[gpui::test]
20724async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20725 let (buffer_id, mut cx) = setup_indent_guides_editor(
20726 &"
20727 def a:
20728 \tb = 3
20729 \tif True:
20730 \t\tc = 4
20731 \t\td = 5
20732 \tprint(b)
20733 "
20734 .unindent(),
20735 cx,
20736 )
20737 .await;
20738
20739 assert_indent_guides(
20740 0..6,
20741 vec![
20742 indent_guide(buffer_id, 1, 5, 0),
20743 indent_guide(buffer_id, 3, 4, 1),
20744 ],
20745 None,
20746 &mut cx,
20747 );
20748}
20749
20750#[gpui::test]
20751async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20752 let (buffer_id, mut cx) = setup_indent_guides_editor(
20753 &"
20754 fn main() {
20755 let a = 1;
20756 }"
20757 .unindent(),
20758 cx,
20759 )
20760 .await;
20761
20762 cx.update_editor(|editor, window, cx| {
20763 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20764 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20765 });
20766 });
20767
20768 assert_indent_guides(
20769 0..3,
20770 vec![indent_guide(buffer_id, 1, 1, 0)],
20771 Some(vec![0]),
20772 &mut cx,
20773 );
20774}
20775
20776#[gpui::test]
20777async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20778 let (buffer_id, mut cx) = setup_indent_guides_editor(
20779 &"
20780 fn main() {
20781 if 1 == 2 {
20782 let a = 1;
20783 }
20784 }"
20785 .unindent(),
20786 cx,
20787 )
20788 .await;
20789
20790 cx.update_editor(|editor, window, cx| {
20791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20792 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20793 });
20794 });
20795
20796 assert_indent_guides(
20797 0..4,
20798 vec![
20799 indent_guide(buffer_id, 1, 3, 0),
20800 indent_guide(buffer_id, 2, 2, 1),
20801 ],
20802 Some(vec![1]),
20803 &mut cx,
20804 );
20805
20806 cx.update_editor(|editor, window, cx| {
20807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20808 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20809 });
20810 });
20811
20812 assert_indent_guides(
20813 0..4,
20814 vec![
20815 indent_guide(buffer_id, 1, 3, 0),
20816 indent_guide(buffer_id, 2, 2, 1),
20817 ],
20818 Some(vec![1]),
20819 &mut cx,
20820 );
20821
20822 cx.update_editor(|editor, window, cx| {
20823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20824 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20825 });
20826 });
20827
20828 assert_indent_guides(
20829 0..4,
20830 vec![
20831 indent_guide(buffer_id, 1, 3, 0),
20832 indent_guide(buffer_id, 2, 2, 1),
20833 ],
20834 Some(vec![0]),
20835 &mut cx,
20836 );
20837}
20838
20839#[gpui::test]
20840async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20841 let (buffer_id, mut cx) = setup_indent_guides_editor(
20842 &"
20843 fn main() {
20844 let a = 1;
20845
20846 let b = 2;
20847 }"
20848 .unindent(),
20849 cx,
20850 )
20851 .await;
20852
20853 cx.update_editor(|editor, window, cx| {
20854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20855 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20856 });
20857 });
20858
20859 assert_indent_guides(
20860 0..5,
20861 vec![indent_guide(buffer_id, 1, 3, 0)],
20862 Some(vec![0]),
20863 &mut cx,
20864 );
20865}
20866
20867#[gpui::test]
20868async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20869 let (buffer_id, mut cx) = setup_indent_guides_editor(
20870 &"
20871 def m:
20872 a = 1
20873 pass"
20874 .unindent(),
20875 cx,
20876 )
20877 .await;
20878
20879 cx.update_editor(|editor, window, cx| {
20880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20881 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20882 });
20883 });
20884
20885 assert_indent_guides(
20886 0..3,
20887 vec![indent_guide(buffer_id, 1, 2, 0)],
20888 Some(vec![0]),
20889 &mut cx,
20890 );
20891}
20892
20893#[gpui::test]
20894async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20895 init_test(cx, |_| {});
20896 let mut cx = EditorTestContext::new(cx).await;
20897 let text = indoc! {
20898 "
20899 impl A {
20900 fn b() {
20901 0;
20902 3;
20903 5;
20904 6;
20905 7;
20906 }
20907 }
20908 "
20909 };
20910 let base_text = indoc! {
20911 "
20912 impl A {
20913 fn b() {
20914 0;
20915 1;
20916 2;
20917 3;
20918 4;
20919 }
20920 fn c() {
20921 5;
20922 6;
20923 7;
20924 }
20925 }
20926 "
20927 };
20928
20929 cx.update_editor(|editor, window, cx| {
20930 editor.set_text(text, window, cx);
20931
20932 editor.buffer().update(cx, |multibuffer, cx| {
20933 let buffer = multibuffer.as_singleton().unwrap();
20934 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20935
20936 multibuffer.set_all_diff_hunks_expanded(cx);
20937 multibuffer.add_diff(diff, cx);
20938
20939 buffer.read(cx).remote_id()
20940 })
20941 });
20942 cx.run_until_parked();
20943
20944 cx.assert_state_with_diff(
20945 indoc! { "
20946 impl A {
20947 fn b() {
20948 0;
20949 - 1;
20950 - 2;
20951 3;
20952 - 4;
20953 - }
20954 - fn c() {
20955 5;
20956 6;
20957 7;
20958 }
20959 }
20960 ˇ"
20961 }
20962 .to_string(),
20963 );
20964
20965 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20966 editor
20967 .snapshot(window, cx)
20968 .buffer_snapshot()
20969 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20970 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20971 .collect::<Vec<_>>()
20972 });
20973 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20974 assert_eq!(
20975 actual_guides,
20976 vec![
20977 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20978 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20979 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20980 ]
20981 );
20982}
20983
20984#[gpui::test]
20985async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20986 init_test(cx, |_| {});
20987 let mut cx = EditorTestContext::new(cx).await;
20988
20989 let diff_base = r#"
20990 a
20991 b
20992 c
20993 "#
20994 .unindent();
20995
20996 cx.set_state(
20997 &r#"
20998 ˇA
20999 b
21000 C
21001 "#
21002 .unindent(),
21003 );
21004 cx.set_head_text(&diff_base);
21005 cx.update_editor(|editor, window, cx| {
21006 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21007 });
21008 executor.run_until_parked();
21009
21010 let both_hunks_expanded = r#"
21011 - a
21012 + ˇA
21013 b
21014 - c
21015 + C
21016 "#
21017 .unindent();
21018
21019 cx.assert_state_with_diff(both_hunks_expanded.clone());
21020
21021 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21022 let snapshot = editor.snapshot(window, cx);
21023 let hunks = editor
21024 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21025 .collect::<Vec<_>>();
21026 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21027 let buffer_id = hunks[0].buffer_id;
21028 hunks
21029 .into_iter()
21030 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21031 .collect::<Vec<_>>()
21032 });
21033 assert_eq!(hunk_ranges.len(), 2);
21034
21035 cx.update_editor(|editor, _, cx| {
21036 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21037 });
21038 executor.run_until_parked();
21039
21040 let second_hunk_expanded = r#"
21041 ˇA
21042 b
21043 - c
21044 + C
21045 "#
21046 .unindent();
21047
21048 cx.assert_state_with_diff(second_hunk_expanded);
21049
21050 cx.update_editor(|editor, _, cx| {
21051 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21052 });
21053 executor.run_until_parked();
21054
21055 cx.assert_state_with_diff(both_hunks_expanded.clone());
21056
21057 cx.update_editor(|editor, _, cx| {
21058 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21059 });
21060 executor.run_until_parked();
21061
21062 let first_hunk_expanded = r#"
21063 - a
21064 + ˇA
21065 b
21066 C
21067 "#
21068 .unindent();
21069
21070 cx.assert_state_with_diff(first_hunk_expanded);
21071
21072 cx.update_editor(|editor, _, cx| {
21073 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21074 });
21075 executor.run_until_parked();
21076
21077 cx.assert_state_with_diff(both_hunks_expanded);
21078
21079 cx.set_state(
21080 &r#"
21081 ˇA
21082 b
21083 "#
21084 .unindent(),
21085 );
21086 cx.run_until_parked();
21087
21088 // TODO this cursor position seems bad
21089 cx.assert_state_with_diff(
21090 r#"
21091 - ˇa
21092 + A
21093 b
21094 "#
21095 .unindent(),
21096 );
21097
21098 cx.update_editor(|editor, window, cx| {
21099 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21100 });
21101
21102 cx.assert_state_with_diff(
21103 r#"
21104 - ˇa
21105 + A
21106 b
21107 - c
21108 "#
21109 .unindent(),
21110 );
21111
21112 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21113 let snapshot = editor.snapshot(window, cx);
21114 let hunks = editor
21115 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21116 .collect::<Vec<_>>();
21117 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21118 let buffer_id = hunks[0].buffer_id;
21119 hunks
21120 .into_iter()
21121 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21122 .collect::<Vec<_>>()
21123 });
21124 assert_eq!(hunk_ranges.len(), 2);
21125
21126 cx.update_editor(|editor, _, cx| {
21127 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21128 });
21129 executor.run_until_parked();
21130
21131 cx.assert_state_with_diff(
21132 r#"
21133 - ˇa
21134 + A
21135 b
21136 "#
21137 .unindent(),
21138 );
21139}
21140
21141#[gpui::test]
21142async fn test_toggle_deletion_hunk_at_start_of_file(
21143 executor: BackgroundExecutor,
21144 cx: &mut TestAppContext,
21145) {
21146 init_test(cx, |_| {});
21147 let mut cx = EditorTestContext::new(cx).await;
21148
21149 let diff_base = r#"
21150 a
21151 b
21152 c
21153 "#
21154 .unindent();
21155
21156 cx.set_state(
21157 &r#"
21158 ˇb
21159 c
21160 "#
21161 .unindent(),
21162 );
21163 cx.set_head_text(&diff_base);
21164 cx.update_editor(|editor, window, cx| {
21165 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21166 });
21167 executor.run_until_parked();
21168
21169 let hunk_expanded = r#"
21170 - a
21171 ˇb
21172 c
21173 "#
21174 .unindent();
21175
21176 cx.assert_state_with_diff(hunk_expanded.clone());
21177
21178 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21179 let snapshot = editor.snapshot(window, cx);
21180 let hunks = editor
21181 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21182 .collect::<Vec<_>>();
21183 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21184 let buffer_id = hunks[0].buffer_id;
21185 hunks
21186 .into_iter()
21187 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21188 .collect::<Vec<_>>()
21189 });
21190 assert_eq!(hunk_ranges.len(), 1);
21191
21192 cx.update_editor(|editor, _, cx| {
21193 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21194 });
21195 executor.run_until_parked();
21196
21197 let hunk_collapsed = r#"
21198 ˇb
21199 c
21200 "#
21201 .unindent();
21202
21203 cx.assert_state_with_diff(hunk_collapsed);
21204
21205 cx.update_editor(|editor, _, cx| {
21206 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21207 });
21208 executor.run_until_parked();
21209
21210 cx.assert_state_with_diff(hunk_expanded);
21211}
21212
21213#[gpui::test]
21214async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21215 init_test(cx, |_| {});
21216
21217 let fs = FakeFs::new(cx.executor());
21218 fs.insert_tree(
21219 path!("/test"),
21220 json!({
21221 ".git": {},
21222 "file-1": "ONE\n",
21223 "file-2": "TWO\n",
21224 "file-3": "THREE\n",
21225 }),
21226 )
21227 .await;
21228
21229 fs.set_head_for_repo(
21230 path!("/test/.git").as_ref(),
21231 &[
21232 ("file-1", "one\n".into()),
21233 ("file-2", "two\n".into()),
21234 ("file-3", "three\n".into()),
21235 ],
21236 "deadbeef",
21237 );
21238
21239 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21240 let mut buffers = vec![];
21241 for i in 1..=3 {
21242 let buffer = project
21243 .update(cx, |project, cx| {
21244 let path = format!(path!("/test/file-{}"), i);
21245 project.open_local_buffer(path, cx)
21246 })
21247 .await
21248 .unwrap();
21249 buffers.push(buffer);
21250 }
21251
21252 let multibuffer = cx.new(|cx| {
21253 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21254 multibuffer.set_all_diff_hunks_expanded(cx);
21255 for buffer in &buffers {
21256 let snapshot = buffer.read(cx).snapshot();
21257 multibuffer.set_excerpts_for_path(
21258 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21259 buffer.clone(),
21260 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21261 2,
21262 cx,
21263 );
21264 }
21265 multibuffer
21266 });
21267
21268 let editor = cx.add_window(|window, cx| {
21269 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21270 });
21271 cx.run_until_parked();
21272
21273 let snapshot = editor
21274 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21275 .unwrap();
21276 let hunks = snapshot
21277 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21278 .map(|hunk| match hunk {
21279 DisplayDiffHunk::Unfolded {
21280 display_row_range, ..
21281 } => display_row_range,
21282 DisplayDiffHunk::Folded { .. } => unreachable!(),
21283 })
21284 .collect::<Vec<_>>();
21285 assert_eq!(
21286 hunks,
21287 [
21288 DisplayRow(2)..DisplayRow(4),
21289 DisplayRow(7)..DisplayRow(9),
21290 DisplayRow(12)..DisplayRow(14),
21291 ]
21292 );
21293}
21294
21295#[gpui::test]
21296async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21297 init_test(cx, |_| {});
21298
21299 let mut cx = EditorTestContext::new(cx).await;
21300 cx.set_head_text(indoc! { "
21301 one
21302 two
21303 three
21304 four
21305 five
21306 "
21307 });
21308 cx.set_index_text(indoc! { "
21309 one
21310 two
21311 three
21312 four
21313 five
21314 "
21315 });
21316 cx.set_state(indoc! {"
21317 one
21318 TWO
21319 ˇTHREE
21320 FOUR
21321 five
21322 "});
21323 cx.run_until_parked();
21324 cx.update_editor(|editor, window, cx| {
21325 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21326 });
21327 cx.run_until_parked();
21328 cx.assert_index_text(Some(indoc! {"
21329 one
21330 TWO
21331 THREE
21332 FOUR
21333 five
21334 "}));
21335 cx.set_state(indoc! { "
21336 one
21337 TWO
21338 ˇTHREE-HUNDRED
21339 FOUR
21340 five
21341 "});
21342 cx.run_until_parked();
21343 cx.update_editor(|editor, window, cx| {
21344 let snapshot = editor.snapshot(window, cx);
21345 let hunks = editor
21346 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21347 .collect::<Vec<_>>();
21348 assert_eq!(hunks.len(), 1);
21349 assert_eq!(
21350 hunks[0].status(),
21351 DiffHunkStatus {
21352 kind: DiffHunkStatusKind::Modified,
21353 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21354 }
21355 );
21356
21357 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21358 });
21359 cx.run_until_parked();
21360 cx.assert_index_text(Some(indoc! {"
21361 one
21362 TWO
21363 THREE-HUNDRED
21364 FOUR
21365 five
21366 "}));
21367}
21368
21369#[gpui::test]
21370fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21371 init_test(cx, |_| {});
21372
21373 let editor = cx.add_window(|window, cx| {
21374 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21375 build_editor(buffer, window, cx)
21376 });
21377
21378 let render_args = Arc::new(Mutex::new(None));
21379 let snapshot = editor
21380 .update(cx, |editor, window, cx| {
21381 let snapshot = editor.buffer().read(cx).snapshot(cx);
21382 let range =
21383 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21384
21385 struct RenderArgs {
21386 row: MultiBufferRow,
21387 folded: bool,
21388 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21389 }
21390
21391 let crease = Crease::inline(
21392 range,
21393 FoldPlaceholder::test(),
21394 {
21395 let toggle_callback = render_args.clone();
21396 move |row, folded, callback, _window, _cx| {
21397 *toggle_callback.lock() = Some(RenderArgs {
21398 row,
21399 folded,
21400 callback,
21401 });
21402 div()
21403 }
21404 },
21405 |_row, _folded, _window, _cx| div(),
21406 );
21407
21408 editor.insert_creases(Some(crease), cx);
21409 let snapshot = editor.snapshot(window, cx);
21410 let _div =
21411 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21412 snapshot
21413 })
21414 .unwrap();
21415
21416 let render_args = render_args.lock().take().unwrap();
21417 assert_eq!(render_args.row, MultiBufferRow(1));
21418 assert!(!render_args.folded);
21419 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21420
21421 cx.update_window(*editor, |_, window, cx| {
21422 (render_args.callback)(true, window, cx)
21423 })
21424 .unwrap();
21425 let snapshot = editor
21426 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21427 .unwrap();
21428 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21429
21430 cx.update_window(*editor, |_, window, cx| {
21431 (render_args.callback)(false, window, cx)
21432 })
21433 .unwrap();
21434 let snapshot = editor
21435 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21436 .unwrap();
21437 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21438}
21439
21440#[gpui::test]
21441async fn test_input_text(cx: &mut TestAppContext) {
21442 init_test(cx, |_| {});
21443 let mut cx = EditorTestContext::new(cx).await;
21444
21445 cx.set_state(
21446 &r#"ˇone
21447 two
21448
21449 three
21450 fourˇ
21451 five
21452
21453 siˇx"#
21454 .unindent(),
21455 );
21456
21457 cx.dispatch_action(HandleInput(String::new()));
21458 cx.assert_editor_state(
21459 &r#"ˇone
21460 two
21461
21462 three
21463 fourˇ
21464 five
21465
21466 siˇx"#
21467 .unindent(),
21468 );
21469
21470 cx.dispatch_action(HandleInput("AAAA".to_string()));
21471 cx.assert_editor_state(
21472 &r#"AAAAˇone
21473 two
21474
21475 three
21476 fourAAAAˇ
21477 five
21478
21479 siAAAAˇx"#
21480 .unindent(),
21481 );
21482}
21483
21484#[gpui::test]
21485async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21486 init_test(cx, |_| {});
21487
21488 let mut cx = EditorTestContext::new(cx).await;
21489 cx.set_state(
21490 r#"let foo = 1;
21491let foo = 2;
21492let foo = 3;
21493let fooˇ = 4;
21494let foo = 5;
21495let foo = 6;
21496let foo = 7;
21497let foo = 8;
21498let foo = 9;
21499let foo = 10;
21500let foo = 11;
21501let foo = 12;
21502let foo = 13;
21503let foo = 14;
21504let foo = 15;"#,
21505 );
21506
21507 cx.update_editor(|e, window, cx| {
21508 assert_eq!(
21509 e.next_scroll_position,
21510 NextScrollCursorCenterTopBottom::Center,
21511 "Default next scroll direction is center",
21512 );
21513
21514 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21515 assert_eq!(
21516 e.next_scroll_position,
21517 NextScrollCursorCenterTopBottom::Top,
21518 "After center, next scroll direction should be top",
21519 );
21520
21521 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21522 assert_eq!(
21523 e.next_scroll_position,
21524 NextScrollCursorCenterTopBottom::Bottom,
21525 "After top, next scroll direction should be bottom",
21526 );
21527
21528 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21529 assert_eq!(
21530 e.next_scroll_position,
21531 NextScrollCursorCenterTopBottom::Center,
21532 "After bottom, scrolling should start over",
21533 );
21534
21535 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21536 assert_eq!(
21537 e.next_scroll_position,
21538 NextScrollCursorCenterTopBottom::Top,
21539 "Scrolling continues if retriggered fast enough"
21540 );
21541 });
21542
21543 cx.executor()
21544 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21545 cx.executor().run_until_parked();
21546 cx.update_editor(|e, _, _| {
21547 assert_eq!(
21548 e.next_scroll_position,
21549 NextScrollCursorCenterTopBottom::Center,
21550 "If scrolling is not triggered fast enough, it should reset"
21551 );
21552 });
21553}
21554
21555#[gpui::test]
21556async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21557 init_test(cx, |_| {});
21558 let mut cx = EditorLspTestContext::new_rust(
21559 lsp::ServerCapabilities {
21560 definition_provider: Some(lsp::OneOf::Left(true)),
21561 references_provider: Some(lsp::OneOf::Left(true)),
21562 ..lsp::ServerCapabilities::default()
21563 },
21564 cx,
21565 )
21566 .await;
21567
21568 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21569 let go_to_definition = cx
21570 .lsp
21571 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21572 move |params, _| async move {
21573 if empty_go_to_definition {
21574 Ok(None)
21575 } else {
21576 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21577 uri: params.text_document_position_params.text_document.uri,
21578 range: lsp::Range::new(
21579 lsp::Position::new(4, 3),
21580 lsp::Position::new(4, 6),
21581 ),
21582 })))
21583 }
21584 },
21585 );
21586 let references = cx
21587 .lsp
21588 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21589 Ok(Some(vec![lsp::Location {
21590 uri: params.text_document_position.text_document.uri,
21591 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21592 }]))
21593 });
21594 (go_to_definition, references)
21595 };
21596
21597 cx.set_state(
21598 &r#"fn one() {
21599 let mut a = ˇtwo();
21600 }
21601
21602 fn two() {}"#
21603 .unindent(),
21604 );
21605 set_up_lsp_handlers(false, &mut cx);
21606 let navigated = cx
21607 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21608 .await
21609 .expect("Failed to navigate to definition");
21610 assert_eq!(
21611 navigated,
21612 Navigated::Yes,
21613 "Should have navigated to definition from the GetDefinition response"
21614 );
21615 cx.assert_editor_state(
21616 &r#"fn one() {
21617 let mut a = two();
21618 }
21619
21620 fn «twoˇ»() {}"#
21621 .unindent(),
21622 );
21623
21624 let editors = cx.update_workspace(|workspace, _, cx| {
21625 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21626 });
21627 cx.update_editor(|_, _, test_editor_cx| {
21628 assert_eq!(
21629 editors.len(),
21630 1,
21631 "Initially, only one, test, editor should be open in the workspace"
21632 );
21633 assert_eq!(
21634 test_editor_cx.entity(),
21635 editors.last().expect("Asserted len is 1").clone()
21636 );
21637 });
21638
21639 set_up_lsp_handlers(true, &mut cx);
21640 let navigated = cx
21641 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21642 .await
21643 .expect("Failed to navigate to lookup references");
21644 assert_eq!(
21645 navigated,
21646 Navigated::Yes,
21647 "Should have navigated to references as a fallback after empty GoToDefinition response"
21648 );
21649 // We should not change the selections in the existing file,
21650 // if opening another milti buffer with the references
21651 cx.assert_editor_state(
21652 &r#"fn one() {
21653 let mut a = two();
21654 }
21655
21656 fn «twoˇ»() {}"#
21657 .unindent(),
21658 );
21659 let editors = cx.update_workspace(|workspace, _, cx| {
21660 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21661 });
21662 cx.update_editor(|_, _, test_editor_cx| {
21663 assert_eq!(
21664 editors.len(),
21665 2,
21666 "After falling back to references search, we open a new editor with the results"
21667 );
21668 let references_fallback_text = editors
21669 .into_iter()
21670 .find(|new_editor| *new_editor != test_editor_cx.entity())
21671 .expect("Should have one non-test editor now")
21672 .read(test_editor_cx)
21673 .text(test_editor_cx);
21674 assert_eq!(
21675 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21676 "Should use the range from the references response and not the GoToDefinition one"
21677 );
21678 });
21679}
21680
21681#[gpui::test]
21682async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21683 init_test(cx, |_| {});
21684 cx.update(|cx| {
21685 let mut editor_settings = EditorSettings::get_global(cx).clone();
21686 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21687 EditorSettings::override_global(editor_settings, cx);
21688 });
21689 let mut cx = EditorLspTestContext::new_rust(
21690 lsp::ServerCapabilities {
21691 definition_provider: Some(lsp::OneOf::Left(true)),
21692 references_provider: Some(lsp::OneOf::Left(true)),
21693 ..lsp::ServerCapabilities::default()
21694 },
21695 cx,
21696 )
21697 .await;
21698 let original_state = r#"fn one() {
21699 let mut a = ˇtwo();
21700 }
21701
21702 fn two() {}"#
21703 .unindent();
21704 cx.set_state(&original_state);
21705
21706 let mut go_to_definition = cx
21707 .lsp
21708 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21709 move |_, _| async move { Ok(None) },
21710 );
21711 let _references = cx
21712 .lsp
21713 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21714 panic!("Should not call for references with no go to definition fallback")
21715 });
21716
21717 let navigated = cx
21718 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21719 .await
21720 .expect("Failed to navigate to lookup references");
21721 go_to_definition
21722 .next()
21723 .await
21724 .expect("Should have called the go_to_definition handler");
21725
21726 assert_eq!(
21727 navigated,
21728 Navigated::No,
21729 "Should have navigated to references as a fallback after empty GoToDefinition response"
21730 );
21731 cx.assert_editor_state(&original_state);
21732 let editors = cx.update_workspace(|workspace, _, cx| {
21733 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21734 });
21735 cx.update_editor(|_, _, _| {
21736 assert_eq!(
21737 editors.len(),
21738 1,
21739 "After unsuccessful fallback, no other editor should have been opened"
21740 );
21741 });
21742}
21743
21744#[gpui::test]
21745async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21746 init_test(cx, |_| {});
21747 let mut cx = EditorLspTestContext::new_rust(
21748 lsp::ServerCapabilities {
21749 references_provider: Some(lsp::OneOf::Left(true)),
21750 ..lsp::ServerCapabilities::default()
21751 },
21752 cx,
21753 )
21754 .await;
21755
21756 cx.set_state(
21757 &r#"
21758 fn one() {
21759 let mut a = two();
21760 }
21761
21762 fn ˇtwo() {}"#
21763 .unindent(),
21764 );
21765 cx.lsp
21766 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21767 Ok(Some(vec![
21768 lsp::Location {
21769 uri: params.text_document_position.text_document.uri.clone(),
21770 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21771 },
21772 lsp::Location {
21773 uri: params.text_document_position.text_document.uri,
21774 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21775 },
21776 ]))
21777 });
21778 let navigated = cx
21779 .update_editor(|editor, window, cx| {
21780 editor.find_all_references(&FindAllReferences, window, cx)
21781 })
21782 .unwrap()
21783 .await
21784 .expect("Failed to navigate to references");
21785 assert_eq!(
21786 navigated,
21787 Navigated::Yes,
21788 "Should have navigated to references from the FindAllReferences response"
21789 );
21790 cx.assert_editor_state(
21791 &r#"fn one() {
21792 let mut a = two();
21793 }
21794
21795 fn ˇtwo() {}"#
21796 .unindent(),
21797 );
21798
21799 let editors = cx.update_workspace(|workspace, _, cx| {
21800 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21801 });
21802 cx.update_editor(|_, _, _| {
21803 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21804 });
21805
21806 cx.set_state(
21807 &r#"fn one() {
21808 let mut a = ˇtwo();
21809 }
21810
21811 fn two() {}"#
21812 .unindent(),
21813 );
21814 let navigated = cx
21815 .update_editor(|editor, window, cx| {
21816 editor.find_all_references(&FindAllReferences, window, cx)
21817 })
21818 .unwrap()
21819 .await
21820 .expect("Failed to navigate to references");
21821 assert_eq!(
21822 navigated,
21823 Navigated::Yes,
21824 "Should have navigated to references from the FindAllReferences response"
21825 );
21826 cx.assert_editor_state(
21827 &r#"fn one() {
21828 let mut a = ˇtwo();
21829 }
21830
21831 fn two() {}"#
21832 .unindent(),
21833 );
21834 let editors = cx.update_workspace(|workspace, _, cx| {
21835 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21836 });
21837 cx.update_editor(|_, _, _| {
21838 assert_eq!(
21839 editors.len(),
21840 2,
21841 "should have re-used the previous multibuffer"
21842 );
21843 });
21844
21845 cx.set_state(
21846 &r#"fn one() {
21847 let mut a = ˇtwo();
21848 }
21849 fn three() {}
21850 fn two() {}"#
21851 .unindent(),
21852 );
21853 cx.lsp
21854 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21855 Ok(Some(vec![
21856 lsp::Location {
21857 uri: params.text_document_position.text_document.uri.clone(),
21858 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21859 },
21860 lsp::Location {
21861 uri: params.text_document_position.text_document.uri,
21862 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21863 },
21864 ]))
21865 });
21866 let navigated = cx
21867 .update_editor(|editor, window, cx| {
21868 editor.find_all_references(&FindAllReferences, window, cx)
21869 })
21870 .unwrap()
21871 .await
21872 .expect("Failed to navigate to references");
21873 assert_eq!(
21874 navigated,
21875 Navigated::Yes,
21876 "Should have navigated to references from the FindAllReferences response"
21877 );
21878 cx.assert_editor_state(
21879 &r#"fn one() {
21880 let mut a = ˇtwo();
21881 }
21882 fn three() {}
21883 fn two() {}"#
21884 .unindent(),
21885 );
21886 let editors = cx.update_workspace(|workspace, _, cx| {
21887 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21888 });
21889 cx.update_editor(|_, _, _| {
21890 assert_eq!(
21891 editors.len(),
21892 3,
21893 "should have used a new multibuffer as offsets changed"
21894 );
21895 });
21896}
21897#[gpui::test]
21898async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21899 init_test(cx, |_| {});
21900
21901 let language = Arc::new(Language::new(
21902 LanguageConfig::default(),
21903 Some(tree_sitter_rust::LANGUAGE.into()),
21904 ));
21905
21906 let text = r#"
21907 #[cfg(test)]
21908 mod tests() {
21909 #[test]
21910 fn runnable_1() {
21911 let a = 1;
21912 }
21913
21914 #[test]
21915 fn runnable_2() {
21916 let a = 1;
21917 let b = 2;
21918 }
21919 }
21920 "#
21921 .unindent();
21922
21923 let fs = FakeFs::new(cx.executor());
21924 fs.insert_file("/file.rs", Default::default()).await;
21925
21926 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21927 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21928 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21929 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21930 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21931
21932 let editor = cx.new_window_entity(|window, cx| {
21933 Editor::new(
21934 EditorMode::full(),
21935 multi_buffer,
21936 Some(project.clone()),
21937 window,
21938 cx,
21939 )
21940 });
21941
21942 editor.update_in(cx, |editor, window, cx| {
21943 let snapshot = editor.buffer().read(cx).snapshot(cx);
21944 editor.tasks.insert(
21945 (buffer.read(cx).remote_id(), 3),
21946 RunnableTasks {
21947 templates: vec![],
21948 offset: snapshot.anchor_before(43),
21949 column: 0,
21950 extra_variables: HashMap::default(),
21951 context_range: BufferOffset(43)..BufferOffset(85),
21952 },
21953 );
21954 editor.tasks.insert(
21955 (buffer.read(cx).remote_id(), 8),
21956 RunnableTasks {
21957 templates: vec![],
21958 offset: snapshot.anchor_before(86),
21959 column: 0,
21960 extra_variables: HashMap::default(),
21961 context_range: BufferOffset(86)..BufferOffset(191),
21962 },
21963 );
21964
21965 // Test finding task when cursor is inside function body
21966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21967 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21968 });
21969 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21970 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21971
21972 // Test finding task when cursor is on function name
21973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21974 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21975 });
21976 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21977 assert_eq!(row, 8, "Should find task when cursor is on function name");
21978 });
21979}
21980
21981#[gpui::test]
21982async fn test_folding_buffers(cx: &mut TestAppContext) {
21983 init_test(cx, |_| {});
21984
21985 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21986 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21987 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21988
21989 let fs = FakeFs::new(cx.executor());
21990 fs.insert_tree(
21991 path!("/a"),
21992 json!({
21993 "first.rs": sample_text_1,
21994 "second.rs": sample_text_2,
21995 "third.rs": sample_text_3,
21996 }),
21997 )
21998 .await;
21999 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22000 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22001 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22002 let worktree = project.update(cx, |project, cx| {
22003 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22004 assert_eq!(worktrees.len(), 1);
22005 worktrees.pop().unwrap()
22006 });
22007 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22008
22009 let buffer_1 = project
22010 .update(cx, |project, cx| {
22011 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22012 })
22013 .await
22014 .unwrap();
22015 let buffer_2 = project
22016 .update(cx, |project, cx| {
22017 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22018 })
22019 .await
22020 .unwrap();
22021 let buffer_3 = project
22022 .update(cx, |project, cx| {
22023 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22024 })
22025 .await
22026 .unwrap();
22027
22028 let multi_buffer = cx.new(|cx| {
22029 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22030 multi_buffer.push_excerpts(
22031 buffer_1.clone(),
22032 [
22033 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22034 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22035 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22036 ],
22037 cx,
22038 );
22039 multi_buffer.push_excerpts(
22040 buffer_2.clone(),
22041 [
22042 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22043 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22044 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22045 ],
22046 cx,
22047 );
22048 multi_buffer.push_excerpts(
22049 buffer_3.clone(),
22050 [
22051 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22052 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22053 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22054 ],
22055 cx,
22056 );
22057 multi_buffer
22058 });
22059 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22060 Editor::new(
22061 EditorMode::full(),
22062 multi_buffer.clone(),
22063 Some(project.clone()),
22064 window,
22065 cx,
22066 )
22067 });
22068
22069 assert_eq!(
22070 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22071 "\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",
22072 );
22073
22074 multi_buffer_editor.update(cx, |editor, cx| {
22075 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22076 });
22077 assert_eq!(
22078 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22079 "\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",
22080 "After folding the first buffer, its text should not be displayed"
22081 );
22082
22083 multi_buffer_editor.update(cx, |editor, cx| {
22084 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22085 });
22086 assert_eq!(
22087 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22088 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22089 "After folding the second buffer, its text should not be displayed"
22090 );
22091
22092 multi_buffer_editor.update(cx, |editor, cx| {
22093 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22094 });
22095 assert_eq!(
22096 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22097 "\n\n\n\n\n",
22098 "After folding the third buffer, its text should not be displayed"
22099 );
22100
22101 // Emulate selection inside the fold logic, that should work
22102 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22103 editor
22104 .snapshot(window, cx)
22105 .next_line_boundary(Point::new(0, 4));
22106 });
22107
22108 multi_buffer_editor.update(cx, |editor, cx| {
22109 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22110 });
22111 assert_eq!(
22112 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22113 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22114 "After unfolding the second buffer, its text should be displayed"
22115 );
22116
22117 // Typing inside of buffer 1 causes that buffer to be unfolded.
22118 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22119 assert_eq!(
22120 multi_buffer
22121 .read(cx)
22122 .snapshot(cx)
22123 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22124 .collect::<String>(),
22125 "bbbb"
22126 );
22127 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22128 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22129 });
22130 editor.handle_input("B", window, cx);
22131 });
22132
22133 assert_eq!(
22134 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22135 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22136 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22137 );
22138
22139 multi_buffer_editor.update(cx, |editor, cx| {
22140 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22141 });
22142 assert_eq!(
22143 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22144 "\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",
22145 "After unfolding the all buffers, all original text should be displayed"
22146 );
22147}
22148
22149#[gpui::test]
22150async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22151 init_test(cx, |_| {});
22152
22153 let sample_text_1 = "1111\n2222\n3333".to_string();
22154 let sample_text_2 = "4444\n5555\n6666".to_string();
22155 let sample_text_3 = "7777\n8888\n9999".to_string();
22156
22157 let fs = FakeFs::new(cx.executor());
22158 fs.insert_tree(
22159 path!("/a"),
22160 json!({
22161 "first.rs": sample_text_1,
22162 "second.rs": sample_text_2,
22163 "third.rs": sample_text_3,
22164 }),
22165 )
22166 .await;
22167 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22168 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22169 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22170 let worktree = project.update(cx, |project, cx| {
22171 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22172 assert_eq!(worktrees.len(), 1);
22173 worktrees.pop().unwrap()
22174 });
22175 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22176
22177 let buffer_1 = project
22178 .update(cx, |project, cx| {
22179 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22180 })
22181 .await
22182 .unwrap();
22183 let buffer_2 = project
22184 .update(cx, |project, cx| {
22185 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22186 })
22187 .await
22188 .unwrap();
22189 let buffer_3 = project
22190 .update(cx, |project, cx| {
22191 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22192 })
22193 .await
22194 .unwrap();
22195
22196 let multi_buffer = cx.new(|cx| {
22197 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22198 multi_buffer.push_excerpts(
22199 buffer_1.clone(),
22200 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22201 cx,
22202 );
22203 multi_buffer.push_excerpts(
22204 buffer_2.clone(),
22205 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22206 cx,
22207 );
22208 multi_buffer.push_excerpts(
22209 buffer_3.clone(),
22210 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22211 cx,
22212 );
22213 multi_buffer
22214 });
22215
22216 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22217 Editor::new(
22218 EditorMode::full(),
22219 multi_buffer,
22220 Some(project.clone()),
22221 window,
22222 cx,
22223 )
22224 });
22225
22226 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22227 assert_eq!(
22228 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22229 full_text,
22230 );
22231
22232 multi_buffer_editor.update(cx, |editor, cx| {
22233 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22234 });
22235 assert_eq!(
22236 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22237 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22238 "After folding the first buffer, its text should not be displayed"
22239 );
22240
22241 multi_buffer_editor.update(cx, |editor, cx| {
22242 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22243 });
22244
22245 assert_eq!(
22246 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22247 "\n\n\n\n\n\n7777\n8888\n9999",
22248 "After folding the second buffer, its text should not be displayed"
22249 );
22250
22251 multi_buffer_editor.update(cx, |editor, cx| {
22252 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22253 });
22254 assert_eq!(
22255 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22256 "\n\n\n\n\n",
22257 "After folding the third buffer, its text should not be displayed"
22258 );
22259
22260 multi_buffer_editor.update(cx, |editor, cx| {
22261 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22262 });
22263 assert_eq!(
22264 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22265 "\n\n\n\n4444\n5555\n6666\n\n",
22266 "After unfolding the second buffer, its text should be displayed"
22267 );
22268
22269 multi_buffer_editor.update(cx, |editor, cx| {
22270 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22271 });
22272 assert_eq!(
22273 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22274 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22275 "After unfolding the first buffer, its text should be displayed"
22276 );
22277
22278 multi_buffer_editor.update(cx, |editor, cx| {
22279 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22280 });
22281 assert_eq!(
22282 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22283 full_text,
22284 "After unfolding all buffers, all original text should be displayed"
22285 );
22286}
22287
22288#[gpui::test]
22289async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22290 init_test(cx, |_| {});
22291
22292 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22293
22294 let fs = FakeFs::new(cx.executor());
22295 fs.insert_tree(
22296 path!("/a"),
22297 json!({
22298 "main.rs": sample_text,
22299 }),
22300 )
22301 .await;
22302 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22303 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22304 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22305 let worktree = project.update(cx, |project, cx| {
22306 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22307 assert_eq!(worktrees.len(), 1);
22308 worktrees.pop().unwrap()
22309 });
22310 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22311
22312 let buffer_1 = project
22313 .update(cx, |project, cx| {
22314 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22315 })
22316 .await
22317 .unwrap();
22318
22319 let multi_buffer = cx.new(|cx| {
22320 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22321 multi_buffer.push_excerpts(
22322 buffer_1.clone(),
22323 [ExcerptRange::new(
22324 Point::new(0, 0)
22325 ..Point::new(
22326 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22327 0,
22328 ),
22329 )],
22330 cx,
22331 );
22332 multi_buffer
22333 });
22334 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22335 Editor::new(
22336 EditorMode::full(),
22337 multi_buffer,
22338 Some(project.clone()),
22339 window,
22340 cx,
22341 )
22342 });
22343
22344 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22345 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22346 enum TestHighlight {}
22347 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22348 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22349 editor.highlight_text::<TestHighlight>(
22350 vec![highlight_range.clone()],
22351 HighlightStyle::color(Hsla::green()),
22352 cx,
22353 );
22354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22355 s.select_ranges(Some(highlight_range))
22356 });
22357 });
22358
22359 let full_text = format!("\n\n{sample_text}");
22360 assert_eq!(
22361 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22362 full_text,
22363 );
22364}
22365
22366#[gpui::test]
22367async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22368 init_test(cx, |_| {});
22369 cx.update(|cx| {
22370 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22371 "keymaps/default-linux.json",
22372 cx,
22373 )
22374 .unwrap();
22375 cx.bind_keys(default_key_bindings);
22376 });
22377
22378 let (editor, cx) = cx.add_window_view(|window, cx| {
22379 let multi_buffer = MultiBuffer::build_multi(
22380 [
22381 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22382 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22383 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22384 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22385 ],
22386 cx,
22387 );
22388 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22389
22390 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22391 // fold all but the second buffer, so that we test navigating between two
22392 // adjacent folded buffers, as well as folded buffers at the start and
22393 // end the multibuffer
22394 editor.fold_buffer(buffer_ids[0], cx);
22395 editor.fold_buffer(buffer_ids[2], cx);
22396 editor.fold_buffer(buffer_ids[3], cx);
22397
22398 editor
22399 });
22400 cx.simulate_resize(size(px(1000.), px(1000.)));
22401
22402 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22403 cx.assert_excerpts_with_selections(indoc! {"
22404 [EXCERPT]
22405 ˇ[FOLDED]
22406 [EXCERPT]
22407 a1
22408 b1
22409 [EXCERPT]
22410 [FOLDED]
22411 [EXCERPT]
22412 [FOLDED]
22413 "
22414 });
22415 cx.simulate_keystroke("down");
22416 cx.assert_excerpts_with_selections(indoc! {"
22417 [EXCERPT]
22418 [FOLDED]
22419 [EXCERPT]
22420 ˇa1
22421 b1
22422 [EXCERPT]
22423 [FOLDED]
22424 [EXCERPT]
22425 [FOLDED]
22426 "
22427 });
22428 cx.simulate_keystroke("down");
22429 cx.assert_excerpts_with_selections(indoc! {"
22430 [EXCERPT]
22431 [FOLDED]
22432 [EXCERPT]
22433 a1
22434 ˇb1
22435 [EXCERPT]
22436 [FOLDED]
22437 [EXCERPT]
22438 [FOLDED]
22439 "
22440 });
22441 cx.simulate_keystroke("down");
22442 cx.assert_excerpts_with_selections(indoc! {"
22443 [EXCERPT]
22444 [FOLDED]
22445 [EXCERPT]
22446 a1
22447 b1
22448 ˇ[EXCERPT]
22449 [FOLDED]
22450 [EXCERPT]
22451 [FOLDED]
22452 "
22453 });
22454 cx.simulate_keystroke("down");
22455 cx.assert_excerpts_with_selections(indoc! {"
22456 [EXCERPT]
22457 [FOLDED]
22458 [EXCERPT]
22459 a1
22460 b1
22461 [EXCERPT]
22462 ˇ[FOLDED]
22463 [EXCERPT]
22464 [FOLDED]
22465 "
22466 });
22467 for _ in 0..5 {
22468 cx.simulate_keystroke("down");
22469 cx.assert_excerpts_with_selections(indoc! {"
22470 [EXCERPT]
22471 [FOLDED]
22472 [EXCERPT]
22473 a1
22474 b1
22475 [EXCERPT]
22476 [FOLDED]
22477 [EXCERPT]
22478 ˇ[FOLDED]
22479 "
22480 });
22481 }
22482
22483 cx.simulate_keystroke("up");
22484 cx.assert_excerpts_with_selections(indoc! {"
22485 [EXCERPT]
22486 [FOLDED]
22487 [EXCERPT]
22488 a1
22489 b1
22490 [EXCERPT]
22491 ˇ[FOLDED]
22492 [EXCERPT]
22493 [FOLDED]
22494 "
22495 });
22496 cx.simulate_keystroke("up");
22497 cx.assert_excerpts_with_selections(indoc! {"
22498 [EXCERPT]
22499 [FOLDED]
22500 [EXCERPT]
22501 a1
22502 b1
22503 ˇ[EXCERPT]
22504 [FOLDED]
22505 [EXCERPT]
22506 [FOLDED]
22507 "
22508 });
22509 cx.simulate_keystroke("up");
22510 cx.assert_excerpts_with_selections(indoc! {"
22511 [EXCERPT]
22512 [FOLDED]
22513 [EXCERPT]
22514 a1
22515 ˇb1
22516 [EXCERPT]
22517 [FOLDED]
22518 [EXCERPT]
22519 [FOLDED]
22520 "
22521 });
22522 cx.simulate_keystroke("up");
22523 cx.assert_excerpts_with_selections(indoc! {"
22524 [EXCERPT]
22525 [FOLDED]
22526 [EXCERPT]
22527 ˇa1
22528 b1
22529 [EXCERPT]
22530 [FOLDED]
22531 [EXCERPT]
22532 [FOLDED]
22533 "
22534 });
22535 for _ in 0..5 {
22536 cx.simulate_keystroke("up");
22537 cx.assert_excerpts_with_selections(indoc! {"
22538 [EXCERPT]
22539 ˇ[FOLDED]
22540 [EXCERPT]
22541 a1
22542 b1
22543 [EXCERPT]
22544 [FOLDED]
22545 [EXCERPT]
22546 [FOLDED]
22547 "
22548 });
22549 }
22550}
22551
22552#[gpui::test]
22553async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22554 init_test(cx, |_| {});
22555
22556 // Simple insertion
22557 assert_highlighted_edits(
22558 "Hello, world!",
22559 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22560 true,
22561 cx,
22562 |highlighted_edits, cx| {
22563 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22564 assert_eq!(highlighted_edits.highlights.len(), 1);
22565 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22566 assert_eq!(
22567 highlighted_edits.highlights[0].1.background_color,
22568 Some(cx.theme().status().created_background)
22569 );
22570 },
22571 )
22572 .await;
22573
22574 // Replacement
22575 assert_highlighted_edits(
22576 "This is a test.",
22577 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22578 false,
22579 cx,
22580 |highlighted_edits, cx| {
22581 assert_eq!(highlighted_edits.text, "That is a test.");
22582 assert_eq!(highlighted_edits.highlights.len(), 1);
22583 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22584 assert_eq!(
22585 highlighted_edits.highlights[0].1.background_color,
22586 Some(cx.theme().status().created_background)
22587 );
22588 },
22589 )
22590 .await;
22591
22592 // Multiple edits
22593 assert_highlighted_edits(
22594 "Hello, world!",
22595 vec![
22596 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22597 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22598 ],
22599 false,
22600 cx,
22601 |highlighted_edits, cx| {
22602 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22603 assert_eq!(highlighted_edits.highlights.len(), 2);
22604 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22605 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22606 assert_eq!(
22607 highlighted_edits.highlights[0].1.background_color,
22608 Some(cx.theme().status().created_background)
22609 );
22610 assert_eq!(
22611 highlighted_edits.highlights[1].1.background_color,
22612 Some(cx.theme().status().created_background)
22613 );
22614 },
22615 )
22616 .await;
22617
22618 // Multiple lines with edits
22619 assert_highlighted_edits(
22620 "First line\nSecond line\nThird line\nFourth line",
22621 vec![
22622 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22623 (
22624 Point::new(2, 0)..Point::new(2, 10),
22625 "New third line".to_string(),
22626 ),
22627 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22628 ],
22629 false,
22630 cx,
22631 |highlighted_edits, cx| {
22632 assert_eq!(
22633 highlighted_edits.text,
22634 "Second modified\nNew third line\nFourth updated line"
22635 );
22636 assert_eq!(highlighted_edits.highlights.len(), 3);
22637 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22638 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22639 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22640 for highlight in &highlighted_edits.highlights {
22641 assert_eq!(
22642 highlight.1.background_color,
22643 Some(cx.theme().status().created_background)
22644 );
22645 }
22646 },
22647 )
22648 .await;
22649}
22650
22651#[gpui::test]
22652async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22653 init_test(cx, |_| {});
22654
22655 // Deletion
22656 assert_highlighted_edits(
22657 "Hello, world!",
22658 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22659 true,
22660 cx,
22661 |highlighted_edits, cx| {
22662 assert_eq!(highlighted_edits.text, "Hello, world!");
22663 assert_eq!(highlighted_edits.highlights.len(), 1);
22664 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22665 assert_eq!(
22666 highlighted_edits.highlights[0].1.background_color,
22667 Some(cx.theme().status().deleted_background)
22668 );
22669 },
22670 )
22671 .await;
22672
22673 // Insertion
22674 assert_highlighted_edits(
22675 "Hello, world!",
22676 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22677 true,
22678 cx,
22679 |highlighted_edits, cx| {
22680 assert_eq!(highlighted_edits.highlights.len(), 1);
22681 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22682 assert_eq!(
22683 highlighted_edits.highlights[0].1.background_color,
22684 Some(cx.theme().status().created_background)
22685 );
22686 },
22687 )
22688 .await;
22689}
22690
22691async fn assert_highlighted_edits(
22692 text: &str,
22693 edits: Vec<(Range<Point>, String)>,
22694 include_deletions: bool,
22695 cx: &mut TestAppContext,
22696 assertion_fn: impl Fn(HighlightedText, &App),
22697) {
22698 let window = cx.add_window(|window, cx| {
22699 let buffer = MultiBuffer::build_simple(text, cx);
22700 Editor::new(EditorMode::full(), buffer, None, window, cx)
22701 });
22702 let cx = &mut VisualTestContext::from_window(*window, cx);
22703
22704 let (buffer, snapshot) = window
22705 .update(cx, |editor, _window, cx| {
22706 (
22707 editor.buffer().clone(),
22708 editor.buffer().read(cx).snapshot(cx),
22709 )
22710 })
22711 .unwrap();
22712
22713 let edits = edits
22714 .into_iter()
22715 .map(|(range, edit)| {
22716 (
22717 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22718 edit,
22719 )
22720 })
22721 .collect::<Vec<_>>();
22722
22723 let text_anchor_edits = edits
22724 .clone()
22725 .into_iter()
22726 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22727 .collect::<Vec<_>>();
22728
22729 let edit_preview = window
22730 .update(cx, |_, _window, cx| {
22731 buffer
22732 .read(cx)
22733 .as_singleton()
22734 .unwrap()
22735 .read(cx)
22736 .preview_edits(text_anchor_edits.into(), cx)
22737 })
22738 .unwrap()
22739 .await;
22740
22741 cx.update(|_window, cx| {
22742 let highlighted_edits = edit_prediction_edit_text(
22743 snapshot.as_singleton().unwrap().2,
22744 &edits,
22745 &edit_preview,
22746 include_deletions,
22747 cx,
22748 );
22749 assertion_fn(highlighted_edits, cx)
22750 });
22751}
22752
22753#[track_caller]
22754fn assert_breakpoint(
22755 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22756 path: &Arc<Path>,
22757 expected: Vec<(u32, Breakpoint)>,
22758) {
22759 if expected.is_empty() {
22760 assert!(!breakpoints.contains_key(path), "{}", path.display());
22761 } else {
22762 let mut breakpoint = breakpoints
22763 .get(path)
22764 .unwrap()
22765 .iter()
22766 .map(|breakpoint| {
22767 (
22768 breakpoint.row,
22769 Breakpoint {
22770 message: breakpoint.message.clone(),
22771 state: breakpoint.state,
22772 condition: breakpoint.condition.clone(),
22773 hit_condition: breakpoint.hit_condition.clone(),
22774 },
22775 )
22776 })
22777 .collect::<Vec<_>>();
22778
22779 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22780
22781 assert_eq!(expected, breakpoint);
22782 }
22783}
22784
22785fn add_log_breakpoint_at_cursor(
22786 editor: &mut Editor,
22787 log_message: &str,
22788 window: &mut Window,
22789 cx: &mut Context<Editor>,
22790) {
22791 let (anchor, bp) = editor
22792 .breakpoints_at_cursors(window, cx)
22793 .first()
22794 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22795 .unwrap_or_else(|| {
22796 let snapshot = editor.snapshot(window, cx);
22797 let cursor_position: Point =
22798 editor.selections.newest(&snapshot.display_snapshot).head();
22799
22800 let breakpoint_position = snapshot
22801 .buffer_snapshot()
22802 .anchor_before(Point::new(cursor_position.row, 0));
22803
22804 (breakpoint_position, Breakpoint::new_log(log_message))
22805 });
22806
22807 editor.edit_breakpoint_at_anchor(
22808 anchor,
22809 bp,
22810 BreakpointEditAction::EditLogMessage(log_message.into()),
22811 cx,
22812 );
22813}
22814
22815#[gpui::test]
22816async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22817 init_test(cx, |_| {});
22818
22819 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22820 let fs = FakeFs::new(cx.executor());
22821 fs.insert_tree(
22822 path!("/a"),
22823 json!({
22824 "main.rs": sample_text,
22825 }),
22826 )
22827 .await;
22828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22831
22832 let fs = FakeFs::new(cx.executor());
22833 fs.insert_tree(
22834 path!("/a"),
22835 json!({
22836 "main.rs": sample_text,
22837 }),
22838 )
22839 .await;
22840 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22841 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22842 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22843 let worktree_id = workspace
22844 .update(cx, |workspace, _window, cx| {
22845 workspace.project().update(cx, |project, cx| {
22846 project.worktrees(cx).next().unwrap().read(cx).id()
22847 })
22848 })
22849 .unwrap();
22850
22851 let buffer = project
22852 .update(cx, |project, cx| {
22853 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22854 })
22855 .await
22856 .unwrap();
22857
22858 let (editor, cx) = cx.add_window_view(|window, cx| {
22859 Editor::new(
22860 EditorMode::full(),
22861 MultiBuffer::build_from_buffer(buffer, cx),
22862 Some(project.clone()),
22863 window,
22864 cx,
22865 )
22866 });
22867
22868 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22869 let abs_path = project.read_with(cx, |project, cx| {
22870 project
22871 .absolute_path(&project_path, cx)
22872 .map(Arc::from)
22873 .unwrap()
22874 });
22875
22876 // assert we can add breakpoint on the first line
22877 editor.update_in(cx, |editor, window, cx| {
22878 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22879 editor.move_to_end(&MoveToEnd, window, cx);
22880 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22881 });
22882
22883 let breakpoints = editor.update(cx, |editor, cx| {
22884 editor
22885 .breakpoint_store()
22886 .as_ref()
22887 .unwrap()
22888 .read(cx)
22889 .all_source_breakpoints(cx)
22890 });
22891
22892 assert_eq!(1, breakpoints.len());
22893 assert_breakpoint(
22894 &breakpoints,
22895 &abs_path,
22896 vec![
22897 (0, Breakpoint::new_standard()),
22898 (3, Breakpoint::new_standard()),
22899 ],
22900 );
22901
22902 editor.update_in(cx, |editor, window, cx| {
22903 editor.move_to_beginning(&MoveToBeginning, window, cx);
22904 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22905 });
22906
22907 let breakpoints = editor.update(cx, |editor, cx| {
22908 editor
22909 .breakpoint_store()
22910 .as_ref()
22911 .unwrap()
22912 .read(cx)
22913 .all_source_breakpoints(cx)
22914 });
22915
22916 assert_eq!(1, breakpoints.len());
22917 assert_breakpoint(
22918 &breakpoints,
22919 &abs_path,
22920 vec![(3, Breakpoint::new_standard())],
22921 );
22922
22923 editor.update_in(cx, |editor, window, cx| {
22924 editor.move_to_end(&MoveToEnd, window, cx);
22925 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22926 });
22927
22928 let breakpoints = editor.update(cx, |editor, cx| {
22929 editor
22930 .breakpoint_store()
22931 .as_ref()
22932 .unwrap()
22933 .read(cx)
22934 .all_source_breakpoints(cx)
22935 });
22936
22937 assert_eq!(0, breakpoints.len());
22938 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22939}
22940
22941#[gpui::test]
22942async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22943 init_test(cx, |_| {});
22944
22945 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22946
22947 let fs = FakeFs::new(cx.executor());
22948 fs.insert_tree(
22949 path!("/a"),
22950 json!({
22951 "main.rs": sample_text,
22952 }),
22953 )
22954 .await;
22955 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22956 let (workspace, cx) =
22957 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22958
22959 let worktree_id = workspace.update(cx, |workspace, cx| {
22960 workspace.project().update(cx, |project, cx| {
22961 project.worktrees(cx).next().unwrap().read(cx).id()
22962 })
22963 });
22964
22965 let buffer = project
22966 .update(cx, |project, cx| {
22967 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22968 })
22969 .await
22970 .unwrap();
22971
22972 let (editor, cx) = cx.add_window_view(|window, cx| {
22973 Editor::new(
22974 EditorMode::full(),
22975 MultiBuffer::build_from_buffer(buffer, cx),
22976 Some(project.clone()),
22977 window,
22978 cx,
22979 )
22980 });
22981
22982 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22983 let abs_path = project.read_with(cx, |project, cx| {
22984 project
22985 .absolute_path(&project_path, cx)
22986 .map(Arc::from)
22987 .unwrap()
22988 });
22989
22990 editor.update_in(cx, |editor, window, cx| {
22991 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22992 });
22993
22994 let breakpoints = editor.update(cx, |editor, cx| {
22995 editor
22996 .breakpoint_store()
22997 .as_ref()
22998 .unwrap()
22999 .read(cx)
23000 .all_source_breakpoints(cx)
23001 });
23002
23003 assert_breakpoint(
23004 &breakpoints,
23005 &abs_path,
23006 vec![(0, Breakpoint::new_log("hello world"))],
23007 );
23008
23009 // Removing a log message from a log breakpoint should remove it
23010 editor.update_in(cx, |editor, window, cx| {
23011 add_log_breakpoint_at_cursor(editor, "", window, cx);
23012 });
23013
23014 let breakpoints = editor.update(cx, |editor, cx| {
23015 editor
23016 .breakpoint_store()
23017 .as_ref()
23018 .unwrap()
23019 .read(cx)
23020 .all_source_breakpoints(cx)
23021 });
23022
23023 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23024
23025 editor.update_in(cx, |editor, window, cx| {
23026 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23027 editor.move_to_end(&MoveToEnd, window, cx);
23028 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23029 // Not adding a log message to a standard breakpoint shouldn't remove it
23030 add_log_breakpoint_at_cursor(editor, "", window, cx);
23031 });
23032
23033 let breakpoints = editor.update(cx, |editor, cx| {
23034 editor
23035 .breakpoint_store()
23036 .as_ref()
23037 .unwrap()
23038 .read(cx)
23039 .all_source_breakpoints(cx)
23040 });
23041
23042 assert_breakpoint(
23043 &breakpoints,
23044 &abs_path,
23045 vec![
23046 (0, Breakpoint::new_standard()),
23047 (3, Breakpoint::new_standard()),
23048 ],
23049 );
23050
23051 editor.update_in(cx, |editor, window, cx| {
23052 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23053 });
23054
23055 let breakpoints = editor.update(cx, |editor, cx| {
23056 editor
23057 .breakpoint_store()
23058 .as_ref()
23059 .unwrap()
23060 .read(cx)
23061 .all_source_breakpoints(cx)
23062 });
23063
23064 assert_breakpoint(
23065 &breakpoints,
23066 &abs_path,
23067 vec![
23068 (0, Breakpoint::new_standard()),
23069 (3, Breakpoint::new_log("hello world")),
23070 ],
23071 );
23072
23073 editor.update_in(cx, |editor, window, cx| {
23074 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23075 });
23076
23077 let breakpoints = editor.update(cx, |editor, cx| {
23078 editor
23079 .breakpoint_store()
23080 .as_ref()
23081 .unwrap()
23082 .read(cx)
23083 .all_source_breakpoints(cx)
23084 });
23085
23086 assert_breakpoint(
23087 &breakpoints,
23088 &abs_path,
23089 vec![
23090 (0, Breakpoint::new_standard()),
23091 (3, Breakpoint::new_log("hello Earth!!")),
23092 ],
23093 );
23094}
23095
23096/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23097/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23098/// or when breakpoints were placed out of order. This tests for a regression too
23099#[gpui::test]
23100async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23101 init_test(cx, |_| {});
23102
23103 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23104 let fs = FakeFs::new(cx.executor());
23105 fs.insert_tree(
23106 path!("/a"),
23107 json!({
23108 "main.rs": sample_text,
23109 }),
23110 )
23111 .await;
23112 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23113 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23114 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23115
23116 let fs = FakeFs::new(cx.executor());
23117 fs.insert_tree(
23118 path!("/a"),
23119 json!({
23120 "main.rs": sample_text,
23121 }),
23122 )
23123 .await;
23124 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23125 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23126 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23127 let worktree_id = workspace
23128 .update(cx, |workspace, _window, cx| {
23129 workspace.project().update(cx, |project, cx| {
23130 project.worktrees(cx).next().unwrap().read(cx).id()
23131 })
23132 })
23133 .unwrap();
23134
23135 let buffer = project
23136 .update(cx, |project, cx| {
23137 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23138 })
23139 .await
23140 .unwrap();
23141
23142 let (editor, cx) = cx.add_window_view(|window, cx| {
23143 Editor::new(
23144 EditorMode::full(),
23145 MultiBuffer::build_from_buffer(buffer, cx),
23146 Some(project.clone()),
23147 window,
23148 cx,
23149 )
23150 });
23151
23152 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23153 let abs_path = project.read_with(cx, |project, cx| {
23154 project
23155 .absolute_path(&project_path, cx)
23156 .map(Arc::from)
23157 .unwrap()
23158 });
23159
23160 // assert we can add breakpoint on the first line
23161 editor.update_in(cx, |editor, window, cx| {
23162 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23163 editor.move_to_end(&MoveToEnd, window, cx);
23164 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23165 editor.move_up(&MoveUp, window, cx);
23166 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23167 });
23168
23169 let breakpoints = editor.update(cx, |editor, cx| {
23170 editor
23171 .breakpoint_store()
23172 .as_ref()
23173 .unwrap()
23174 .read(cx)
23175 .all_source_breakpoints(cx)
23176 });
23177
23178 assert_eq!(1, breakpoints.len());
23179 assert_breakpoint(
23180 &breakpoints,
23181 &abs_path,
23182 vec![
23183 (0, Breakpoint::new_standard()),
23184 (2, Breakpoint::new_standard()),
23185 (3, Breakpoint::new_standard()),
23186 ],
23187 );
23188
23189 editor.update_in(cx, |editor, window, cx| {
23190 editor.move_to_beginning(&MoveToBeginning, window, cx);
23191 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23192 editor.move_to_end(&MoveToEnd, window, cx);
23193 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23194 // Disabling a breakpoint that doesn't exist should do nothing
23195 editor.move_up(&MoveUp, window, cx);
23196 editor.move_up(&MoveUp, window, cx);
23197 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23198 });
23199
23200 let breakpoints = editor.update(cx, |editor, cx| {
23201 editor
23202 .breakpoint_store()
23203 .as_ref()
23204 .unwrap()
23205 .read(cx)
23206 .all_source_breakpoints(cx)
23207 });
23208
23209 let disable_breakpoint = {
23210 let mut bp = Breakpoint::new_standard();
23211 bp.state = BreakpointState::Disabled;
23212 bp
23213 };
23214
23215 assert_eq!(1, breakpoints.len());
23216 assert_breakpoint(
23217 &breakpoints,
23218 &abs_path,
23219 vec![
23220 (0, disable_breakpoint.clone()),
23221 (2, Breakpoint::new_standard()),
23222 (3, disable_breakpoint.clone()),
23223 ],
23224 );
23225
23226 editor.update_in(cx, |editor, window, cx| {
23227 editor.move_to_beginning(&MoveToBeginning, window, cx);
23228 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23229 editor.move_to_end(&MoveToEnd, window, cx);
23230 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23231 editor.move_up(&MoveUp, window, cx);
23232 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23233 });
23234
23235 let breakpoints = editor.update(cx, |editor, cx| {
23236 editor
23237 .breakpoint_store()
23238 .as_ref()
23239 .unwrap()
23240 .read(cx)
23241 .all_source_breakpoints(cx)
23242 });
23243
23244 assert_eq!(1, breakpoints.len());
23245 assert_breakpoint(
23246 &breakpoints,
23247 &abs_path,
23248 vec![
23249 (0, Breakpoint::new_standard()),
23250 (2, disable_breakpoint),
23251 (3, Breakpoint::new_standard()),
23252 ],
23253 );
23254}
23255
23256#[gpui::test]
23257async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23258 init_test(cx, |_| {});
23259 let capabilities = lsp::ServerCapabilities {
23260 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23261 prepare_provider: Some(true),
23262 work_done_progress_options: Default::default(),
23263 })),
23264 ..Default::default()
23265 };
23266 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23267
23268 cx.set_state(indoc! {"
23269 struct Fˇoo {}
23270 "});
23271
23272 cx.update_editor(|editor, _, cx| {
23273 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23274 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23275 editor.highlight_background::<DocumentHighlightRead>(
23276 &[highlight_range],
23277 |theme| theme.colors().editor_document_highlight_read_background,
23278 cx,
23279 );
23280 });
23281
23282 let mut prepare_rename_handler = cx
23283 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23284 move |_, _, _| async move {
23285 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23286 start: lsp::Position {
23287 line: 0,
23288 character: 7,
23289 },
23290 end: lsp::Position {
23291 line: 0,
23292 character: 10,
23293 },
23294 })))
23295 },
23296 );
23297 let prepare_rename_task = cx
23298 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23299 .expect("Prepare rename was not started");
23300 prepare_rename_handler.next().await.unwrap();
23301 prepare_rename_task.await.expect("Prepare rename failed");
23302
23303 let mut rename_handler =
23304 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23305 let edit = lsp::TextEdit {
23306 range: lsp::Range {
23307 start: lsp::Position {
23308 line: 0,
23309 character: 7,
23310 },
23311 end: lsp::Position {
23312 line: 0,
23313 character: 10,
23314 },
23315 },
23316 new_text: "FooRenamed".to_string(),
23317 };
23318 Ok(Some(lsp::WorkspaceEdit::new(
23319 // Specify the same edit twice
23320 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23321 )))
23322 });
23323 let rename_task = cx
23324 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23325 .expect("Confirm rename was not started");
23326 rename_handler.next().await.unwrap();
23327 rename_task.await.expect("Confirm rename failed");
23328 cx.run_until_parked();
23329
23330 // Despite two edits, only one is actually applied as those are identical
23331 cx.assert_editor_state(indoc! {"
23332 struct FooRenamedˇ {}
23333 "});
23334}
23335
23336#[gpui::test]
23337async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23338 init_test(cx, |_| {});
23339 // These capabilities indicate that the server does not support prepare rename.
23340 let capabilities = lsp::ServerCapabilities {
23341 rename_provider: Some(lsp::OneOf::Left(true)),
23342 ..Default::default()
23343 };
23344 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23345
23346 cx.set_state(indoc! {"
23347 struct Fˇoo {}
23348 "});
23349
23350 cx.update_editor(|editor, _window, cx| {
23351 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23352 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23353 editor.highlight_background::<DocumentHighlightRead>(
23354 &[highlight_range],
23355 |theme| theme.colors().editor_document_highlight_read_background,
23356 cx,
23357 );
23358 });
23359
23360 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23361 .expect("Prepare rename was not started")
23362 .await
23363 .expect("Prepare rename failed");
23364
23365 let mut rename_handler =
23366 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23367 let edit = lsp::TextEdit {
23368 range: lsp::Range {
23369 start: lsp::Position {
23370 line: 0,
23371 character: 7,
23372 },
23373 end: lsp::Position {
23374 line: 0,
23375 character: 10,
23376 },
23377 },
23378 new_text: "FooRenamed".to_string(),
23379 };
23380 Ok(Some(lsp::WorkspaceEdit::new(
23381 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23382 )))
23383 });
23384 let rename_task = cx
23385 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23386 .expect("Confirm rename was not started");
23387 rename_handler.next().await.unwrap();
23388 rename_task.await.expect("Confirm rename failed");
23389 cx.run_until_parked();
23390
23391 // Correct range is renamed, as `surrounding_word` is used to find it.
23392 cx.assert_editor_state(indoc! {"
23393 struct FooRenamedˇ {}
23394 "});
23395}
23396
23397#[gpui::test]
23398async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23399 init_test(cx, |_| {});
23400 let mut cx = EditorTestContext::new(cx).await;
23401
23402 let language = Arc::new(
23403 Language::new(
23404 LanguageConfig::default(),
23405 Some(tree_sitter_html::LANGUAGE.into()),
23406 )
23407 .with_brackets_query(
23408 r#"
23409 ("<" @open "/>" @close)
23410 ("</" @open ">" @close)
23411 ("<" @open ">" @close)
23412 ("\"" @open "\"" @close)
23413 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23414 "#,
23415 )
23416 .unwrap(),
23417 );
23418 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23419
23420 cx.set_state(indoc! {"
23421 <span>ˇ</span>
23422 "});
23423 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23424 cx.assert_editor_state(indoc! {"
23425 <span>
23426 ˇ
23427 </span>
23428 "});
23429
23430 cx.set_state(indoc! {"
23431 <span><span></span>ˇ</span>
23432 "});
23433 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23434 cx.assert_editor_state(indoc! {"
23435 <span><span></span>
23436 ˇ</span>
23437 "});
23438
23439 cx.set_state(indoc! {"
23440 <span>ˇ
23441 </span>
23442 "});
23443 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23444 cx.assert_editor_state(indoc! {"
23445 <span>
23446 ˇ
23447 </span>
23448 "});
23449}
23450
23451#[gpui::test(iterations = 10)]
23452async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23453 init_test(cx, |_| {});
23454
23455 let fs = FakeFs::new(cx.executor());
23456 fs.insert_tree(
23457 path!("/dir"),
23458 json!({
23459 "a.ts": "a",
23460 }),
23461 )
23462 .await;
23463
23464 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23465 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23466 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23467
23468 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23469 language_registry.add(Arc::new(Language::new(
23470 LanguageConfig {
23471 name: "TypeScript".into(),
23472 matcher: LanguageMatcher {
23473 path_suffixes: vec!["ts".to_string()],
23474 ..Default::default()
23475 },
23476 ..Default::default()
23477 },
23478 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23479 )));
23480 let mut fake_language_servers = language_registry.register_fake_lsp(
23481 "TypeScript",
23482 FakeLspAdapter {
23483 capabilities: lsp::ServerCapabilities {
23484 code_lens_provider: Some(lsp::CodeLensOptions {
23485 resolve_provider: Some(true),
23486 }),
23487 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23488 commands: vec!["_the/command".to_string()],
23489 ..lsp::ExecuteCommandOptions::default()
23490 }),
23491 ..lsp::ServerCapabilities::default()
23492 },
23493 ..FakeLspAdapter::default()
23494 },
23495 );
23496
23497 let editor = workspace
23498 .update(cx, |workspace, window, cx| {
23499 workspace.open_abs_path(
23500 PathBuf::from(path!("/dir/a.ts")),
23501 OpenOptions::default(),
23502 window,
23503 cx,
23504 )
23505 })
23506 .unwrap()
23507 .await
23508 .unwrap()
23509 .downcast::<Editor>()
23510 .unwrap();
23511 cx.executor().run_until_parked();
23512
23513 let fake_server = fake_language_servers.next().await.unwrap();
23514
23515 let buffer = editor.update(cx, |editor, cx| {
23516 editor
23517 .buffer()
23518 .read(cx)
23519 .as_singleton()
23520 .expect("have opened a single file by path")
23521 });
23522
23523 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23524 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23525 drop(buffer_snapshot);
23526 let actions = cx
23527 .update_window(*workspace, |_, window, cx| {
23528 project.code_actions(&buffer, anchor..anchor, window, cx)
23529 })
23530 .unwrap();
23531
23532 fake_server
23533 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23534 Ok(Some(vec![
23535 lsp::CodeLens {
23536 range: lsp::Range::default(),
23537 command: Some(lsp::Command {
23538 title: "Code lens command".to_owned(),
23539 command: "_the/command".to_owned(),
23540 arguments: None,
23541 }),
23542 data: None,
23543 },
23544 lsp::CodeLens {
23545 range: lsp::Range::default(),
23546 command: Some(lsp::Command {
23547 title: "Command not in capabilities".to_owned(),
23548 command: "not in capabilities".to_owned(),
23549 arguments: None,
23550 }),
23551 data: None,
23552 },
23553 lsp::CodeLens {
23554 range: lsp::Range {
23555 start: lsp::Position {
23556 line: 1,
23557 character: 1,
23558 },
23559 end: lsp::Position {
23560 line: 1,
23561 character: 1,
23562 },
23563 },
23564 command: Some(lsp::Command {
23565 title: "Command not in range".to_owned(),
23566 command: "_the/command".to_owned(),
23567 arguments: None,
23568 }),
23569 data: None,
23570 },
23571 ]))
23572 })
23573 .next()
23574 .await;
23575
23576 let actions = actions.await.unwrap();
23577 assert_eq!(
23578 actions.len(),
23579 1,
23580 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23581 );
23582 let action = actions[0].clone();
23583 let apply = project.update(cx, |project, cx| {
23584 project.apply_code_action(buffer.clone(), action, true, cx)
23585 });
23586
23587 // Resolving the code action does not populate its edits. In absence of
23588 // edits, we must execute the given command.
23589 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23590 |mut lens, _| async move {
23591 let lens_command = lens.command.as_mut().expect("should have a command");
23592 assert_eq!(lens_command.title, "Code lens command");
23593 lens_command.arguments = Some(vec![json!("the-argument")]);
23594 Ok(lens)
23595 },
23596 );
23597
23598 // While executing the command, the language server sends the editor
23599 // a `workspaceEdit` request.
23600 fake_server
23601 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23602 let fake = fake_server.clone();
23603 move |params, _| {
23604 assert_eq!(params.command, "_the/command");
23605 let fake = fake.clone();
23606 async move {
23607 fake.server
23608 .request::<lsp::request::ApplyWorkspaceEdit>(
23609 lsp::ApplyWorkspaceEditParams {
23610 label: None,
23611 edit: lsp::WorkspaceEdit {
23612 changes: Some(
23613 [(
23614 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23615 vec![lsp::TextEdit {
23616 range: lsp::Range::new(
23617 lsp::Position::new(0, 0),
23618 lsp::Position::new(0, 0),
23619 ),
23620 new_text: "X".into(),
23621 }],
23622 )]
23623 .into_iter()
23624 .collect(),
23625 ),
23626 ..lsp::WorkspaceEdit::default()
23627 },
23628 },
23629 )
23630 .await
23631 .into_response()
23632 .unwrap();
23633 Ok(Some(json!(null)))
23634 }
23635 }
23636 })
23637 .next()
23638 .await;
23639
23640 // Applying the code lens command returns a project transaction containing the edits
23641 // sent by the language server in its `workspaceEdit` request.
23642 let transaction = apply.await.unwrap();
23643 assert!(transaction.0.contains_key(&buffer));
23644 buffer.update(cx, |buffer, cx| {
23645 assert_eq!(buffer.text(), "Xa");
23646 buffer.undo(cx);
23647 assert_eq!(buffer.text(), "a");
23648 });
23649
23650 let actions_after_edits = cx
23651 .update_window(*workspace, |_, window, cx| {
23652 project.code_actions(&buffer, anchor..anchor, window, cx)
23653 })
23654 .unwrap()
23655 .await
23656 .unwrap();
23657 assert_eq!(
23658 actions, actions_after_edits,
23659 "For the same selection, same code lens actions should be returned"
23660 );
23661
23662 let _responses =
23663 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23664 panic!("No more code lens requests are expected");
23665 });
23666 editor.update_in(cx, |editor, window, cx| {
23667 editor.select_all(&SelectAll, window, cx);
23668 });
23669 cx.executor().run_until_parked();
23670 let new_actions = cx
23671 .update_window(*workspace, |_, window, cx| {
23672 project.code_actions(&buffer, anchor..anchor, window, cx)
23673 })
23674 .unwrap()
23675 .await
23676 .unwrap();
23677 assert_eq!(
23678 actions, new_actions,
23679 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23680 );
23681}
23682
23683#[gpui::test]
23684async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23685 init_test(cx, |_| {});
23686
23687 let fs = FakeFs::new(cx.executor());
23688 let main_text = r#"fn main() {
23689println!("1");
23690println!("2");
23691println!("3");
23692println!("4");
23693println!("5");
23694}"#;
23695 let lib_text = "mod foo {}";
23696 fs.insert_tree(
23697 path!("/a"),
23698 json!({
23699 "lib.rs": lib_text,
23700 "main.rs": main_text,
23701 }),
23702 )
23703 .await;
23704
23705 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23706 let (workspace, cx) =
23707 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23708 let worktree_id = workspace.update(cx, |workspace, cx| {
23709 workspace.project().update(cx, |project, cx| {
23710 project.worktrees(cx).next().unwrap().read(cx).id()
23711 })
23712 });
23713
23714 let expected_ranges = vec![
23715 Point::new(0, 0)..Point::new(0, 0),
23716 Point::new(1, 0)..Point::new(1, 1),
23717 Point::new(2, 0)..Point::new(2, 2),
23718 Point::new(3, 0)..Point::new(3, 3),
23719 ];
23720
23721 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23722 let editor_1 = workspace
23723 .update_in(cx, |workspace, window, cx| {
23724 workspace.open_path(
23725 (worktree_id, rel_path("main.rs")),
23726 Some(pane_1.downgrade()),
23727 true,
23728 window,
23729 cx,
23730 )
23731 })
23732 .unwrap()
23733 .await
23734 .downcast::<Editor>()
23735 .unwrap();
23736 pane_1.update(cx, |pane, cx| {
23737 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23738 open_editor.update(cx, |editor, cx| {
23739 assert_eq!(
23740 editor.display_text(cx),
23741 main_text,
23742 "Original main.rs text on initial open",
23743 );
23744 assert_eq!(
23745 editor
23746 .selections
23747 .all::<Point>(&editor.display_snapshot(cx))
23748 .into_iter()
23749 .map(|s| s.range())
23750 .collect::<Vec<_>>(),
23751 vec![Point::zero()..Point::zero()],
23752 "Default selections on initial open",
23753 );
23754 })
23755 });
23756 editor_1.update_in(cx, |editor, window, cx| {
23757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23758 s.select_ranges(expected_ranges.clone());
23759 });
23760 });
23761
23762 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23763 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23764 });
23765 let editor_2 = workspace
23766 .update_in(cx, |workspace, window, cx| {
23767 workspace.open_path(
23768 (worktree_id, rel_path("main.rs")),
23769 Some(pane_2.downgrade()),
23770 true,
23771 window,
23772 cx,
23773 )
23774 })
23775 .unwrap()
23776 .await
23777 .downcast::<Editor>()
23778 .unwrap();
23779 pane_2.update(cx, |pane, cx| {
23780 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23781 open_editor.update(cx, |editor, cx| {
23782 assert_eq!(
23783 editor.display_text(cx),
23784 main_text,
23785 "Original main.rs text on initial open in another panel",
23786 );
23787 assert_eq!(
23788 editor
23789 .selections
23790 .all::<Point>(&editor.display_snapshot(cx))
23791 .into_iter()
23792 .map(|s| s.range())
23793 .collect::<Vec<_>>(),
23794 vec![Point::zero()..Point::zero()],
23795 "Default selections on initial open in another panel",
23796 );
23797 })
23798 });
23799
23800 editor_2.update_in(cx, |editor, window, cx| {
23801 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23802 });
23803
23804 let _other_editor_1 = workspace
23805 .update_in(cx, |workspace, window, cx| {
23806 workspace.open_path(
23807 (worktree_id, rel_path("lib.rs")),
23808 Some(pane_1.downgrade()),
23809 true,
23810 window,
23811 cx,
23812 )
23813 })
23814 .unwrap()
23815 .await
23816 .downcast::<Editor>()
23817 .unwrap();
23818 pane_1
23819 .update_in(cx, |pane, window, cx| {
23820 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23821 })
23822 .await
23823 .unwrap();
23824 drop(editor_1);
23825 pane_1.update(cx, |pane, cx| {
23826 pane.active_item()
23827 .unwrap()
23828 .downcast::<Editor>()
23829 .unwrap()
23830 .update(cx, |editor, cx| {
23831 assert_eq!(
23832 editor.display_text(cx),
23833 lib_text,
23834 "Other file should be open and active",
23835 );
23836 });
23837 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23838 });
23839
23840 let _other_editor_2 = workspace
23841 .update_in(cx, |workspace, window, cx| {
23842 workspace.open_path(
23843 (worktree_id, rel_path("lib.rs")),
23844 Some(pane_2.downgrade()),
23845 true,
23846 window,
23847 cx,
23848 )
23849 })
23850 .unwrap()
23851 .await
23852 .downcast::<Editor>()
23853 .unwrap();
23854 pane_2
23855 .update_in(cx, |pane, window, cx| {
23856 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23857 })
23858 .await
23859 .unwrap();
23860 drop(editor_2);
23861 pane_2.update(cx, |pane, cx| {
23862 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23863 open_editor.update(cx, |editor, cx| {
23864 assert_eq!(
23865 editor.display_text(cx),
23866 lib_text,
23867 "Other file should be open and active in another panel too",
23868 );
23869 });
23870 assert_eq!(
23871 pane.items().count(),
23872 1,
23873 "No other editors should be open in another pane",
23874 );
23875 });
23876
23877 let _editor_1_reopened = workspace
23878 .update_in(cx, |workspace, window, cx| {
23879 workspace.open_path(
23880 (worktree_id, rel_path("main.rs")),
23881 Some(pane_1.downgrade()),
23882 true,
23883 window,
23884 cx,
23885 )
23886 })
23887 .unwrap()
23888 .await
23889 .downcast::<Editor>()
23890 .unwrap();
23891 let _editor_2_reopened = workspace
23892 .update_in(cx, |workspace, window, cx| {
23893 workspace.open_path(
23894 (worktree_id, rel_path("main.rs")),
23895 Some(pane_2.downgrade()),
23896 true,
23897 window,
23898 cx,
23899 )
23900 })
23901 .unwrap()
23902 .await
23903 .downcast::<Editor>()
23904 .unwrap();
23905 pane_1.update(cx, |pane, cx| {
23906 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23907 open_editor.update(cx, |editor, cx| {
23908 assert_eq!(
23909 editor.display_text(cx),
23910 main_text,
23911 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23912 );
23913 assert_eq!(
23914 editor
23915 .selections
23916 .all::<Point>(&editor.display_snapshot(cx))
23917 .into_iter()
23918 .map(|s| s.range())
23919 .collect::<Vec<_>>(),
23920 expected_ranges,
23921 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23922 );
23923 })
23924 });
23925 pane_2.update(cx, |pane, cx| {
23926 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23927 open_editor.update(cx, |editor, cx| {
23928 assert_eq!(
23929 editor.display_text(cx),
23930 r#"fn main() {
23931⋯rintln!("1");
23932⋯intln!("2");
23933⋯ntln!("3");
23934println!("4");
23935println!("5");
23936}"#,
23937 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23938 );
23939 assert_eq!(
23940 editor
23941 .selections
23942 .all::<Point>(&editor.display_snapshot(cx))
23943 .into_iter()
23944 .map(|s| s.range())
23945 .collect::<Vec<_>>(),
23946 vec![Point::zero()..Point::zero()],
23947 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23948 );
23949 })
23950 });
23951}
23952
23953#[gpui::test]
23954async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23955 init_test(cx, |_| {});
23956
23957 let fs = FakeFs::new(cx.executor());
23958 let main_text = r#"fn main() {
23959println!("1");
23960println!("2");
23961println!("3");
23962println!("4");
23963println!("5");
23964}"#;
23965 let lib_text = "mod foo {}";
23966 fs.insert_tree(
23967 path!("/a"),
23968 json!({
23969 "lib.rs": lib_text,
23970 "main.rs": main_text,
23971 }),
23972 )
23973 .await;
23974
23975 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23976 let (workspace, cx) =
23977 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23978 let worktree_id = workspace.update(cx, |workspace, cx| {
23979 workspace.project().update(cx, |project, cx| {
23980 project.worktrees(cx).next().unwrap().read(cx).id()
23981 })
23982 });
23983
23984 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23985 let editor = workspace
23986 .update_in(cx, |workspace, window, cx| {
23987 workspace.open_path(
23988 (worktree_id, rel_path("main.rs")),
23989 Some(pane.downgrade()),
23990 true,
23991 window,
23992 cx,
23993 )
23994 })
23995 .unwrap()
23996 .await
23997 .downcast::<Editor>()
23998 .unwrap();
23999 pane.update(cx, |pane, cx| {
24000 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24001 open_editor.update(cx, |editor, cx| {
24002 assert_eq!(
24003 editor.display_text(cx),
24004 main_text,
24005 "Original main.rs text on initial open",
24006 );
24007 })
24008 });
24009 editor.update_in(cx, |editor, window, cx| {
24010 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24011 });
24012
24013 cx.update_global(|store: &mut SettingsStore, cx| {
24014 store.update_user_settings(cx, |s| {
24015 s.workspace.restore_on_file_reopen = Some(false);
24016 });
24017 });
24018 editor.update_in(cx, |editor, window, cx| {
24019 editor.fold_ranges(
24020 vec![
24021 Point::new(1, 0)..Point::new(1, 1),
24022 Point::new(2, 0)..Point::new(2, 2),
24023 Point::new(3, 0)..Point::new(3, 3),
24024 ],
24025 false,
24026 window,
24027 cx,
24028 );
24029 });
24030 pane.update_in(cx, |pane, window, cx| {
24031 pane.close_all_items(&CloseAllItems::default(), window, cx)
24032 })
24033 .await
24034 .unwrap();
24035 pane.update(cx, |pane, _| {
24036 assert!(pane.active_item().is_none());
24037 });
24038 cx.update_global(|store: &mut SettingsStore, cx| {
24039 store.update_user_settings(cx, |s| {
24040 s.workspace.restore_on_file_reopen = Some(true);
24041 });
24042 });
24043
24044 let _editor_reopened = workspace
24045 .update_in(cx, |workspace, window, cx| {
24046 workspace.open_path(
24047 (worktree_id, rel_path("main.rs")),
24048 Some(pane.downgrade()),
24049 true,
24050 window,
24051 cx,
24052 )
24053 })
24054 .unwrap()
24055 .await
24056 .downcast::<Editor>()
24057 .unwrap();
24058 pane.update(cx, |pane, cx| {
24059 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24060 open_editor.update(cx, |editor, cx| {
24061 assert_eq!(
24062 editor.display_text(cx),
24063 main_text,
24064 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24065 );
24066 })
24067 });
24068}
24069
24070#[gpui::test]
24071async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24072 struct EmptyModalView {
24073 focus_handle: gpui::FocusHandle,
24074 }
24075 impl EventEmitter<DismissEvent> for EmptyModalView {}
24076 impl Render for EmptyModalView {
24077 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24078 div()
24079 }
24080 }
24081 impl Focusable for EmptyModalView {
24082 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24083 self.focus_handle.clone()
24084 }
24085 }
24086 impl workspace::ModalView for EmptyModalView {}
24087 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24088 EmptyModalView {
24089 focus_handle: cx.focus_handle(),
24090 }
24091 }
24092
24093 init_test(cx, |_| {});
24094
24095 let fs = FakeFs::new(cx.executor());
24096 let project = Project::test(fs, [], cx).await;
24097 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24098 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24099 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24100 let editor = cx.new_window_entity(|window, cx| {
24101 Editor::new(
24102 EditorMode::full(),
24103 buffer,
24104 Some(project.clone()),
24105 window,
24106 cx,
24107 )
24108 });
24109 workspace
24110 .update(cx, |workspace, window, cx| {
24111 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24112 })
24113 .unwrap();
24114 editor.update_in(cx, |editor, window, cx| {
24115 editor.open_context_menu(&OpenContextMenu, window, cx);
24116 assert!(editor.mouse_context_menu.is_some());
24117 });
24118 workspace
24119 .update(cx, |workspace, window, cx| {
24120 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24121 })
24122 .unwrap();
24123 cx.read(|cx| {
24124 assert!(editor.read(cx).mouse_context_menu.is_none());
24125 });
24126}
24127
24128fn set_linked_edit_ranges(
24129 opening: (Point, Point),
24130 closing: (Point, Point),
24131 editor: &mut Editor,
24132 cx: &mut Context<Editor>,
24133) {
24134 let Some((buffer, _)) = editor
24135 .buffer
24136 .read(cx)
24137 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24138 else {
24139 panic!("Failed to get buffer for selection position");
24140 };
24141 let buffer = buffer.read(cx);
24142 let buffer_id = buffer.remote_id();
24143 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24144 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24145 let mut linked_ranges = HashMap::default();
24146 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24147 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24148}
24149
24150#[gpui::test]
24151async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24152 init_test(cx, |_| {});
24153
24154 let fs = FakeFs::new(cx.executor());
24155 fs.insert_file(path!("/file.html"), Default::default())
24156 .await;
24157
24158 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24159
24160 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24161 let html_language = Arc::new(Language::new(
24162 LanguageConfig {
24163 name: "HTML".into(),
24164 matcher: LanguageMatcher {
24165 path_suffixes: vec!["html".to_string()],
24166 ..LanguageMatcher::default()
24167 },
24168 brackets: BracketPairConfig {
24169 pairs: vec![BracketPair {
24170 start: "<".into(),
24171 end: ">".into(),
24172 close: true,
24173 ..Default::default()
24174 }],
24175 ..Default::default()
24176 },
24177 ..Default::default()
24178 },
24179 Some(tree_sitter_html::LANGUAGE.into()),
24180 ));
24181 language_registry.add(html_language);
24182 let mut fake_servers = language_registry.register_fake_lsp(
24183 "HTML",
24184 FakeLspAdapter {
24185 capabilities: lsp::ServerCapabilities {
24186 completion_provider: Some(lsp::CompletionOptions {
24187 resolve_provider: Some(true),
24188 ..Default::default()
24189 }),
24190 ..Default::default()
24191 },
24192 ..Default::default()
24193 },
24194 );
24195
24196 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24197 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24198
24199 let worktree_id = workspace
24200 .update(cx, |workspace, _window, cx| {
24201 workspace.project().update(cx, |project, cx| {
24202 project.worktrees(cx).next().unwrap().read(cx).id()
24203 })
24204 })
24205 .unwrap();
24206 project
24207 .update(cx, |project, cx| {
24208 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24209 })
24210 .await
24211 .unwrap();
24212 let editor = workspace
24213 .update(cx, |workspace, window, cx| {
24214 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24215 })
24216 .unwrap()
24217 .await
24218 .unwrap()
24219 .downcast::<Editor>()
24220 .unwrap();
24221
24222 let fake_server = fake_servers.next().await.unwrap();
24223 editor.update_in(cx, |editor, window, cx| {
24224 editor.set_text("<ad></ad>", window, cx);
24225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24226 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24227 });
24228 set_linked_edit_ranges(
24229 (Point::new(0, 1), Point::new(0, 3)),
24230 (Point::new(0, 6), Point::new(0, 8)),
24231 editor,
24232 cx,
24233 );
24234 });
24235 let mut completion_handle =
24236 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24237 Ok(Some(lsp::CompletionResponse::Array(vec![
24238 lsp::CompletionItem {
24239 label: "head".to_string(),
24240 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24241 lsp::InsertReplaceEdit {
24242 new_text: "head".to_string(),
24243 insert: lsp::Range::new(
24244 lsp::Position::new(0, 1),
24245 lsp::Position::new(0, 3),
24246 ),
24247 replace: lsp::Range::new(
24248 lsp::Position::new(0, 1),
24249 lsp::Position::new(0, 3),
24250 ),
24251 },
24252 )),
24253 ..Default::default()
24254 },
24255 ])))
24256 });
24257 editor.update_in(cx, |editor, window, cx| {
24258 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24259 });
24260 cx.run_until_parked();
24261 completion_handle.next().await.unwrap();
24262 editor.update(cx, |editor, _| {
24263 assert!(
24264 editor.context_menu_visible(),
24265 "Completion menu should be visible"
24266 );
24267 });
24268 editor.update_in(cx, |editor, window, cx| {
24269 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24270 });
24271 cx.executor().run_until_parked();
24272 editor.update(cx, |editor, cx| {
24273 assert_eq!(editor.text(cx), "<head></head>");
24274 });
24275}
24276
24277#[gpui::test]
24278async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24279 init_test(cx, |_| {});
24280
24281 let mut cx = EditorTestContext::new(cx).await;
24282 let language = Arc::new(Language::new(
24283 LanguageConfig {
24284 name: "TSX".into(),
24285 matcher: LanguageMatcher {
24286 path_suffixes: vec!["tsx".to_string()],
24287 ..LanguageMatcher::default()
24288 },
24289 brackets: BracketPairConfig {
24290 pairs: vec![BracketPair {
24291 start: "<".into(),
24292 end: ">".into(),
24293 close: true,
24294 ..Default::default()
24295 }],
24296 ..Default::default()
24297 },
24298 linked_edit_characters: HashSet::from_iter(['.']),
24299 ..Default::default()
24300 },
24301 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24302 ));
24303 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24304
24305 // Test typing > does not extend linked pair
24306 cx.set_state("<divˇ<div></div>");
24307 cx.update_editor(|editor, _, cx| {
24308 set_linked_edit_ranges(
24309 (Point::new(0, 1), Point::new(0, 4)),
24310 (Point::new(0, 11), Point::new(0, 14)),
24311 editor,
24312 cx,
24313 );
24314 });
24315 cx.update_editor(|editor, window, cx| {
24316 editor.handle_input(">", window, cx);
24317 });
24318 cx.assert_editor_state("<div>ˇ<div></div>");
24319
24320 // Test typing . do extend linked pair
24321 cx.set_state("<Animatedˇ></Animated>");
24322 cx.update_editor(|editor, _, cx| {
24323 set_linked_edit_ranges(
24324 (Point::new(0, 1), Point::new(0, 9)),
24325 (Point::new(0, 12), Point::new(0, 20)),
24326 editor,
24327 cx,
24328 );
24329 });
24330 cx.update_editor(|editor, window, cx| {
24331 editor.handle_input(".", window, cx);
24332 });
24333 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24334 cx.update_editor(|editor, _, cx| {
24335 set_linked_edit_ranges(
24336 (Point::new(0, 1), Point::new(0, 10)),
24337 (Point::new(0, 13), Point::new(0, 21)),
24338 editor,
24339 cx,
24340 );
24341 });
24342 cx.update_editor(|editor, window, cx| {
24343 editor.handle_input("V", window, cx);
24344 });
24345 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24346}
24347
24348#[gpui::test]
24349async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24350 init_test(cx, |_| {});
24351
24352 let fs = FakeFs::new(cx.executor());
24353 fs.insert_tree(
24354 path!("/root"),
24355 json!({
24356 "a": {
24357 "main.rs": "fn main() {}",
24358 },
24359 "foo": {
24360 "bar": {
24361 "external_file.rs": "pub mod external {}",
24362 }
24363 }
24364 }),
24365 )
24366 .await;
24367
24368 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24369 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24370 language_registry.add(rust_lang());
24371 let _fake_servers = language_registry.register_fake_lsp(
24372 "Rust",
24373 FakeLspAdapter {
24374 ..FakeLspAdapter::default()
24375 },
24376 );
24377 let (workspace, cx) =
24378 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24379 let worktree_id = workspace.update(cx, |workspace, cx| {
24380 workspace.project().update(cx, |project, cx| {
24381 project.worktrees(cx).next().unwrap().read(cx).id()
24382 })
24383 });
24384
24385 let assert_language_servers_count =
24386 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24387 project.update(cx, |project, cx| {
24388 let current = project
24389 .lsp_store()
24390 .read(cx)
24391 .as_local()
24392 .unwrap()
24393 .language_servers
24394 .len();
24395 assert_eq!(expected, current, "{context}");
24396 });
24397 };
24398
24399 assert_language_servers_count(
24400 0,
24401 "No servers should be running before any file is open",
24402 cx,
24403 );
24404 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24405 let main_editor = workspace
24406 .update_in(cx, |workspace, window, cx| {
24407 workspace.open_path(
24408 (worktree_id, rel_path("main.rs")),
24409 Some(pane.downgrade()),
24410 true,
24411 window,
24412 cx,
24413 )
24414 })
24415 .unwrap()
24416 .await
24417 .downcast::<Editor>()
24418 .unwrap();
24419 pane.update(cx, |pane, cx| {
24420 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24421 open_editor.update(cx, |editor, cx| {
24422 assert_eq!(
24423 editor.display_text(cx),
24424 "fn main() {}",
24425 "Original main.rs text on initial open",
24426 );
24427 });
24428 assert_eq!(open_editor, main_editor);
24429 });
24430 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24431
24432 let external_editor = workspace
24433 .update_in(cx, |workspace, window, cx| {
24434 workspace.open_abs_path(
24435 PathBuf::from("/root/foo/bar/external_file.rs"),
24436 OpenOptions::default(),
24437 window,
24438 cx,
24439 )
24440 })
24441 .await
24442 .expect("opening external file")
24443 .downcast::<Editor>()
24444 .expect("downcasted external file's open element to editor");
24445 pane.update(cx, |pane, cx| {
24446 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24447 open_editor.update(cx, |editor, cx| {
24448 assert_eq!(
24449 editor.display_text(cx),
24450 "pub mod external {}",
24451 "External file is open now",
24452 );
24453 });
24454 assert_eq!(open_editor, external_editor);
24455 });
24456 assert_language_servers_count(
24457 1,
24458 "Second, external, *.rs file should join the existing server",
24459 cx,
24460 );
24461
24462 pane.update_in(cx, |pane, window, cx| {
24463 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24464 })
24465 .await
24466 .unwrap();
24467 pane.update_in(cx, |pane, window, cx| {
24468 pane.navigate_backward(&Default::default(), window, cx);
24469 });
24470 cx.run_until_parked();
24471 pane.update(cx, |pane, cx| {
24472 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24473 open_editor.update(cx, |editor, cx| {
24474 assert_eq!(
24475 editor.display_text(cx),
24476 "pub mod external {}",
24477 "External file is open now",
24478 );
24479 });
24480 });
24481 assert_language_servers_count(
24482 1,
24483 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24484 cx,
24485 );
24486
24487 cx.update(|_, cx| {
24488 workspace::reload(cx);
24489 });
24490 assert_language_servers_count(
24491 1,
24492 "After reloading the worktree with local and external files opened, only one project should be started",
24493 cx,
24494 );
24495}
24496
24497#[gpui::test]
24498async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24499 init_test(cx, |_| {});
24500
24501 let mut cx = EditorTestContext::new(cx).await;
24502 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24503 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24504
24505 // test cursor move to start of each line on tab
24506 // for `if`, `elif`, `else`, `while`, `with` and `for`
24507 cx.set_state(indoc! {"
24508 def main():
24509 ˇ for item in items:
24510 ˇ while item.active:
24511 ˇ if item.value > 10:
24512 ˇ continue
24513 ˇ elif item.value < 0:
24514 ˇ break
24515 ˇ else:
24516 ˇ with item.context() as ctx:
24517 ˇ yield count
24518 ˇ else:
24519 ˇ log('while else')
24520 ˇ else:
24521 ˇ log('for else')
24522 "});
24523 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24524 cx.assert_editor_state(indoc! {"
24525 def main():
24526 ˇfor item in items:
24527 ˇwhile item.active:
24528 ˇif item.value > 10:
24529 ˇcontinue
24530 ˇelif item.value < 0:
24531 ˇbreak
24532 ˇelse:
24533 ˇwith item.context() as ctx:
24534 ˇyield count
24535 ˇelse:
24536 ˇlog('while else')
24537 ˇelse:
24538 ˇlog('for else')
24539 "});
24540 // test relative indent is preserved when tab
24541 // for `if`, `elif`, `else`, `while`, `with` and `for`
24542 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24543 cx.assert_editor_state(indoc! {"
24544 def main():
24545 ˇfor item in items:
24546 ˇwhile item.active:
24547 ˇif item.value > 10:
24548 ˇcontinue
24549 ˇelif item.value < 0:
24550 ˇbreak
24551 ˇelse:
24552 ˇwith item.context() as ctx:
24553 ˇyield count
24554 ˇelse:
24555 ˇlog('while else')
24556 ˇelse:
24557 ˇlog('for else')
24558 "});
24559
24560 // test cursor move to start of each line on tab
24561 // for `try`, `except`, `else`, `finally`, `match` and `def`
24562 cx.set_state(indoc! {"
24563 def main():
24564 ˇ try:
24565 ˇ fetch()
24566 ˇ except ValueError:
24567 ˇ handle_error()
24568 ˇ else:
24569 ˇ match value:
24570 ˇ case _:
24571 ˇ finally:
24572 ˇ def status():
24573 ˇ return 0
24574 "});
24575 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24576 cx.assert_editor_state(indoc! {"
24577 def main():
24578 ˇtry:
24579 ˇfetch()
24580 ˇexcept ValueError:
24581 ˇhandle_error()
24582 ˇelse:
24583 ˇmatch value:
24584 ˇcase _:
24585 ˇfinally:
24586 ˇdef status():
24587 ˇreturn 0
24588 "});
24589 // test relative indent is preserved when tab
24590 // for `try`, `except`, `else`, `finally`, `match` and `def`
24591 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24592 cx.assert_editor_state(indoc! {"
24593 def main():
24594 ˇtry:
24595 ˇfetch()
24596 ˇexcept ValueError:
24597 ˇhandle_error()
24598 ˇelse:
24599 ˇmatch value:
24600 ˇcase _:
24601 ˇfinally:
24602 ˇdef status():
24603 ˇreturn 0
24604 "});
24605}
24606
24607#[gpui::test]
24608async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24609 init_test(cx, |_| {});
24610
24611 let mut cx = EditorTestContext::new(cx).await;
24612 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24613 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24614
24615 // test `else` auto outdents when typed inside `if` block
24616 cx.set_state(indoc! {"
24617 def main():
24618 if i == 2:
24619 return
24620 ˇ
24621 "});
24622 cx.update_editor(|editor, window, cx| {
24623 editor.handle_input("else:", window, cx);
24624 });
24625 cx.assert_editor_state(indoc! {"
24626 def main():
24627 if i == 2:
24628 return
24629 else:ˇ
24630 "});
24631
24632 // test `except` auto outdents when typed inside `try` block
24633 cx.set_state(indoc! {"
24634 def main():
24635 try:
24636 i = 2
24637 ˇ
24638 "});
24639 cx.update_editor(|editor, window, cx| {
24640 editor.handle_input("except:", window, cx);
24641 });
24642 cx.assert_editor_state(indoc! {"
24643 def main():
24644 try:
24645 i = 2
24646 except:ˇ
24647 "});
24648
24649 // test `else` auto outdents when typed inside `except` block
24650 cx.set_state(indoc! {"
24651 def main():
24652 try:
24653 i = 2
24654 except:
24655 j = 2
24656 ˇ
24657 "});
24658 cx.update_editor(|editor, window, cx| {
24659 editor.handle_input("else:", window, cx);
24660 });
24661 cx.assert_editor_state(indoc! {"
24662 def main():
24663 try:
24664 i = 2
24665 except:
24666 j = 2
24667 else:ˇ
24668 "});
24669
24670 // test `finally` auto outdents when typed inside `else` block
24671 cx.set_state(indoc! {"
24672 def main():
24673 try:
24674 i = 2
24675 except:
24676 j = 2
24677 else:
24678 k = 2
24679 ˇ
24680 "});
24681 cx.update_editor(|editor, window, cx| {
24682 editor.handle_input("finally:", window, cx);
24683 });
24684 cx.assert_editor_state(indoc! {"
24685 def main():
24686 try:
24687 i = 2
24688 except:
24689 j = 2
24690 else:
24691 k = 2
24692 finally:ˇ
24693 "});
24694
24695 // test `else` does not outdents when typed inside `except` block right after for block
24696 cx.set_state(indoc! {"
24697 def main():
24698 try:
24699 i = 2
24700 except:
24701 for i in range(n):
24702 pass
24703 ˇ
24704 "});
24705 cx.update_editor(|editor, window, cx| {
24706 editor.handle_input("else:", window, cx);
24707 });
24708 cx.assert_editor_state(indoc! {"
24709 def main():
24710 try:
24711 i = 2
24712 except:
24713 for i in range(n):
24714 pass
24715 else:ˇ
24716 "});
24717
24718 // test `finally` auto outdents when typed inside `else` block right after for block
24719 cx.set_state(indoc! {"
24720 def main():
24721 try:
24722 i = 2
24723 except:
24724 j = 2
24725 else:
24726 for i in range(n):
24727 pass
24728 ˇ
24729 "});
24730 cx.update_editor(|editor, window, cx| {
24731 editor.handle_input("finally:", window, cx);
24732 });
24733 cx.assert_editor_state(indoc! {"
24734 def main():
24735 try:
24736 i = 2
24737 except:
24738 j = 2
24739 else:
24740 for i in range(n):
24741 pass
24742 finally:ˇ
24743 "});
24744
24745 // test `except` outdents to inner "try" block
24746 cx.set_state(indoc! {"
24747 def main():
24748 try:
24749 i = 2
24750 if i == 2:
24751 try:
24752 i = 3
24753 ˇ
24754 "});
24755 cx.update_editor(|editor, window, cx| {
24756 editor.handle_input("except:", window, cx);
24757 });
24758 cx.assert_editor_state(indoc! {"
24759 def main():
24760 try:
24761 i = 2
24762 if i == 2:
24763 try:
24764 i = 3
24765 except:ˇ
24766 "});
24767
24768 // test `except` outdents to outer "try" block
24769 cx.set_state(indoc! {"
24770 def main():
24771 try:
24772 i = 2
24773 if i == 2:
24774 try:
24775 i = 3
24776 ˇ
24777 "});
24778 cx.update_editor(|editor, window, cx| {
24779 editor.handle_input("except:", window, cx);
24780 });
24781 cx.assert_editor_state(indoc! {"
24782 def main():
24783 try:
24784 i = 2
24785 if i == 2:
24786 try:
24787 i = 3
24788 except:ˇ
24789 "});
24790
24791 // test `else` stays at correct indent when typed after `for` block
24792 cx.set_state(indoc! {"
24793 def main():
24794 for i in range(10):
24795 if i == 3:
24796 break
24797 ˇ
24798 "});
24799 cx.update_editor(|editor, window, cx| {
24800 editor.handle_input("else:", window, cx);
24801 });
24802 cx.assert_editor_state(indoc! {"
24803 def main():
24804 for i in range(10):
24805 if i == 3:
24806 break
24807 else:ˇ
24808 "});
24809
24810 // test does not outdent on typing after line with square brackets
24811 cx.set_state(indoc! {"
24812 def f() -> list[str]:
24813 ˇ
24814 "});
24815 cx.update_editor(|editor, window, cx| {
24816 editor.handle_input("a", window, cx);
24817 });
24818 cx.assert_editor_state(indoc! {"
24819 def f() -> list[str]:
24820 aˇ
24821 "});
24822
24823 // test does not outdent on typing : after case keyword
24824 cx.set_state(indoc! {"
24825 match 1:
24826 caseˇ
24827 "});
24828 cx.update_editor(|editor, window, cx| {
24829 editor.handle_input(":", window, cx);
24830 });
24831 cx.assert_editor_state(indoc! {"
24832 match 1:
24833 case:ˇ
24834 "});
24835}
24836
24837#[gpui::test]
24838async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24839 init_test(cx, |_| {});
24840 update_test_language_settings(cx, |settings| {
24841 settings.defaults.extend_comment_on_newline = Some(false);
24842 });
24843 let mut cx = EditorTestContext::new(cx).await;
24844 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24845 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24846
24847 // test correct indent after newline on comment
24848 cx.set_state(indoc! {"
24849 # COMMENT:ˇ
24850 "});
24851 cx.update_editor(|editor, window, cx| {
24852 editor.newline(&Newline, window, cx);
24853 });
24854 cx.assert_editor_state(indoc! {"
24855 # COMMENT:
24856 ˇ
24857 "});
24858
24859 // test correct indent after newline in brackets
24860 cx.set_state(indoc! {"
24861 {ˇ}
24862 "});
24863 cx.update_editor(|editor, window, cx| {
24864 editor.newline(&Newline, window, cx);
24865 });
24866 cx.run_until_parked();
24867 cx.assert_editor_state(indoc! {"
24868 {
24869 ˇ
24870 }
24871 "});
24872
24873 cx.set_state(indoc! {"
24874 (ˇ)
24875 "});
24876 cx.update_editor(|editor, window, cx| {
24877 editor.newline(&Newline, window, cx);
24878 });
24879 cx.run_until_parked();
24880 cx.assert_editor_state(indoc! {"
24881 (
24882 ˇ
24883 )
24884 "});
24885
24886 // do not indent after empty lists or dictionaries
24887 cx.set_state(indoc! {"
24888 a = []ˇ
24889 "});
24890 cx.update_editor(|editor, window, cx| {
24891 editor.newline(&Newline, window, cx);
24892 });
24893 cx.run_until_parked();
24894 cx.assert_editor_state(indoc! {"
24895 a = []
24896 ˇ
24897 "});
24898}
24899
24900#[gpui::test]
24901async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24902 init_test(cx, |_| {});
24903
24904 let mut cx = EditorTestContext::new(cx).await;
24905 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24906 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24907
24908 // test cursor move to start of each line on tab
24909 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24910 cx.set_state(indoc! {"
24911 function main() {
24912 ˇ for item in $items; do
24913 ˇ while [ -n \"$item\" ]; do
24914 ˇ if [ \"$value\" -gt 10 ]; then
24915 ˇ continue
24916 ˇ elif [ \"$value\" -lt 0 ]; then
24917 ˇ break
24918 ˇ else
24919 ˇ echo \"$item\"
24920 ˇ fi
24921 ˇ done
24922 ˇ done
24923 ˇ}
24924 "});
24925 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24926 cx.assert_editor_state(indoc! {"
24927 function main() {
24928 ˇfor item in $items; do
24929 ˇwhile [ -n \"$item\" ]; do
24930 ˇif [ \"$value\" -gt 10 ]; then
24931 ˇcontinue
24932 ˇelif [ \"$value\" -lt 0 ]; then
24933 ˇbreak
24934 ˇelse
24935 ˇecho \"$item\"
24936 ˇfi
24937 ˇdone
24938 ˇdone
24939 ˇ}
24940 "});
24941 // test relative indent is preserved when tab
24942 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24943 cx.assert_editor_state(indoc! {"
24944 function main() {
24945 ˇfor item in $items; do
24946 ˇwhile [ -n \"$item\" ]; do
24947 ˇif [ \"$value\" -gt 10 ]; then
24948 ˇcontinue
24949 ˇelif [ \"$value\" -lt 0 ]; then
24950 ˇbreak
24951 ˇelse
24952 ˇecho \"$item\"
24953 ˇfi
24954 ˇdone
24955 ˇdone
24956 ˇ}
24957 "});
24958
24959 // test cursor move to start of each line on tab
24960 // for `case` statement with patterns
24961 cx.set_state(indoc! {"
24962 function handle() {
24963 ˇ case \"$1\" in
24964 ˇ start)
24965 ˇ echo \"a\"
24966 ˇ ;;
24967 ˇ stop)
24968 ˇ echo \"b\"
24969 ˇ ;;
24970 ˇ *)
24971 ˇ echo \"c\"
24972 ˇ ;;
24973 ˇ esac
24974 ˇ}
24975 "});
24976 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24977 cx.assert_editor_state(indoc! {"
24978 function handle() {
24979 ˇcase \"$1\" in
24980 ˇstart)
24981 ˇecho \"a\"
24982 ˇ;;
24983 ˇstop)
24984 ˇecho \"b\"
24985 ˇ;;
24986 ˇ*)
24987 ˇecho \"c\"
24988 ˇ;;
24989 ˇesac
24990 ˇ}
24991 "});
24992}
24993
24994#[gpui::test]
24995async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24996 init_test(cx, |_| {});
24997
24998 let mut cx = EditorTestContext::new(cx).await;
24999 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25000 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25001
25002 // test indents on comment insert
25003 cx.set_state(indoc! {"
25004 function main() {
25005 ˇ for item in $items; do
25006 ˇ while [ -n \"$item\" ]; do
25007 ˇ if [ \"$value\" -gt 10 ]; then
25008 ˇ continue
25009 ˇ elif [ \"$value\" -lt 0 ]; then
25010 ˇ break
25011 ˇ else
25012 ˇ echo \"$item\"
25013 ˇ fi
25014 ˇ done
25015 ˇ done
25016 ˇ}
25017 "});
25018 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25019 cx.assert_editor_state(indoc! {"
25020 function main() {
25021 #ˇ for item in $items; do
25022 #ˇ while [ -n \"$item\" ]; do
25023 #ˇ if [ \"$value\" -gt 10 ]; then
25024 #ˇ continue
25025 #ˇ elif [ \"$value\" -lt 0 ]; then
25026 #ˇ break
25027 #ˇ else
25028 #ˇ echo \"$item\"
25029 #ˇ fi
25030 #ˇ done
25031 #ˇ done
25032 #ˇ}
25033 "});
25034}
25035
25036#[gpui::test]
25037async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25038 init_test(cx, |_| {});
25039
25040 let mut cx = EditorTestContext::new(cx).await;
25041 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25042 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25043
25044 // test `else` auto outdents when typed inside `if` block
25045 cx.set_state(indoc! {"
25046 if [ \"$1\" = \"test\" ]; then
25047 echo \"foo bar\"
25048 ˇ
25049 "});
25050 cx.update_editor(|editor, window, cx| {
25051 editor.handle_input("else", window, cx);
25052 });
25053 cx.assert_editor_state(indoc! {"
25054 if [ \"$1\" = \"test\" ]; then
25055 echo \"foo bar\"
25056 elseˇ
25057 "});
25058
25059 // test `elif` auto outdents when typed inside `if` block
25060 cx.set_state(indoc! {"
25061 if [ \"$1\" = \"test\" ]; then
25062 echo \"foo bar\"
25063 ˇ
25064 "});
25065 cx.update_editor(|editor, window, cx| {
25066 editor.handle_input("elif", window, cx);
25067 });
25068 cx.assert_editor_state(indoc! {"
25069 if [ \"$1\" = \"test\" ]; then
25070 echo \"foo bar\"
25071 elifˇ
25072 "});
25073
25074 // test `fi` auto outdents when typed inside `else` block
25075 cx.set_state(indoc! {"
25076 if [ \"$1\" = \"test\" ]; then
25077 echo \"foo bar\"
25078 else
25079 echo \"bar baz\"
25080 ˇ
25081 "});
25082 cx.update_editor(|editor, window, cx| {
25083 editor.handle_input("fi", window, cx);
25084 });
25085 cx.assert_editor_state(indoc! {"
25086 if [ \"$1\" = \"test\" ]; then
25087 echo \"foo bar\"
25088 else
25089 echo \"bar baz\"
25090 fiˇ
25091 "});
25092
25093 // test `done` auto outdents when typed inside `while` block
25094 cx.set_state(indoc! {"
25095 while read line; do
25096 echo \"$line\"
25097 ˇ
25098 "});
25099 cx.update_editor(|editor, window, cx| {
25100 editor.handle_input("done", window, cx);
25101 });
25102 cx.assert_editor_state(indoc! {"
25103 while read line; do
25104 echo \"$line\"
25105 doneˇ
25106 "});
25107
25108 // test `done` auto outdents when typed inside `for` block
25109 cx.set_state(indoc! {"
25110 for file in *.txt; do
25111 cat \"$file\"
25112 ˇ
25113 "});
25114 cx.update_editor(|editor, window, cx| {
25115 editor.handle_input("done", window, cx);
25116 });
25117 cx.assert_editor_state(indoc! {"
25118 for file in *.txt; do
25119 cat \"$file\"
25120 doneˇ
25121 "});
25122
25123 // test `esac` auto outdents when typed inside `case` block
25124 cx.set_state(indoc! {"
25125 case \"$1\" in
25126 start)
25127 echo \"foo bar\"
25128 ;;
25129 stop)
25130 echo \"bar baz\"
25131 ;;
25132 ˇ
25133 "});
25134 cx.update_editor(|editor, window, cx| {
25135 editor.handle_input("esac", window, cx);
25136 });
25137 cx.assert_editor_state(indoc! {"
25138 case \"$1\" in
25139 start)
25140 echo \"foo bar\"
25141 ;;
25142 stop)
25143 echo \"bar baz\"
25144 ;;
25145 esacˇ
25146 "});
25147
25148 // test `*)` auto outdents when typed inside `case` block
25149 cx.set_state(indoc! {"
25150 case \"$1\" in
25151 start)
25152 echo \"foo bar\"
25153 ;;
25154 ˇ
25155 "});
25156 cx.update_editor(|editor, window, cx| {
25157 editor.handle_input("*)", window, cx);
25158 });
25159 cx.assert_editor_state(indoc! {"
25160 case \"$1\" in
25161 start)
25162 echo \"foo bar\"
25163 ;;
25164 *)ˇ
25165 "});
25166
25167 // test `fi` outdents to correct level with nested if blocks
25168 cx.set_state(indoc! {"
25169 if [ \"$1\" = \"test\" ]; then
25170 echo \"outer if\"
25171 if [ \"$2\" = \"debug\" ]; then
25172 echo \"inner if\"
25173 ˇ
25174 "});
25175 cx.update_editor(|editor, window, cx| {
25176 editor.handle_input("fi", window, cx);
25177 });
25178 cx.assert_editor_state(indoc! {"
25179 if [ \"$1\" = \"test\" ]; then
25180 echo \"outer if\"
25181 if [ \"$2\" = \"debug\" ]; then
25182 echo \"inner if\"
25183 fiˇ
25184 "});
25185}
25186
25187#[gpui::test]
25188async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25189 init_test(cx, |_| {});
25190 update_test_language_settings(cx, |settings| {
25191 settings.defaults.extend_comment_on_newline = Some(false);
25192 });
25193 let mut cx = EditorTestContext::new(cx).await;
25194 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25196
25197 // test correct indent after newline on comment
25198 cx.set_state(indoc! {"
25199 # COMMENT:ˇ
25200 "});
25201 cx.update_editor(|editor, window, cx| {
25202 editor.newline(&Newline, window, cx);
25203 });
25204 cx.assert_editor_state(indoc! {"
25205 # COMMENT:
25206 ˇ
25207 "});
25208
25209 // test correct indent after newline after `then`
25210 cx.set_state(indoc! {"
25211
25212 if [ \"$1\" = \"test\" ]; thenˇ
25213 "});
25214 cx.update_editor(|editor, window, cx| {
25215 editor.newline(&Newline, window, cx);
25216 });
25217 cx.run_until_parked();
25218 cx.assert_editor_state(indoc! {"
25219
25220 if [ \"$1\" = \"test\" ]; then
25221 ˇ
25222 "});
25223
25224 // test correct indent after newline after `else`
25225 cx.set_state(indoc! {"
25226 if [ \"$1\" = \"test\" ]; then
25227 elseˇ
25228 "});
25229 cx.update_editor(|editor, window, cx| {
25230 editor.newline(&Newline, window, cx);
25231 });
25232 cx.run_until_parked();
25233 cx.assert_editor_state(indoc! {"
25234 if [ \"$1\" = \"test\" ]; then
25235 else
25236 ˇ
25237 "});
25238
25239 // test correct indent after newline after `elif`
25240 cx.set_state(indoc! {"
25241 if [ \"$1\" = \"test\" ]; then
25242 elifˇ
25243 "});
25244 cx.update_editor(|editor, window, cx| {
25245 editor.newline(&Newline, window, cx);
25246 });
25247 cx.run_until_parked();
25248 cx.assert_editor_state(indoc! {"
25249 if [ \"$1\" = \"test\" ]; then
25250 elif
25251 ˇ
25252 "});
25253
25254 // test correct indent after newline after `do`
25255 cx.set_state(indoc! {"
25256 for file in *.txt; doˇ
25257 "});
25258 cx.update_editor(|editor, window, cx| {
25259 editor.newline(&Newline, window, cx);
25260 });
25261 cx.run_until_parked();
25262 cx.assert_editor_state(indoc! {"
25263 for file in *.txt; do
25264 ˇ
25265 "});
25266
25267 // test correct indent after newline after case pattern
25268 cx.set_state(indoc! {"
25269 case \"$1\" in
25270 start)ˇ
25271 "});
25272 cx.update_editor(|editor, window, cx| {
25273 editor.newline(&Newline, window, cx);
25274 });
25275 cx.run_until_parked();
25276 cx.assert_editor_state(indoc! {"
25277 case \"$1\" in
25278 start)
25279 ˇ
25280 "});
25281
25282 // test correct indent after newline after case pattern
25283 cx.set_state(indoc! {"
25284 case \"$1\" in
25285 start)
25286 ;;
25287 *)ˇ
25288 "});
25289 cx.update_editor(|editor, window, cx| {
25290 editor.newline(&Newline, window, cx);
25291 });
25292 cx.run_until_parked();
25293 cx.assert_editor_state(indoc! {"
25294 case \"$1\" in
25295 start)
25296 ;;
25297 *)
25298 ˇ
25299 "});
25300
25301 // test correct indent after newline after function opening brace
25302 cx.set_state(indoc! {"
25303 function test() {ˇ}
25304 "});
25305 cx.update_editor(|editor, window, cx| {
25306 editor.newline(&Newline, window, cx);
25307 });
25308 cx.run_until_parked();
25309 cx.assert_editor_state(indoc! {"
25310 function test() {
25311 ˇ
25312 }
25313 "});
25314
25315 // test no extra indent after semicolon on same line
25316 cx.set_state(indoc! {"
25317 echo \"test\";ˇ
25318 "});
25319 cx.update_editor(|editor, window, cx| {
25320 editor.newline(&Newline, window, cx);
25321 });
25322 cx.run_until_parked();
25323 cx.assert_editor_state(indoc! {"
25324 echo \"test\";
25325 ˇ
25326 "});
25327}
25328
25329fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25330 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25331 point..point
25332}
25333
25334#[track_caller]
25335fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25336 let (text, ranges) = marked_text_ranges(marked_text, true);
25337 assert_eq!(editor.text(cx), text);
25338 assert_eq!(
25339 editor.selections.ranges(&editor.display_snapshot(cx)),
25340 ranges,
25341 "Assert selections are {}",
25342 marked_text
25343 );
25344}
25345
25346pub fn handle_signature_help_request(
25347 cx: &mut EditorLspTestContext,
25348 mocked_response: lsp::SignatureHelp,
25349) -> impl Future<Output = ()> + use<> {
25350 let mut request =
25351 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25352 let mocked_response = mocked_response.clone();
25353 async move { Ok(Some(mocked_response)) }
25354 });
25355
25356 async move {
25357 request.next().await;
25358 }
25359}
25360
25361#[track_caller]
25362pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25363 cx.update_editor(|editor, _, _| {
25364 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25365 let entries = menu.entries.borrow();
25366 let entries = entries
25367 .iter()
25368 .map(|entry| entry.string.as_str())
25369 .collect::<Vec<_>>();
25370 assert_eq!(entries, expected);
25371 } else {
25372 panic!("Expected completions menu");
25373 }
25374 });
25375}
25376
25377/// Handle completion request passing a marked string specifying where the completion
25378/// should be triggered from using '|' character, what range should be replaced, and what completions
25379/// should be returned using '<' and '>' to delimit the range.
25380///
25381/// Also see `handle_completion_request_with_insert_and_replace`.
25382#[track_caller]
25383pub fn handle_completion_request(
25384 marked_string: &str,
25385 completions: Vec<&'static str>,
25386 is_incomplete: bool,
25387 counter: Arc<AtomicUsize>,
25388 cx: &mut EditorLspTestContext,
25389) -> impl Future<Output = ()> {
25390 let complete_from_marker: TextRangeMarker = '|'.into();
25391 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25392 let (_, mut marked_ranges) = marked_text_ranges_by(
25393 marked_string,
25394 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25395 );
25396
25397 let complete_from_position =
25398 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25399 let replace_range =
25400 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25401
25402 let mut request =
25403 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25404 let completions = completions.clone();
25405 counter.fetch_add(1, atomic::Ordering::Release);
25406 async move {
25407 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25408 assert_eq!(
25409 params.text_document_position.position,
25410 complete_from_position
25411 );
25412 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25413 is_incomplete,
25414 item_defaults: None,
25415 items: completions
25416 .iter()
25417 .map(|completion_text| lsp::CompletionItem {
25418 label: completion_text.to_string(),
25419 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25420 range: replace_range,
25421 new_text: completion_text.to_string(),
25422 })),
25423 ..Default::default()
25424 })
25425 .collect(),
25426 })))
25427 }
25428 });
25429
25430 async move {
25431 request.next().await;
25432 }
25433}
25434
25435/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25436/// given instead, which also contains an `insert` range.
25437///
25438/// This function uses markers to define ranges:
25439/// - `|` marks the cursor position
25440/// - `<>` marks the replace range
25441/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25442pub fn handle_completion_request_with_insert_and_replace(
25443 cx: &mut EditorLspTestContext,
25444 marked_string: &str,
25445 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25446 counter: Arc<AtomicUsize>,
25447) -> impl Future<Output = ()> {
25448 let complete_from_marker: TextRangeMarker = '|'.into();
25449 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25450 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25451
25452 let (_, mut marked_ranges) = marked_text_ranges_by(
25453 marked_string,
25454 vec![
25455 complete_from_marker.clone(),
25456 replace_range_marker.clone(),
25457 insert_range_marker.clone(),
25458 ],
25459 );
25460
25461 let complete_from_position =
25462 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25463 let replace_range =
25464 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25465
25466 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25467 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25468 _ => lsp::Range {
25469 start: replace_range.start,
25470 end: complete_from_position,
25471 },
25472 };
25473
25474 let mut request =
25475 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25476 let completions = completions.clone();
25477 counter.fetch_add(1, atomic::Ordering::Release);
25478 async move {
25479 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25480 assert_eq!(
25481 params.text_document_position.position, complete_from_position,
25482 "marker `|` position doesn't match",
25483 );
25484 Ok(Some(lsp::CompletionResponse::Array(
25485 completions
25486 .iter()
25487 .map(|(label, new_text)| lsp::CompletionItem {
25488 label: label.to_string(),
25489 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25490 lsp::InsertReplaceEdit {
25491 insert: insert_range,
25492 replace: replace_range,
25493 new_text: new_text.to_string(),
25494 },
25495 )),
25496 ..Default::default()
25497 })
25498 .collect(),
25499 )))
25500 }
25501 });
25502
25503 async move {
25504 request.next().await;
25505 }
25506}
25507
25508fn handle_resolve_completion_request(
25509 cx: &mut EditorLspTestContext,
25510 edits: Option<Vec<(&'static str, &'static str)>>,
25511) -> impl Future<Output = ()> {
25512 let edits = edits.map(|edits| {
25513 edits
25514 .iter()
25515 .map(|(marked_string, new_text)| {
25516 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25517 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25518 lsp::TextEdit::new(replace_range, new_text.to_string())
25519 })
25520 .collect::<Vec<_>>()
25521 });
25522
25523 let mut request =
25524 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25525 let edits = edits.clone();
25526 async move {
25527 Ok(lsp::CompletionItem {
25528 additional_text_edits: edits,
25529 ..Default::default()
25530 })
25531 }
25532 });
25533
25534 async move {
25535 request.next().await;
25536 }
25537}
25538
25539pub(crate) fn update_test_language_settings(
25540 cx: &mut TestAppContext,
25541 f: impl Fn(&mut AllLanguageSettingsContent),
25542) {
25543 cx.update(|cx| {
25544 SettingsStore::update_global(cx, |store, cx| {
25545 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25546 });
25547 });
25548}
25549
25550pub(crate) fn update_test_project_settings(
25551 cx: &mut TestAppContext,
25552 f: impl Fn(&mut ProjectSettingsContent),
25553) {
25554 cx.update(|cx| {
25555 SettingsStore::update_global(cx, |store, cx| {
25556 store.update_user_settings(cx, |settings| f(&mut settings.project));
25557 });
25558 });
25559}
25560
25561pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25562 cx.update(|cx| {
25563 assets::Assets.load_test_fonts(cx);
25564 let store = SettingsStore::test(cx);
25565 cx.set_global(store);
25566 theme::init(theme::LoadThemes::JustBase, cx);
25567 release_channel::init(SemanticVersion::default(), cx);
25568 client::init_settings(cx);
25569 language::init(cx);
25570 Project::init_settings(cx);
25571 workspace::init_settings(cx);
25572 crate::init(cx);
25573 });
25574 zlog::init_test();
25575 update_test_language_settings(cx, f);
25576}
25577
25578#[track_caller]
25579fn assert_hunk_revert(
25580 not_reverted_text_with_selections: &str,
25581 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25582 expected_reverted_text_with_selections: &str,
25583 base_text: &str,
25584 cx: &mut EditorLspTestContext,
25585) {
25586 cx.set_state(not_reverted_text_with_selections);
25587 cx.set_head_text(base_text);
25588 cx.executor().run_until_parked();
25589
25590 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25591 let snapshot = editor.snapshot(window, cx);
25592 let reverted_hunk_statuses = snapshot
25593 .buffer_snapshot()
25594 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25595 .map(|hunk| hunk.status().kind)
25596 .collect::<Vec<_>>();
25597
25598 editor.git_restore(&Default::default(), window, cx);
25599 reverted_hunk_statuses
25600 });
25601 cx.executor().run_until_parked();
25602 cx.assert_editor_state(expected_reverted_text_with_selections);
25603 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25604}
25605
25606#[gpui::test(iterations = 10)]
25607async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25608 init_test(cx, |_| {});
25609
25610 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25611 let counter = diagnostic_requests.clone();
25612
25613 let fs = FakeFs::new(cx.executor());
25614 fs.insert_tree(
25615 path!("/a"),
25616 json!({
25617 "first.rs": "fn main() { let a = 5; }",
25618 "second.rs": "// Test file",
25619 }),
25620 )
25621 .await;
25622
25623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25625 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25626
25627 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25628 language_registry.add(rust_lang());
25629 let mut fake_servers = language_registry.register_fake_lsp(
25630 "Rust",
25631 FakeLspAdapter {
25632 capabilities: lsp::ServerCapabilities {
25633 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25634 lsp::DiagnosticOptions {
25635 identifier: None,
25636 inter_file_dependencies: true,
25637 workspace_diagnostics: true,
25638 work_done_progress_options: Default::default(),
25639 },
25640 )),
25641 ..Default::default()
25642 },
25643 ..Default::default()
25644 },
25645 );
25646
25647 let editor = workspace
25648 .update(cx, |workspace, window, cx| {
25649 workspace.open_abs_path(
25650 PathBuf::from(path!("/a/first.rs")),
25651 OpenOptions::default(),
25652 window,
25653 cx,
25654 )
25655 })
25656 .unwrap()
25657 .await
25658 .unwrap()
25659 .downcast::<Editor>()
25660 .unwrap();
25661 let fake_server = fake_servers.next().await.unwrap();
25662 let server_id = fake_server.server.server_id();
25663 let mut first_request = fake_server
25664 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25665 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25666 let result_id = Some(new_result_id.to_string());
25667 assert_eq!(
25668 params.text_document.uri,
25669 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25670 );
25671 async move {
25672 Ok(lsp::DocumentDiagnosticReportResult::Report(
25673 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25674 related_documents: None,
25675 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25676 items: Vec::new(),
25677 result_id,
25678 },
25679 }),
25680 ))
25681 }
25682 });
25683
25684 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25685 project.update(cx, |project, cx| {
25686 let buffer_id = editor
25687 .read(cx)
25688 .buffer()
25689 .read(cx)
25690 .as_singleton()
25691 .expect("created a singleton buffer")
25692 .read(cx)
25693 .remote_id();
25694 let buffer_result_id = project
25695 .lsp_store()
25696 .read(cx)
25697 .result_id(server_id, buffer_id, cx);
25698 assert_eq!(expected, buffer_result_id);
25699 });
25700 };
25701
25702 ensure_result_id(None, cx);
25703 cx.executor().advance_clock(Duration::from_millis(60));
25704 cx.executor().run_until_parked();
25705 assert_eq!(
25706 diagnostic_requests.load(atomic::Ordering::Acquire),
25707 1,
25708 "Opening file should trigger diagnostic request"
25709 );
25710 first_request
25711 .next()
25712 .await
25713 .expect("should have sent the first diagnostics pull request");
25714 ensure_result_id(Some("1".to_string()), cx);
25715
25716 // Editing should trigger diagnostics
25717 editor.update_in(cx, |editor, window, cx| {
25718 editor.handle_input("2", window, cx)
25719 });
25720 cx.executor().advance_clock(Duration::from_millis(60));
25721 cx.executor().run_until_parked();
25722 assert_eq!(
25723 diagnostic_requests.load(atomic::Ordering::Acquire),
25724 2,
25725 "Editing should trigger diagnostic request"
25726 );
25727 ensure_result_id(Some("2".to_string()), cx);
25728
25729 // Moving cursor should not trigger diagnostic request
25730 editor.update_in(cx, |editor, window, cx| {
25731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25732 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25733 });
25734 });
25735 cx.executor().advance_clock(Duration::from_millis(60));
25736 cx.executor().run_until_parked();
25737 assert_eq!(
25738 diagnostic_requests.load(atomic::Ordering::Acquire),
25739 2,
25740 "Cursor movement should not trigger diagnostic request"
25741 );
25742 ensure_result_id(Some("2".to_string()), cx);
25743 // Multiple rapid edits should be debounced
25744 for _ in 0..5 {
25745 editor.update_in(cx, |editor, window, cx| {
25746 editor.handle_input("x", window, cx)
25747 });
25748 }
25749 cx.executor().advance_clock(Duration::from_millis(60));
25750 cx.executor().run_until_parked();
25751
25752 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25753 assert!(
25754 final_requests <= 4,
25755 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25756 );
25757 ensure_result_id(Some(final_requests.to_string()), cx);
25758}
25759
25760#[gpui::test]
25761async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25762 // Regression test for issue #11671
25763 // Previously, adding a cursor after moving multiple cursors would reset
25764 // the cursor count instead of adding to the existing cursors.
25765 init_test(cx, |_| {});
25766 let mut cx = EditorTestContext::new(cx).await;
25767
25768 // Create a simple buffer with cursor at start
25769 cx.set_state(indoc! {"
25770 ˇaaaa
25771 bbbb
25772 cccc
25773 dddd
25774 eeee
25775 ffff
25776 gggg
25777 hhhh"});
25778
25779 // Add 2 cursors below (so we have 3 total)
25780 cx.update_editor(|editor, window, cx| {
25781 editor.add_selection_below(&Default::default(), window, cx);
25782 editor.add_selection_below(&Default::default(), window, cx);
25783 });
25784
25785 // Verify we have 3 cursors
25786 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25787 assert_eq!(
25788 initial_count, 3,
25789 "Should have 3 cursors after adding 2 below"
25790 );
25791
25792 // Move down one line
25793 cx.update_editor(|editor, window, cx| {
25794 editor.move_down(&MoveDown, window, cx);
25795 });
25796
25797 // Add another cursor below
25798 cx.update_editor(|editor, window, cx| {
25799 editor.add_selection_below(&Default::default(), window, cx);
25800 });
25801
25802 // Should now have 4 cursors (3 original + 1 new)
25803 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25804 assert_eq!(
25805 final_count, 4,
25806 "Should have 4 cursors after moving and adding another"
25807 );
25808}
25809
25810#[gpui::test]
25811async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25812 init_test(cx, |_| {});
25813
25814 let mut cx = EditorTestContext::new(cx).await;
25815
25816 cx.set_state(indoc!(
25817 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25818 Second line here"#
25819 ));
25820
25821 cx.update_editor(|editor, window, cx| {
25822 // Enable soft wrapping with a narrow width to force soft wrapping and
25823 // confirm that more than 2 rows are being displayed.
25824 editor.set_wrap_width(Some(100.0.into()), cx);
25825 assert!(editor.display_text(cx).lines().count() > 2);
25826
25827 editor.add_selection_below(
25828 &AddSelectionBelow {
25829 skip_soft_wrap: true,
25830 },
25831 window,
25832 cx,
25833 );
25834
25835 assert_eq!(
25836 editor.selections.display_ranges(cx),
25837 &[
25838 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25839 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25840 ]
25841 );
25842
25843 editor.add_selection_above(
25844 &AddSelectionAbove {
25845 skip_soft_wrap: true,
25846 },
25847 window,
25848 cx,
25849 );
25850
25851 assert_eq!(
25852 editor.selections.display_ranges(cx),
25853 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25854 );
25855
25856 editor.add_selection_below(
25857 &AddSelectionBelow {
25858 skip_soft_wrap: false,
25859 },
25860 window,
25861 cx,
25862 );
25863
25864 assert_eq!(
25865 editor.selections.display_ranges(cx),
25866 &[
25867 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25868 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25869 ]
25870 );
25871
25872 editor.add_selection_above(
25873 &AddSelectionAbove {
25874 skip_soft_wrap: false,
25875 },
25876 window,
25877 cx,
25878 );
25879
25880 assert_eq!(
25881 editor.selections.display_ranges(cx),
25882 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25883 );
25884 });
25885}
25886
25887#[gpui::test(iterations = 10)]
25888async fn test_document_colors(cx: &mut TestAppContext) {
25889 let expected_color = Rgba {
25890 r: 0.33,
25891 g: 0.33,
25892 b: 0.33,
25893 a: 0.33,
25894 };
25895
25896 init_test(cx, |_| {});
25897
25898 let fs = FakeFs::new(cx.executor());
25899 fs.insert_tree(
25900 path!("/a"),
25901 json!({
25902 "first.rs": "fn main() { let a = 5; }",
25903 }),
25904 )
25905 .await;
25906
25907 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25908 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25909 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25910
25911 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25912 language_registry.add(rust_lang());
25913 let mut fake_servers = language_registry.register_fake_lsp(
25914 "Rust",
25915 FakeLspAdapter {
25916 capabilities: lsp::ServerCapabilities {
25917 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25918 ..lsp::ServerCapabilities::default()
25919 },
25920 name: "rust-analyzer",
25921 ..FakeLspAdapter::default()
25922 },
25923 );
25924 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25925 "Rust",
25926 FakeLspAdapter {
25927 capabilities: lsp::ServerCapabilities {
25928 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25929 ..lsp::ServerCapabilities::default()
25930 },
25931 name: "not-rust-analyzer",
25932 ..FakeLspAdapter::default()
25933 },
25934 );
25935
25936 let editor = workspace
25937 .update(cx, |workspace, window, cx| {
25938 workspace.open_abs_path(
25939 PathBuf::from(path!("/a/first.rs")),
25940 OpenOptions::default(),
25941 window,
25942 cx,
25943 )
25944 })
25945 .unwrap()
25946 .await
25947 .unwrap()
25948 .downcast::<Editor>()
25949 .unwrap();
25950 let fake_language_server = fake_servers.next().await.unwrap();
25951 let fake_language_server_without_capabilities =
25952 fake_servers_without_capabilities.next().await.unwrap();
25953 let requests_made = Arc::new(AtomicUsize::new(0));
25954 let closure_requests_made = Arc::clone(&requests_made);
25955 let mut color_request_handle = fake_language_server
25956 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25957 let requests_made = Arc::clone(&closure_requests_made);
25958 async move {
25959 assert_eq!(
25960 params.text_document.uri,
25961 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25962 );
25963 requests_made.fetch_add(1, atomic::Ordering::Release);
25964 Ok(vec![
25965 lsp::ColorInformation {
25966 range: lsp::Range {
25967 start: lsp::Position {
25968 line: 0,
25969 character: 0,
25970 },
25971 end: lsp::Position {
25972 line: 0,
25973 character: 1,
25974 },
25975 },
25976 color: lsp::Color {
25977 red: 0.33,
25978 green: 0.33,
25979 blue: 0.33,
25980 alpha: 0.33,
25981 },
25982 },
25983 lsp::ColorInformation {
25984 range: lsp::Range {
25985 start: lsp::Position {
25986 line: 0,
25987 character: 0,
25988 },
25989 end: lsp::Position {
25990 line: 0,
25991 character: 1,
25992 },
25993 },
25994 color: lsp::Color {
25995 red: 0.33,
25996 green: 0.33,
25997 blue: 0.33,
25998 alpha: 0.33,
25999 },
26000 },
26001 ])
26002 }
26003 });
26004
26005 let _handle = fake_language_server_without_capabilities
26006 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26007 panic!("Should not be called");
26008 });
26009 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26010 color_request_handle.next().await.unwrap();
26011 cx.run_until_parked();
26012 assert_eq!(
26013 1,
26014 requests_made.load(atomic::Ordering::Acquire),
26015 "Should query for colors once per editor open"
26016 );
26017 editor.update_in(cx, |editor, _, cx| {
26018 assert_eq!(
26019 vec![expected_color],
26020 extract_color_inlays(editor, cx),
26021 "Should have an initial inlay"
26022 );
26023 });
26024
26025 // opening another file in a split should not influence the LSP query counter
26026 workspace
26027 .update(cx, |workspace, window, cx| {
26028 assert_eq!(
26029 workspace.panes().len(),
26030 1,
26031 "Should have one pane with one editor"
26032 );
26033 workspace.move_item_to_pane_in_direction(
26034 &MoveItemToPaneInDirection {
26035 direction: SplitDirection::Right,
26036 focus: false,
26037 clone: true,
26038 },
26039 window,
26040 cx,
26041 );
26042 })
26043 .unwrap();
26044 cx.run_until_parked();
26045 workspace
26046 .update(cx, |workspace, _, cx| {
26047 let panes = workspace.panes();
26048 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26049 for pane in panes {
26050 let editor = pane
26051 .read(cx)
26052 .active_item()
26053 .and_then(|item| item.downcast::<Editor>())
26054 .expect("Should have opened an editor in each split");
26055 let editor_file = editor
26056 .read(cx)
26057 .buffer()
26058 .read(cx)
26059 .as_singleton()
26060 .expect("test deals with singleton buffers")
26061 .read(cx)
26062 .file()
26063 .expect("test buffese should have a file")
26064 .path();
26065 assert_eq!(
26066 editor_file.as_ref(),
26067 rel_path("first.rs"),
26068 "Both editors should be opened for the same file"
26069 )
26070 }
26071 })
26072 .unwrap();
26073
26074 cx.executor().advance_clock(Duration::from_millis(500));
26075 let save = editor.update_in(cx, |editor, window, cx| {
26076 editor.move_to_end(&MoveToEnd, window, cx);
26077 editor.handle_input("dirty", window, cx);
26078 editor.save(
26079 SaveOptions {
26080 format: true,
26081 autosave: true,
26082 },
26083 project.clone(),
26084 window,
26085 cx,
26086 )
26087 });
26088 save.await.unwrap();
26089
26090 color_request_handle.next().await.unwrap();
26091 cx.run_until_parked();
26092 assert_eq!(
26093 2,
26094 requests_made.load(atomic::Ordering::Acquire),
26095 "Should query for colors once per save (deduplicated) and once per formatting after save"
26096 );
26097
26098 drop(editor);
26099 let close = workspace
26100 .update(cx, |workspace, window, cx| {
26101 workspace.active_pane().update(cx, |pane, cx| {
26102 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26103 })
26104 })
26105 .unwrap();
26106 close.await.unwrap();
26107 let close = workspace
26108 .update(cx, |workspace, window, cx| {
26109 workspace.active_pane().update(cx, |pane, cx| {
26110 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26111 })
26112 })
26113 .unwrap();
26114 close.await.unwrap();
26115 assert_eq!(
26116 2,
26117 requests_made.load(atomic::Ordering::Acquire),
26118 "After saving and closing all editors, no extra requests should be made"
26119 );
26120 workspace
26121 .update(cx, |workspace, _, cx| {
26122 assert!(
26123 workspace.active_item(cx).is_none(),
26124 "Should close all editors"
26125 )
26126 })
26127 .unwrap();
26128
26129 workspace
26130 .update(cx, |workspace, window, cx| {
26131 workspace.active_pane().update(cx, |pane, cx| {
26132 pane.navigate_backward(&workspace::GoBack, window, cx);
26133 })
26134 })
26135 .unwrap();
26136 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26137 cx.run_until_parked();
26138 let editor = workspace
26139 .update(cx, |workspace, _, cx| {
26140 workspace
26141 .active_item(cx)
26142 .expect("Should have reopened the editor again after navigating back")
26143 .downcast::<Editor>()
26144 .expect("Should be an editor")
26145 })
26146 .unwrap();
26147
26148 assert_eq!(
26149 2,
26150 requests_made.load(atomic::Ordering::Acquire),
26151 "Cache should be reused on buffer close and reopen"
26152 );
26153 editor.update(cx, |editor, cx| {
26154 assert_eq!(
26155 vec![expected_color],
26156 extract_color_inlays(editor, cx),
26157 "Should have an initial inlay"
26158 );
26159 });
26160
26161 drop(color_request_handle);
26162 let closure_requests_made = Arc::clone(&requests_made);
26163 let mut empty_color_request_handle = fake_language_server
26164 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26165 let requests_made = Arc::clone(&closure_requests_made);
26166 async move {
26167 assert_eq!(
26168 params.text_document.uri,
26169 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26170 );
26171 requests_made.fetch_add(1, atomic::Ordering::Release);
26172 Ok(Vec::new())
26173 }
26174 });
26175 let save = editor.update_in(cx, |editor, window, cx| {
26176 editor.move_to_end(&MoveToEnd, window, cx);
26177 editor.handle_input("dirty_again", window, cx);
26178 editor.save(
26179 SaveOptions {
26180 format: false,
26181 autosave: true,
26182 },
26183 project.clone(),
26184 window,
26185 cx,
26186 )
26187 });
26188 save.await.unwrap();
26189
26190 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26191 empty_color_request_handle.next().await.unwrap();
26192 cx.run_until_parked();
26193 assert_eq!(
26194 3,
26195 requests_made.load(atomic::Ordering::Acquire),
26196 "Should query for colors once per save only, as formatting was not requested"
26197 );
26198 editor.update(cx, |editor, cx| {
26199 assert_eq!(
26200 Vec::<Rgba>::new(),
26201 extract_color_inlays(editor, cx),
26202 "Should clear all colors when the server returns an empty response"
26203 );
26204 });
26205}
26206
26207#[gpui::test]
26208async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26209 init_test(cx, |_| {});
26210 let (editor, cx) = cx.add_window_view(Editor::single_line);
26211 editor.update_in(cx, |editor, window, cx| {
26212 editor.set_text("oops\n\nwow\n", window, cx)
26213 });
26214 cx.run_until_parked();
26215 editor.update(cx, |editor, cx| {
26216 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26217 });
26218 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26219 cx.run_until_parked();
26220 editor.update(cx, |editor, cx| {
26221 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26222 });
26223}
26224
26225#[gpui::test]
26226async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26227 init_test(cx, |_| {});
26228
26229 cx.update(|cx| {
26230 register_project_item::<Editor>(cx);
26231 });
26232
26233 let fs = FakeFs::new(cx.executor());
26234 fs.insert_tree("/root1", json!({})).await;
26235 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26236 .await;
26237
26238 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26239 let (workspace, cx) =
26240 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26241
26242 let worktree_id = project.update(cx, |project, cx| {
26243 project.worktrees(cx).next().unwrap().read(cx).id()
26244 });
26245
26246 let handle = workspace
26247 .update_in(cx, |workspace, window, cx| {
26248 let project_path = (worktree_id, rel_path("one.pdf"));
26249 workspace.open_path(project_path, None, true, window, cx)
26250 })
26251 .await
26252 .unwrap();
26253
26254 assert_eq!(
26255 handle.to_any().entity_type(),
26256 TypeId::of::<InvalidItemView>()
26257 );
26258}
26259
26260#[gpui::test]
26261async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26262 init_test(cx, |_| {});
26263
26264 let language = Arc::new(Language::new(
26265 LanguageConfig::default(),
26266 Some(tree_sitter_rust::LANGUAGE.into()),
26267 ));
26268
26269 // Test hierarchical sibling navigation
26270 let text = r#"
26271 fn outer() {
26272 if condition {
26273 let a = 1;
26274 }
26275 let b = 2;
26276 }
26277
26278 fn another() {
26279 let c = 3;
26280 }
26281 "#;
26282
26283 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26285 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26286
26287 // Wait for parsing to complete
26288 editor
26289 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26290 .await;
26291
26292 editor.update_in(cx, |editor, window, cx| {
26293 // Start by selecting "let a = 1;" inside the if block
26294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26295 s.select_display_ranges([
26296 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26297 ]);
26298 });
26299
26300 let initial_selection = editor.selections.display_ranges(cx);
26301 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26302
26303 // Test select next sibling - should move up levels to find the next sibling
26304 // Since "let a = 1;" has no siblings in the if block, it should move up
26305 // to find "let b = 2;" which is a sibling of the if block
26306 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26307 let next_selection = editor.selections.display_ranges(cx);
26308
26309 // Should have a selection and it should be different from the initial
26310 assert_eq!(
26311 next_selection.len(),
26312 1,
26313 "Should have one selection after next"
26314 );
26315 assert_ne!(
26316 next_selection[0], initial_selection[0],
26317 "Next sibling selection should be different"
26318 );
26319
26320 // Test hierarchical navigation by going to the end of the current function
26321 // and trying to navigate to the next function
26322 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26323 s.select_display_ranges([
26324 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26325 ]);
26326 });
26327
26328 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26329 let function_next_selection = editor.selections.display_ranges(cx);
26330
26331 // Should move to the next function
26332 assert_eq!(
26333 function_next_selection.len(),
26334 1,
26335 "Should have one selection after function next"
26336 );
26337
26338 // Test select previous sibling navigation
26339 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26340 let prev_selection = editor.selections.display_ranges(cx);
26341
26342 // Should have a selection and it should be different
26343 assert_eq!(
26344 prev_selection.len(),
26345 1,
26346 "Should have one selection after prev"
26347 );
26348 assert_ne!(
26349 prev_selection[0], function_next_selection[0],
26350 "Previous sibling selection should be different from next"
26351 );
26352 });
26353}
26354
26355#[gpui::test]
26356async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26357 init_test(cx, |_| {});
26358
26359 let mut cx = EditorTestContext::new(cx).await;
26360 cx.set_state(
26361 "let ˇvariable = 42;
26362let another = variable + 1;
26363let result = variable * 2;",
26364 );
26365
26366 // Set up document highlights manually (simulating LSP response)
26367 cx.update_editor(|editor, _window, cx| {
26368 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26369
26370 // Create highlights for "variable" occurrences
26371 let highlight_ranges = [
26372 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26373 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26374 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26375 ];
26376
26377 let anchor_ranges: Vec<_> = highlight_ranges
26378 .iter()
26379 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26380 .collect();
26381
26382 editor.highlight_background::<DocumentHighlightRead>(
26383 &anchor_ranges,
26384 |theme| theme.colors().editor_document_highlight_read_background,
26385 cx,
26386 );
26387 });
26388
26389 // Go to next highlight - should move to second "variable"
26390 cx.update_editor(|editor, window, cx| {
26391 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26392 });
26393 cx.assert_editor_state(
26394 "let variable = 42;
26395let another = ˇvariable + 1;
26396let result = variable * 2;",
26397 );
26398
26399 // Go to next highlight - should move to third "variable"
26400 cx.update_editor(|editor, window, cx| {
26401 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26402 });
26403 cx.assert_editor_state(
26404 "let variable = 42;
26405let another = variable + 1;
26406let result = ˇvariable * 2;",
26407 );
26408
26409 // Go to next highlight - should stay at third "variable" (no wrap-around)
26410 cx.update_editor(|editor, window, cx| {
26411 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26412 });
26413 cx.assert_editor_state(
26414 "let variable = 42;
26415let another = variable + 1;
26416let result = ˇvariable * 2;",
26417 );
26418
26419 // Now test going backwards from third position
26420 cx.update_editor(|editor, window, cx| {
26421 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26422 });
26423 cx.assert_editor_state(
26424 "let variable = 42;
26425let another = ˇvariable + 1;
26426let result = variable * 2;",
26427 );
26428
26429 // Go to previous highlight - should move to first "variable"
26430 cx.update_editor(|editor, window, cx| {
26431 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26432 });
26433 cx.assert_editor_state(
26434 "let ˇvariable = 42;
26435let another = variable + 1;
26436let result = variable * 2;",
26437 );
26438
26439 // Go to previous highlight - should stay on first "variable"
26440 cx.update_editor(|editor, window, cx| {
26441 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26442 });
26443 cx.assert_editor_state(
26444 "let ˇvariable = 42;
26445let another = variable + 1;
26446let result = variable * 2;",
26447 );
26448}
26449
26450#[gpui::test]
26451async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26452 cx: &mut gpui::TestAppContext,
26453) {
26454 init_test(cx, |_| {});
26455
26456 let url = "https://zed.dev";
26457
26458 let markdown_language = Arc::new(Language::new(
26459 LanguageConfig {
26460 name: "Markdown".into(),
26461 ..LanguageConfig::default()
26462 },
26463 None,
26464 ));
26465
26466 let mut cx = EditorTestContext::new(cx).await;
26467 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26468 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26469
26470 cx.update_editor(|editor, window, cx| {
26471 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26472 editor.paste(&Paste, window, cx);
26473 });
26474
26475 cx.assert_editor_state(&format!(
26476 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26477 ));
26478}
26479
26480#[gpui::test]
26481async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26482 cx: &mut gpui::TestAppContext,
26483) {
26484 init_test(cx, |_| {});
26485
26486 let url = "https://zed.dev";
26487
26488 let markdown_language = Arc::new(Language::new(
26489 LanguageConfig {
26490 name: "Markdown".into(),
26491 ..LanguageConfig::default()
26492 },
26493 None,
26494 ));
26495
26496 let mut cx = EditorTestContext::new(cx).await;
26497 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26498 cx.set_state(&format!(
26499 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26500 ));
26501
26502 cx.update_editor(|editor, window, cx| {
26503 editor.copy(&Copy, window, cx);
26504 });
26505
26506 cx.set_state(&format!(
26507 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26508 ));
26509
26510 cx.update_editor(|editor, window, cx| {
26511 editor.paste(&Paste, window, cx);
26512 });
26513
26514 cx.assert_editor_state(&format!(
26515 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26516 ));
26517}
26518
26519#[gpui::test]
26520async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26521 cx: &mut gpui::TestAppContext,
26522) {
26523 init_test(cx, |_| {});
26524
26525 let url = "https://zed.dev";
26526
26527 let markdown_language = Arc::new(Language::new(
26528 LanguageConfig {
26529 name: "Markdown".into(),
26530 ..LanguageConfig::default()
26531 },
26532 None,
26533 ));
26534
26535 let mut cx = EditorTestContext::new(cx).await;
26536 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26537 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26538
26539 cx.update_editor(|editor, window, cx| {
26540 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26541 editor.paste(&Paste, window, cx);
26542 });
26543
26544 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26545}
26546
26547#[gpui::test]
26548async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26549 cx: &mut gpui::TestAppContext,
26550) {
26551 init_test(cx, |_| {});
26552
26553 let text = "Awesome";
26554
26555 let markdown_language = Arc::new(Language::new(
26556 LanguageConfig {
26557 name: "Markdown".into(),
26558 ..LanguageConfig::default()
26559 },
26560 None,
26561 ));
26562
26563 let mut cx = EditorTestContext::new(cx).await;
26564 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26565 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26566
26567 cx.update_editor(|editor, window, cx| {
26568 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26569 editor.paste(&Paste, window, cx);
26570 });
26571
26572 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26573}
26574
26575#[gpui::test]
26576async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26577 cx: &mut gpui::TestAppContext,
26578) {
26579 init_test(cx, |_| {});
26580
26581 let url = "https://zed.dev";
26582
26583 let markdown_language = Arc::new(Language::new(
26584 LanguageConfig {
26585 name: "Rust".into(),
26586 ..LanguageConfig::default()
26587 },
26588 None,
26589 ));
26590
26591 let mut cx = EditorTestContext::new(cx).await;
26592 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26593 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26594
26595 cx.update_editor(|editor, window, cx| {
26596 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26597 editor.paste(&Paste, window, cx);
26598 });
26599
26600 cx.assert_editor_state(&format!(
26601 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26602 ));
26603}
26604
26605#[gpui::test]
26606async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26607 cx: &mut TestAppContext,
26608) {
26609 init_test(cx, |_| {});
26610
26611 let url = "https://zed.dev";
26612
26613 let markdown_language = Arc::new(Language::new(
26614 LanguageConfig {
26615 name: "Markdown".into(),
26616 ..LanguageConfig::default()
26617 },
26618 None,
26619 ));
26620
26621 let (editor, cx) = cx.add_window_view(|window, cx| {
26622 let multi_buffer = MultiBuffer::build_multi(
26623 [
26624 ("this will embed -> link", vec![Point::row_range(0..1)]),
26625 ("this will replace -> link", vec![Point::row_range(0..1)]),
26626 ],
26627 cx,
26628 );
26629 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26631 s.select_ranges(vec![
26632 Point::new(0, 19)..Point::new(0, 23),
26633 Point::new(1, 21)..Point::new(1, 25),
26634 ])
26635 });
26636 let first_buffer_id = multi_buffer
26637 .read(cx)
26638 .excerpt_buffer_ids()
26639 .into_iter()
26640 .next()
26641 .unwrap();
26642 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26643 first_buffer.update(cx, |buffer, cx| {
26644 buffer.set_language(Some(markdown_language.clone()), cx);
26645 });
26646
26647 editor
26648 });
26649 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26650
26651 cx.update_editor(|editor, window, cx| {
26652 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26653 editor.paste(&Paste, window, cx);
26654 });
26655
26656 cx.assert_editor_state(&format!(
26657 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26658 ));
26659}
26660
26661#[gpui::test]
26662async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26663 init_test(cx, |_| {});
26664
26665 let fs = FakeFs::new(cx.executor());
26666 fs.insert_tree(
26667 path!("/project"),
26668 json!({
26669 "first.rs": "# First Document\nSome content here.",
26670 "second.rs": "Plain text content for second file.",
26671 }),
26672 )
26673 .await;
26674
26675 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26676 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26677 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26678
26679 let language = rust_lang();
26680 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26681 language_registry.add(language.clone());
26682 let mut fake_servers = language_registry.register_fake_lsp(
26683 "Rust",
26684 FakeLspAdapter {
26685 ..FakeLspAdapter::default()
26686 },
26687 );
26688
26689 let buffer1 = project
26690 .update(cx, |project, cx| {
26691 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26692 })
26693 .await
26694 .unwrap();
26695 let buffer2 = project
26696 .update(cx, |project, cx| {
26697 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26698 })
26699 .await
26700 .unwrap();
26701
26702 let multi_buffer = cx.new(|cx| {
26703 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26704 multi_buffer.set_excerpts_for_path(
26705 PathKey::for_buffer(&buffer1, cx),
26706 buffer1.clone(),
26707 [Point::zero()..buffer1.read(cx).max_point()],
26708 3,
26709 cx,
26710 );
26711 multi_buffer.set_excerpts_for_path(
26712 PathKey::for_buffer(&buffer2, cx),
26713 buffer2.clone(),
26714 [Point::zero()..buffer1.read(cx).max_point()],
26715 3,
26716 cx,
26717 );
26718 multi_buffer
26719 });
26720
26721 let (editor, cx) = cx.add_window_view(|window, cx| {
26722 Editor::new(
26723 EditorMode::full(),
26724 multi_buffer,
26725 Some(project.clone()),
26726 window,
26727 cx,
26728 )
26729 });
26730
26731 let fake_language_server = fake_servers.next().await.unwrap();
26732
26733 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26734
26735 let save = editor.update_in(cx, |editor, window, cx| {
26736 assert!(editor.is_dirty(cx));
26737
26738 editor.save(
26739 SaveOptions {
26740 format: true,
26741 autosave: true,
26742 },
26743 project,
26744 window,
26745 cx,
26746 )
26747 });
26748 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26749 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26750 let mut done_edit_rx = Some(done_edit_rx);
26751 let mut start_edit_tx = Some(start_edit_tx);
26752
26753 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26754 start_edit_tx.take().unwrap().send(()).unwrap();
26755 let done_edit_rx = done_edit_rx.take().unwrap();
26756 async move {
26757 done_edit_rx.await.unwrap();
26758 Ok(None)
26759 }
26760 });
26761
26762 start_edit_rx.await.unwrap();
26763 buffer2
26764 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26765 .unwrap();
26766
26767 done_edit_tx.send(()).unwrap();
26768
26769 save.await.unwrap();
26770 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26771}
26772
26773#[track_caller]
26774fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26775 editor
26776 .all_inlays(cx)
26777 .into_iter()
26778 .filter_map(|inlay| inlay.get_color())
26779 .map(Rgba::from)
26780 .collect()
26781}
26782
26783#[gpui::test]
26784fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26785 init_test(cx, |_| {});
26786
26787 let editor = cx.add_window(|window, cx| {
26788 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26789 build_editor(buffer, window, cx)
26790 });
26791
26792 editor
26793 .update(cx, |editor, window, cx| {
26794 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26795 s.select_display_ranges([
26796 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26797 ])
26798 });
26799
26800 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26801
26802 assert_eq!(
26803 editor.display_text(cx),
26804 "line1\nline2\nline2",
26805 "Duplicating last line upward should create duplicate above, not on same line"
26806 );
26807
26808 assert_eq!(
26809 editor.selections.display_ranges(cx),
26810 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26811 "Selection should remain on the original line"
26812 );
26813 })
26814 .unwrap();
26815}
26816
26817#[gpui::test]
26818async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26819 init_test(cx, |_| {});
26820
26821 let mut cx = EditorTestContext::new(cx).await;
26822
26823 cx.set_state("line1\nline2ˇ");
26824
26825 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26826
26827 let clipboard_text = cx
26828 .read_from_clipboard()
26829 .and_then(|item| item.text().as_deref().map(str::to_string));
26830
26831 assert_eq!(
26832 clipboard_text,
26833 Some("line2\n".to_string()),
26834 "Copying a line without trailing newline should include a newline"
26835 );
26836
26837 cx.set_state("line1\nˇ");
26838
26839 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26840
26841 cx.assert_editor_state("line1\nline2\nˇ");
26842}
26843
26844#[gpui::test]
26845async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26846 init_test(cx, |_| {});
26847
26848 let mut cx = EditorTestContext::new(cx).await;
26849
26850 cx.set_state("line1\nline2ˇ");
26851 cx.update_editor(|e, window, cx| {
26852 e.set_mode(EditorMode::SingleLine);
26853 assert!(e.key_context(window, cx).contains("end_of_input"));
26854 });
26855 cx.set_state("ˇline1\nline2");
26856 cx.update_editor(|e, window, cx| {
26857 assert!(!e.key_context(window, cx).contains("end_of_input"));
26858 });
26859 cx.set_state("line1ˇ\nline2");
26860 cx.update_editor(|e, window, cx| {
26861 assert!(!e.key_context(window, cx).contains("end_of_input"));
26862 });
26863}