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 lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::LspSettings,
42};
43use serde_json::{self, json};
44use settings::{
45 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
46 ProjectSettingsContent,
47};
48use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
49use std::{
50 iter,
51 sync::atomic::{self, AtomicUsize},
52};
53use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
54use text::ToPoint as _;
55use unindent::Unindent;
56use util::{
57 assert_set_eq, path,
58 rel_path::rel_path,
59 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
60 uri,
61};
62use workspace::{
63 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
64 OpenOptions, ViewId,
65 invalid_buffer_view::InvalidBufferView,
66 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
67 register_project_item,
68};
69
70#[gpui::test]
71fn test_edit_events(cx: &mut TestAppContext) {
72 init_test(cx, |_| {});
73
74 let buffer = cx.new(|cx| {
75 let mut buffer = language::Buffer::local("123456", cx);
76 buffer.set_group_interval(Duration::from_secs(1));
77 buffer
78 });
79
80 let events = Rc::new(RefCell::new(Vec::new()));
81 let editor1 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 let entity = cx.entity();
85 cx.subscribe_in(
86 &entity,
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor1", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 let editor2 = cx.add_window({
102 let events = events.clone();
103 |window, cx| {
104 cx.subscribe_in(
105 &cx.entity(),
106 window,
107 move |_, _, event: &EditorEvent, _, _| match event {
108 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
109 EditorEvent::BufferEdited => {
110 events.borrow_mut().push(("editor2", "buffer edited"))
111 }
112 _ => {}
113 },
114 )
115 .detach();
116 Editor::for_buffer(buffer.clone(), None, window, cx)
117 }
118 });
119
120 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
121
122 // Mutating editor 1 will emit an `Edited` event only for that editor.
123 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor1", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Mutating editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Undoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Redoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Undoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Redoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // No event is emitted when the mutation is a no-op.
189 _ = editor2.update(cx, |editor, window, cx| {
190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
191 s.select_ranges([0..0])
192 });
193
194 editor.backspace(&Backspace, window, cx);
195 });
196 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
197}
198
199#[gpui::test]
200fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
201 init_test(cx, |_| {});
202
203 let mut now = Instant::now();
204 let group_interval = Duration::from_millis(1);
205 let buffer = cx.new(|cx| {
206 let mut buf = language::Buffer::local("123456", cx);
207 buf.set_group_interval(group_interval);
208 buf
209 });
210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
211 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
212
213 _ = editor.update(cx, |editor, window, cx| {
214 editor.start_transaction_at(now, window, cx);
215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
216 s.select_ranges([2..4])
217 });
218
219 editor.insert("cd", window, cx);
220 editor.end_transaction_at(now, cx);
221 assert_eq!(editor.text(cx), "12cd56");
222 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
223
224 editor.start_transaction_at(now, window, cx);
225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
226 s.select_ranges([4..5])
227 });
228 editor.insert("e", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
232
233 now += group_interval + Duration::from_millis(1);
234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
235 s.select_ranges([2..2])
236 });
237
238 // Simulate an edit in another editor
239 buffer.update(cx, |buffer, cx| {
240 buffer.start_transaction_at(now, cx);
241 buffer.edit([(0..1, "a")], None, cx);
242 buffer.edit([(1..1, "b")], None, cx);
243 buffer.end_transaction_at(now, cx);
244 });
245
246 assert_eq!(editor.text(cx), "ab2cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
248
249 // Last transaction happened past the group interval in a different editor.
250 // Undo it individually and don't restore selections.
251 editor.undo(&Undo, window, cx);
252 assert_eq!(editor.text(cx), "12cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
254
255 // First two transactions happened within the group interval in this editor.
256 // Undo them together and restore selections.
257 editor.undo(&Undo, window, cx);
258 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
259 assert_eq!(editor.text(cx), "123456");
260 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
261
262 // Redo the first two transactions together.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "12cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
266
267 // Redo the last transaction on its own.
268 editor.redo(&Redo, window, cx);
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
271
272 // Test empty transactions.
273 editor.start_transaction_at(now, window, cx);
274 editor.end_transaction_at(now, cx);
275 editor.undo(&Undo, window, cx);
276 assert_eq!(editor.text(cx), "12cde6");
277 });
278}
279
280#[gpui::test]
281fn test_ime_composition(cx: &mut TestAppContext) {
282 init_test(cx, |_| {});
283
284 let buffer = cx.new(|cx| {
285 let mut buffer = language::Buffer::local("abcde", cx);
286 // Ensure automatic grouping doesn't occur.
287 buffer.set_group_interval(Duration::ZERO);
288 buffer
289 });
290
291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
292 cx.add_window(|window, cx| {
293 let mut editor = build_editor(buffer.clone(), window, cx);
294
295 // Start a new IME composition.
296 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
297 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
299 assert_eq!(editor.text(cx), "äbcde");
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Finalize IME composition.
306 editor.replace_text_in_range(None, "ā", window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // IME composition edits are grouped and are undone/redone at once.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "abcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314 editor.redo(&Default::default(), window, cx);
315 assert_eq!(editor.text(cx), "ābcde");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition.
319 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
320 assert_eq!(
321 editor.marked_text_ranges(cx),
322 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
323 );
324
325 // Undoing during an IME composition cancels it.
326 editor.undo(&Default::default(), window, cx);
327 assert_eq!(editor.text(cx), "ābcde");
328 assert_eq!(editor.marked_text_ranges(cx), None);
329
330 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
331 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
332 assert_eq!(editor.text(cx), "ābcdè");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
336 );
337
338 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
339 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
340 assert_eq!(editor.text(cx), "ābcdę");
341 assert_eq!(editor.marked_text_ranges(cx), None);
342
343 // Start a new IME composition with multiple cursors.
344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
345 s.select_ranges([
346 OffsetUtf16(1)..OffsetUtf16(1),
347 OffsetUtf16(3)..OffsetUtf16(3),
348 OffsetUtf16(5)..OffsetUtf16(5),
349 ])
350 });
351 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
352 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
353 assert_eq!(
354 editor.marked_text_ranges(cx),
355 Some(vec![
356 OffsetUtf16(0)..OffsetUtf16(3),
357 OffsetUtf16(4)..OffsetUtf16(7),
358 OffsetUtf16(8)..OffsetUtf16(11)
359 ])
360 );
361
362 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
363 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
364 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
365 assert_eq!(
366 editor.marked_text_ranges(cx),
367 Some(vec![
368 OffsetUtf16(1)..OffsetUtf16(2),
369 OffsetUtf16(5)..OffsetUtf16(6),
370 OffsetUtf16(9)..OffsetUtf16(10)
371 ])
372 );
373
374 // Finalize IME composition with multiple cursors.
375 editor.replace_text_in_range(Some(9..10), "2", window, cx);
376 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
377 assert_eq!(editor.marked_text_ranges(cx), None);
378
379 editor
380 });
381}
382
383#[gpui::test]
384fn test_selection_with_mouse(cx: &mut TestAppContext) {
385 init_test(cx, |_| {});
386
387 let editor = cx.add_window(|window, cx| {
388 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
389 build_editor(buffer, window, cx)
390 });
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
394 });
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(3), 3),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(1), 1),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.end_selection(window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(3), 3),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
452 );
453
454 _ = editor.update(cx, |editor, window, cx| {
455 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
456 editor.update_selection(
457 DisplayPoint::new(DisplayRow(0), 0),
458 0,
459 gpui::Point::<f32>::default(),
460 window,
461 cx,
462 );
463 });
464
465 assert_eq!(
466 editor
467 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
468 .unwrap(),
469 [
470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
471 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
472 ]
473 );
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 assert_eq!(
480 editor
481 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
482 .unwrap(),
483 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
484 );
485}
486
487#[gpui::test]
488fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
489 init_test(cx, |_| {});
490
491 let editor = cx.add_window(|window, cx| {
492 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
493 build_editor(buffer, window, cx)
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
518 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.end_selection(window, cx);
528 });
529
530 assert_eq!(
531 editor
532 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
533 .unwrap(),
534 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
535 );
536}
537
538#[gpui::test]
539fn test_canceling_pending_selection(cx: &mut TestAppContext) {
540 init_test(cx, |_| {});
541
542 let editor = cx.add_window(|window, cx| {
543 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
544 build_editor(buffer, window, cx)
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
549 assert_eq!(
550 editor.selections.display_ranges(cx),
551 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
552 );
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(3), 3),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.cancel(&Cancel, window, cx);
571 editor.update_selection(
572 DisplayPoint::new(DisplayRow(1), 1),
573 0,
574 gpui::Point::<f32>::default(),
575 window,
576 cx,
577 );
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
581 );
582 });
583}
584
585#[gpui::test]
586fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600
601 editor.move_down(&Default::default(), window, cx);
602 assert_eq!(
603 editor.selections.display_ranges(cx),
604 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
605 );
606
607 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
608 assert_eq!(
609 editor.selections.display_ranges(cx),
610 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
611 );
612
613 editor.move_up(&Default::default(), window, cx);
614 assert_eq!(
615 editor.selections.display_ranges(cx),
616 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
617 );
618 });
619}
620
621#[gpui::test]
622fn test_extending_selection(cx: &mut TestAppContext) {
623 init_test(cx, |_| {});
624
625 let editor = cx.add_window(|window, cx| {
626 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
627 build_editor(buffer, window, cx)
628 });
629
630 _ = editor.update(cx, |editor, window, cx| {
631 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
632 editor.end_selection(window, cx);
633 assert_eq!(
634 editor.selections.display_ranges(cx),
635 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
636 );
637
638 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
639 editor.end_selection(window, cx);
640 assert_eq!(
641 editor.selections.display_ranges(cx),
642 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
643 );
644
645 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
646 editor.end_selection(window, cx);
647 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
648 assert_eq!(
649 editor.selections.display_ranges(cx),
650 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
651 );
652
653 editor.update_selection(
654 DisplayPoint::new(DisplayRow(0), 1),
655 0,
656 gpui::Point::<f32>::default(),
657 window,
658 cx,
659 );
660 editor.end_selection(window, cx);
661 assert_eq!(
662 editor.selections.display_ranges(cx),
663 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
664 );
665
666 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
667 editor.end_selection(window, cx);
668 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
669 editor.end_selection(window, cx);
670 assert_eq!(
671 editor.selections.display_ranges(cx),
672 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
673 );
674
675 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
676 assert_eq!(
677 editor.selections.display_ranges(cx),
678 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
679 );
680
681 editor.update_selection(
682 DisplayPoint::new(DisplayRow(0), 6),
683 0,
684 gpui::Point::<f32>::default(),
685 window,
686 cx,
687 );
688 assert_eq!(
689 editor.selections.display_ranges(cx),
690 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
691 );
692
693 editor.update_selection(
694 DisplayPoint::new(DisplayRow(0), 1),
695 0,
696 gpui::Point::<f32>::default(),
697 window,
698 cx,
699 );
700 editor.end_selection(window, cx);
701 assert_eq!(
702 editor.selections.display_ranges(cx),
703 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
704 );
705 });
706}
707
708#[gpui::test]
709fn test_clone(cx: &mut TestAppContext) {
710 init_test(cx, |_| {});
711
712 let (text, selection_ranges) = marked_text_ranges(
713 indoc! {"
714 one
715 two
716 threeˇ
717 four
718 fiveˇ
719 "},
720 true,
721 );
722
723 let editor = cx.add_window(|window, cx| {
724 let buffer = MultiBuffer::build_simple(&text, cx);
725 build_editor(buffer, window, cx)
726 });
727
728 _ = editor.update(cx, |editor, window, cx| {
729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
730 s.select_ranges(selection_ranges.clone())
731 });
732 editor.fold_creases(
733 vec![
734 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
735 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
736 ],
737 true,
738 window,
739 cx,
740 );
741 });
742
743 let cloned_editor = editor
744 .update(cx, |editor, _, cx| {
745 cx.open_window(Default::default(), |window, cx| {
746 cx.new(|cx| editor.clone(window, cx))
747 })
748 })
749 .unwrap()
750 .unwrap();
751
752 let snapshot = editor
753 .update(cx, |e, window, cx| e.snapshot(window, cx))
754 .unwrap();
755 let cloned_snapshot = cloned_editor
756 .update(cx, |e, window, cx| e.snapshot(window, cx))
757 .unwrap();
758
759 assert_eq!(
760 cloned_editor
761 .update(cx, |e, _, cx| e.display_text(cx))
762 .unwrap(),
763 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
764 );
765 assert_eq!(
766 cloned_snapshot
767 .folds_in_range(0..text.len())
768 .collect::<Vec<_>>(),
769 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
770 );
771 assert_set_eq!(
772 cloned_editor
773 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
774 .unwrap(),
775 editor
776 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
777 .unwrap()
778 );
779 assert_set_eq!(
780 cloned_editor
781 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
782 .unwrap(),
783 editor
784 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
785 .unwrap()
786 );
787}
788
789#[gpui::test]
790async fn test_navigation_history(cx: &mut TestAppContext) {
791 init_test(cx, |_| {});
792
793 use workspace::item::Item;
794
795 let fs = FakeFs::new(cx.executor());
796 let project = Project::test(fs, [], cx).await;
797 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
798 let pane = workspace
799 .update(cx, |workspace, _, _| workspace.active_pane().clone())
800 .unwrap();
801
802 _ = workspace.update(cx, |_v, window, cx| {
803 cx.new(|cx| {
804 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
805 let mut editor = build_editor(buffer, window, cx);
806 let handle = cx.entity();
807 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
808
809 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
810 editor.nav_history.as_mut().unwrap().pop_backward(cx)
811 }
812
813 // Move the cursor a small distance.
814 // Nothing is added to the navigation history.
815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
816 s.select_display_ranges([
817 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
818 ])
819 });
820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
821 s.select_display_ranges([
822 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
823 ])
824 });
825 assert!(pop_history(&mut editor, cx).is_none());
826
827 // Move the cursor a large distance.
828 // The history can jump back to the previous position.
829 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
830 s.select_display_ranges([
831 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
832 ])
833 });
834 let nav_entry = pop_history(&mut editor, cx).unwrap();
835 editor.navigate(nav_entry.data.unwrap(), window, cx);
836 assert_eq!(nav_entry.item.id(), cx.entity_id());
837 assert_eq!(
838 editor.selections.display_ranges(cx),
839 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
840 );
841 assert!(pop_history(&mut editor, cx).is_none());
842
843 // Move the cursor a small distance via the mouse.
844 // Nothing is added to the navigation history.
845 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
846 editor.end_selection(window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
850 );
851 assert!(pop_history(&mut editor, cx).is_none());
852
853 // Move the cursor a large distance via the mouse.
854 // The history can jump back to the previous position.
855 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
856 editor.end_selection(window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
860 );
861 let nav_entry = pop_history(&mut editor, cx).unwrap();
862 editor.navigate(nav_entry.data.unwrap(), window, cx);
863 assert_eq!(nav_entry.item.id(), cx.entity_id());
864 assert_eq!(
865 editor.selections.display_ranges(cx),
866 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
867 );
868 assert!(pop_history(&mut editor, cx).is_none());
869
870 // Set scroll position to check later
871 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
872 let original_scroll_position = editor.scroll_manager.anchor();
873
874 // Jump to the end of the document and adjust scroll
875 editor.move_to_end(&MoveToEnd, window, cx);
876 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
877 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
878
879 let nav_entry = pop_history(&mut editor, cx).unwrap();
880 editor.navigate(nav_entry.data.unwrap(), window, cx);
881 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
882
883 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
884 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
885 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
886 let invalid_point = Point::new(9999, 0);
887 editor.navigate(
888 Box::new(NavigationData {
889 cursor_anchor: invalid_anchor,
890 cursor_position: invalid_point,
891 scroll_anchor: ScrollAnchor {
892 anchor: invalid_anchor,
893 offset: Default::default(),
894 },
895 scroll_top_row: invalid_point.row,
896 }),
897 window,
898 cx,
899 );
900 assert_eq!(
901 editor.selections.display_ranges(cx),
902 &[editor.max_point(cx)..editor.max_point(cx)]
903 );
904 assert_eq!(
905 editor.scroll_position(cx),
906 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
907 );
908
909 editor
910 })
911 });
912}
913
914#[gpui::test]
915fn test_cancel(cx: &mut TestAppContext) {
916 init_test(cx, |_| {});
917
918 let editor = cx.add_window(|window, cx| {
919 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
920 build_editor(buffer, window, cx)
921 });
922
923 _ = editor.update(cx, |editor, window, cx| {
924 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
925 editor.update_selection(
926 DisplayPoint::new(DisplayRow(1), 1),
927 0,
928 gpui::Point::<f32>::default(),
929 window,
930 cx,
931 );
932 editor.end_selection(window, cx);
933
934 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
935 editor.update_selection(
936 DisplayPoint::new(DisplayRow(0), 3),
937 0,
938 gpui::Point::<f32>::default(),
939 window,
940 cx,
941 );
942 editor.end_selection(window, cx);
943 assert_eq!(
944 editor.selections.display_ranges(cx),
945 [
946 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
947 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
948 ]
949 );
950 });
951
952 _ = editor.update(cx, |editor, window, cx| {
953 editor.cancel(&Cancel, window, cx);
954 assert_eq!(
955 editor.selections.display_ranges(cx),
956 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
957 );
958 });
959
960 _ = editor.update(cx, |editor, window, cx| {
961 editor.cancel(&Cancel, window, cx);
962 assert_eq!(
963 editor.selections.display_ranges(cx),
964 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
965 );
966 });
967}
968
969#[gpui::test]
970fn test_fold_action(cx: &mut TestAppContext) {
971 init_test(cx, |_| {});
972
973 let editor = cx.add_window(|window, cx| {
974 let buffer = MultiBuffer::build_simple(
975 &"
976 impl Foo {
977 // Hello!
978
979 fn a() {
980 1
981 }
982
983 fn b() {
984 2
985 }
986
987 fn c() {
988 3
989 }
990 }
991 "
992 .unindent(),
993 cx,
994 );
995 build_editor(buffer, window, cx)
996 });
997
998 _ = editor.update(cx, |editor, window, cx| {
999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1000 s.select_display_ranges([
1001 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1002 ]);
1003 });
1004 editor.fold(&Fold, window, cx);
1005 assert_eq!(
1006 editor.display_text(cx),
1007 "
1008 impl Foo {
1009 // Hello!
1010
1011 fn a() {
1012 1
1013 }
1014
1015 fn b() {⋯
1016 }
1017
1018 fn c() {⋯
1019 }
1020 }
1021 "
1022 .unindent(),
1023 );
1024
1025 editor.fold(&Fold, window, cx);
1026 assert_eq!(
1027 editor.display_text(cx),
1028 "
1029 impl Foo {⋯
1030 }
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 "
1039 impl Foo {
1040 // Hello!
1041
1042 fn a() {
1043 1
1044 }
1045
1046 fn b() {⋯
1047 }
1048
1049 fn c() {⋯
1050 }
1051 }
1052 "
1053 .unindent(),
1054 );
1055
1056 editor.unfold_lines(&UnfoldLines, window, cx);
1057 assert_eq!(
1058 editor.display_text(cx),
1059 editor.buffer.read(cx).read(cx).text()
1060 );
1061 });
1062}
1063
1064#[gpui::test]
1065fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1066 init_test(cx, |_| {});
1067
1068 let editor = cx.add_window(|window, cx| {
1069 let buffer = MultiBuffer::build_simple(
1070 &"
1071 class Foo:
1072 # Hello!
1073
1074 def a():
1075 print(1)
1076
1077 def b():
1078 print(2)
1079
1080 def c():
1081 print(3)
1082 "
1083 .unindent(),
1084 cx,
1085 );
1086 build_editor(buffer, window, cx)
1087 });
1088
1089 _ = editor.update(cx, |editor, window, cx| {
1090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1091 s.select_display_ranges([
1092 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1093 ]);
1094 });
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 class Foo:
1100 # Hello!
1101
1102 def a():
1103 print(1)
1104
1105 def b():⋯
1106
1107 def c():⋯
1108 "
1109 .unindent(),
1110 );
1111
1112 editor.fold(&Fold, window, cx);
1113 assert_eq!(
1114 editor.display_text(cx),
1115 "
1116 class Foo:⋯
1117 "
1118 .unindent(),
1119 );
1120
1121 editor.unfold_lines(&UnfoldLines, 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.unfold_lines(&UnfoldLines, window, cx);
1139 assert_eq!(
1140 editor.display_text(cx),
1141 editor.buffer.read(cx).read(cx).text()
1142 );
1143 });
1144}
1145
1146#[gpui::test]
1147fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1148 init_test(cx, |_| {});
1149
1150 let editor = cx.add_window(|window, cx| {
1151 let buffer = MultiBuffer::build_simple(
1152 &"
1153 class Foo:
1154 # Hello!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 def c():
1164 print(3)
1165
1166
1167 "
1168 .unindent(),
1169 cx,
1170 );
1171 build_editor(buffer, window, cx)
1172 });
1173
1174 _ = editor.update(cx, |editor, window, cx| {
1175 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1176 s.select_display_ranges([
1177 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1178 ]);
1179 });
1180 editor.fold(&Fold, window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():
1188 print(1)
1189
1190 def b():⋯
1191
1192
1193 def c():⋯
1194
1195
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.fold(&Fold, window, cx);
1201 assert_eq!(
1202 editor.display_text(cx),
1203 "
1204 class Foo:⋯
1205
1206
1207 "
1208 .unindent(),
1209 );
1210
1211 editor.unfold_lines(&UnfoldLines, window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:
1216 # Hello!
1217
1218 def a():
1219 print(1)
1220
1221 def b():⋯
1222
1223
1224 def c():⋯
1225
1226
1227 "
1228 .unindent(),
1229 );
1230
1231 editor.unfold_lines(&UnfoldLines, window, cx);
1232 assert_eq!(
1233 editor.display_text(cx),
1234 editor.buffer.read(cx).read(cx).text()
1235 );
1236 });
1237}
1238
1239#[gpui::test]
1240fn test_fold_at_level(cx: &mut TestAppContext) {
1241 init_test(cx, |_| {});
1242
1243 let editor = cx.add_window(|window, cx| {
1244 let buffer = MultiBuffer::build_simple(
1245 &"
1246 class Foo:
1247 # Hello!
1248
1249 def a():
1250 print(1)
1251
1252 def b():
1253 print(2)
1254
1255
1256 class Bar:
1257 # World!
1258
1259 def a():
1260 print(1)
1261
1262 def b():
1263 print(2)
1264
1265
1266 "
1267 .unindent(),
1268 cx,
1269 );
1270 build_editor(buffer, window, cx)
1271 });
1272
1273 _ = editor.update(cx, |editor, window, cx| {
1274 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1275 assert_eq!(
1276 editor.display_text(cx),
1277 "
1278 class Foo:
1279 # Hello!
1280
1281 def a():⋯
1282
1283 def b():⋯
1284
1285
1286 class Bar:
1287 # World!
1288
1289 def a():⋯
1290
1291 def b():⋯
1292
1293
1294 "
1295 .unindent(),
1296 );
1297
1298 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1299 assert_eq!(
1300 editor.display_text(cx),
1301 "
1302 class Foo:⋯
1303
1304
1305 class Bar:⋯
1306
1307
1308 "
1309 .unindent(),
1310 );
1311
1312 editor.unfold_all(&UnfoldAll, window, cx);
1313 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1314 assert_eq!(
1315 editor.display_text(cx),
1316 "
1317 class Foo:
1318 # Hello!
1319
1320 def a():
1321 print(1)
1322
1323 def b():
1324 print(2)
1325
1326
1327 class Bar:
1328 # World!
1329
1330 def a():
1331 print(1)
1332
1333 def b():
1334 print(2)
1335
1336
1337 "
1338 .unindent(),
1339 );
1340
1341 assert_eq!(
1342 editor.display_text(cx),
1343 editor.buffer.read(cx).read(cx).text()
1344 );
1345 let (_, positions) = marked_text_ranges(
1346 &"
1347 class Foo:
1348 # Hello!
1349
1350 def a():
1351 print(1)
1352
1353 def b():
1354 p«riˇ»nt(2)
1355
1356
1357 class Bar:
1358 # World!
1359
1360 def a():
1361 «ˇprint(1)
1362
1363 def b():
1364 print(2)»
1365
1366
1367 "
1368 .unindent(),
1369 true,
1370 );
1371
1372 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1373 s.select_ranges(positions)
1374 });
1375
1376 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1377 assert_eq!(
1378 editor.display_text(cx),
1379 "
1380 class Foo:
1381 # Hello!
1382
1383 def a():⋯
1384
1385 def b():
1386 print(2)
1387
1388
1389 class Bar:
1390 # World!
1391
1392 def a():
1393 print(1)
1394
1395 def b():
1396 print(2)
1397
1398
1399 "
1400 .unindent(),
1401 );
1402 });
1403}
1404
1405#[gpui::test]
1406fn test_move_cursor(cx: &mut TestAppContext) {
1407 init_test(cx, |_| {});
1408
1409 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1410 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1411
1412 buffer.update(cx, |buffer, cx| {
1413 buffer.edit(
1414 vec![
1415 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1416 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1417 ],
1418 None,
1419 cx,
1420 );
1421 });
1422 _ = editor.update(cx, |editor, window, cx| {
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1426 );
1427
1428 editor.move_down(&MoveDown, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1432 );
1433
1434 editor.move_right(&MoveRight, window, cx);
1435 assert_eq!(
1436 editor.selections.display_ranges(cx),
1437 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1438 );
1439
1440 editor.move_left(&MoveLeft, window, cx);
1441 assert_eq!(
1442 editor.selections.display_ranges(cx),
1443 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1444 );
1445
1446 editor.move_up(&MoveUp, window, cx);
1447 assert_eq!(
1448 editor.selections.display_ranges(cx),
1449 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1450 );
1451
1452 editor.move_to_end(&MoveToEnd, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1456 );
1457
1458 editor.move_to_beginning(&MoveToBeginning, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1462 );
1463
1464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1465 s.select_display_ranges([
1466 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1467 ]);
1468 });
1469 editor.select_to_beginning(&SelectToBeginning, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1473 );
1474
1475 editor.select_to_end(&SelectToEnd, window, cx);
1476 assert_eq!(
1477 editor.selections.display_ranges(cx),
1478 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1479 );
1480 });
1481}
1482
1483#[gpui::test]
1484fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1485 init_test(cx, |_| {});
1486
1487 let editor = cx.add_window(|window, cx| {
1488 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1489 build_editor(buffer, window, cx)
1490 });
1491
1492 assert_eq!('🟥'.len_utf8(), 4);
1493 assert_eq!('α'.len_utf8(), 2);
1494
1495 _ = editor.update(cx, |editor, window, cx| {
1496 editor.fold_creases(
1497 vec![
1498 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1499 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1500 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1501 ],
1502 true,
1503 window,
1504 cx,
1505 );
1506 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1507
1508 editor.move_right(&MoveRight, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(0, "🟥".len())]
1512 );
1513 editor.move_right(&MoveRight, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(0, "🟥🟧".len())]
1517 );
1518 editor.move_right(&MoveRight, window, cx);
1519 assert_eq!(
1520 editor.selections.display_ranges(cx),
1521 &[empty_range(0, "🟥🟧⋯".len())]
1522 );
1523
1524 editor.move_down(&MoveDown, window, cx);
1525 assert_eq!(
1526 editor.selections.display_ranges(cx),
1527 &[empty_range(1, "ab⋯e".len())]
1528 );
1529 editor.move_left(&MoveLeft, window, cx);
1530 assert_eq!(
1531 editor.selections.display_ranges(cx),
1532 &[empty_range(1, "ab⋯".len())]
1533 );
1534 editor.move_left(&MoveLeft, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(1, "ab".len())]
1538 );
1539 editor.move_left(&MoveLeft, window, cx);
1540 assert_eq!(
1541 editor.selections.display_ranges(cx),
1542 &[empty_range(1, "a".len())]
1543 );
1544
1545 editor.move_down(&MoveDown, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[empty_range(2, "α".len())]
1549 );
1550 editor.move_right(&MoveRight, window, cx);
1551 assert_eq!(
1552 editor.selections.display_ranges(cx),
1553 &[empty_range(2, "αβ".len())]
1554 );
1555 editor.move_right(&MoveRight, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[empty_range(2, "αβ⋯".len())]
1559 );
1560 editor.move_right(&MoveRight, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(2, "αβ⋯ε".len())]
1564 );
1565
1566 editor.move_up(&MoveUp, window, cx);
1567 assert_eq!(
1568 editor.selections.display_ranges(cx),
1569 &[empty_range(1, "ab⋯e".len())]
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_up(&MoveUp, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[empty_range(1, "ab⋯e".len())]
1580 );
1581
1582 editor.move_up(&MoveUp, window, cx);
1583 assert_eq!(
1584 editor.selections.display_ranges(cx),
1585 &[empty_range(0, "🟥🟧".len())]
1586 );
1587 editor.move_left(&MoveLeft, window, cx);
1588 assert_eq!(
1589 editor.selections.display_ranges(cx),
1590 &[empty_range(0, "🟥".len())]
1591 );
1592 editor.move_left(&MoveLeft, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[empty_range(0, "".len())]
1596 );
1597 });
1598}
1599
1600#[gpui::test]
1601fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1602 init_test(cx, |_| {});
1603
1604 let editor = cx.add_window(|window, cx| {
1605 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1606 build_editor(buffer, window, cx)
1607 });
1608 _ = editor.update(cx, |editor, window, cx| {
1609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1610 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1611 });
1612
1613 // moving above start of document should move selection to start of document,
1614 // but the next move down should still be at the original goal_x
1615 editor.move_up(&MoveUp, window, cx);
1616 assert_eq!(
1617 editor.selections.display_ranges(cx),
1618 &[empty_range(0, "".len())]
1619 );
1620
1621 editor.move_down(&MoveDown, window, cx);
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[empty_range(1, "abcd".len())]
1625 );
1626
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[empty_range(2, "αβγ".len())]
1631 );
1632
1633 editor.move_down(&MoveDown, window, cx);
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[empty_range(3, "abcd".len())]
1637 );
1638
1639 editor.move_down(&MoveDown, window, cx);
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1643 );
1644
1645 // moving past end of document should not change goal_x
1646 editor.move_down(&MoveDown, window, cx);
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[empty_range(5, "".len())]
1650 );
1651
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(
1654 editor.selections.display_ranges(cx),
1655 &[empty_range(5, "".len())]
1656 );
1657
1658 editor.move_up(&MoveUp, window, cx);
1659 assert_eq!(
1660 editor.selections.display_ranges(cx),
1661 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1662 );
1663
1664 editor.move_up(&MoveUp, window, cx);
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[empty_range(3, "abcd".len())]
1668 );
1669
1670 editor.move_up(&MoveUp, window, cx);
1671 assert_eq!(
1672 editor.selections.display_ranges(cx),
1673 &[empty_range(2, "αβγ".len())]
1674 );
1675 });
1676}
1677
1678#[gpui::test]
1679fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1680 init_test(cx, |_| {});
1681 let move_to_beg = MoveToBeginningOfLine {
1682 stop_at_soft_wraps: true,
1683 stop_at_indent: true,
1684 };
1685
1686 let delete_to_beg = DeleteToBeginningOfLine {
1687 stop_at_indent: false,
1688 };
1689
1690 let move_to_end = MoveToEndOfLine {
1691 stop_at_soft_wraps: true,
1692 };
1693
1694 let editor = cx.add_window(|window, cx| {
1695 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1696 build_editor(buffer, window, cx)
1697 });
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1700 s.select_display_ranges([
1701 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1702 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1703 ]);
1704 });
1705 });
1706
1707 _ = editor.update(cx, |editor, window, cx| {
1708 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1709 assert_eq!(
1710 editor.selections.display_ranges(cx),
1711 &[
1712 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1713 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1714 ]
1715 );
1716 });
1717
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1720 assert_eq!(
1721 editor.selections.display_ranges(cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1724 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1731 assert_eq!(
1732 editor.selections.display_ranges(cx),
1733 &[
1734 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1735 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1736 ]
1737 );
1738 });
1739
1740 _ = editor.update(cx, |editor, window, cx| {
1741 editor.move_to_end_of_line(&move_to_end, window, cx);
1742 assert_eq!(
1743 editor.selections.display_ranges(cx),
1744 &[
1745 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1746 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1747 ]
1748 );
1749 });
1750
1751 // Moving to the end of line again is a no-op.
1752 _ = editor.update(cx, |editor, window, cx| {
1753 editor.move_to_end_of_line(&move_to_end, window, cx);
1754 assert_eq!(
1755 editor.selections.display_ranges(cx),
1756 &[
1757 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1758 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1759 ]
1760 );
1761 });
1762
1763 _ = editor.update(cx, |editor, window, cx| {
1764 editor.move_left(&MoveLeft, window, cx);
1765 editor.select_to_beginning_of_line(
1766 &SelectToBeginningOfLine {
1767 stop_at_soft_wraps: true,
1768 stop_at_indent: true,
1769 },
1770 window,
1771 cx,
1772 );
1773 assert_eq!(
1774 editor.selections.display_ranges(cx),
1775 &[
1776 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1777 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1778 ]
1779 );
1780 });
1781
1782 _ = editor.update(cx, |editor, window, cx| {
1783 editor.select_to_beginning_of_line(
1784 &SelectToBeginningOfLine {
1785 stop_at_soft_wraps: true,
1786 stop_at_indent: true,
1787 },
1788 window,
1789 cx,
1790 );
1791 assert_eq!(
1792 editor.selections.display_ranges(cx),
1793 &[
1794 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1795 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1796 ]
1797 );
1798 });
1799
1800 _ = editor.update(cx, |editor, window, cx| {
1801 editor.select_to_beginning_of_line(
1802 &SelectToBeginningOfLine {
1803 stop_at_soft_wraps: true,
1804 stop_at_indent: true,
1805 },
1806 window,
1807 cx,
1808 );
1809 assert_eq!(
1810 editor.selections.display_ranges(cx),
1811 &[
1812 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1813 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1814 ]
1815 );
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.select_to_end_of_line(
1820 &SelectToEndOfLine {
1821 stop_at_soft_wraps: true,
1822 },
1823 window,
1824 cx,
1825 );
1826 assert_eq!(
1827 editor.selections.display_ranges(cx),
1828 &[
1829 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1830 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1831 ]
1832 );
1833 });
1834
1835 _ = editor.update(cx, |editor, window, cx| {
1836 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1837 assert_eq!(editor.display_text(cx), "ab\n de");
1838 assert_eq!(
1839 editor.selections.display_ranges(cx),
1840 &[
1841 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1842 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1843 ]
1844 );
1845 });
1846
1847 _ = editor.update(cx, |editor, window, cx| {
1848 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1849 assert_eq!(editor.display_text(cx), "\n");
1850 assert_eq!(
1851 editor.selections.display_ranges(cx),
1852 &[
1853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1855 ]
1856 );
1857 });
1858}
1859
1860#[gpui::test]
1861fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1862 init_test(cx, |_| {});
1863 let move_to_beg = MoveToBeginningOfLine {
1864 stop_at_soft_wraps: false,
1865 stop_at_indent: false,
1866 };
1867
1868 let move_to_end = MoveToEndOfLine {
1869 stop_at_soft_wraps: false,
1870 };
1871
1872 let editor = cx.add_window(|window, cx| {
1873 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1874 build_editor(buffer, window, cx)
1875 });
1876
1877 _ = editor.update(cx, |editor, window, cx| {
1878 editor.set_wrap_width(Some(140.0.into()), cx);
1879
1880 // We expect the following lines after wrapping
1881 // ```
1882 // thequickbrownfox
1883 // jumpedoverthelazydo
1884 // gs
1885 // ```
1886 // The final `gs` was soft-wrapped onto a new line.
1887 assert_eq!(
1888 "thequickbrownfox\njumpedoverthelaz\nydogs",
1889 editor.display_text(cx),
1890 );
1891
1892 // First, let's assert behavior on the first line, that was not soft-wrapped.
1893 // Start the cursor at the `k` on the first line
1894 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1895 s.select_display_ranges([
1896 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1897 ]);
1898 });
1899
1900 // Moving to the beginning of the line should put us at the beginning of the line.
1901 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1902 assert_eq!(
1903 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1904 editor.selections.display_ranges(cx)
1905 );
1906
1907 // Moving to the end of the line should put us at the end of the line.
1908 editor.move_to_end_of_line(&move_to_end, window, cx);
1909 assert_eq!(
1910 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1911 editor.selections.display_ranges(cx)
1912 );
1913
1914 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1915 // Start the cursor at the last line (`y` that was wrapped to a new line)
1916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1917 s.select_display_ranges([
1918 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1919 ]);
1920 });
1921
1922 // Moving to the beginning of the line should put us at the start of the second line of
1923 // display text, i.e., the `j`.
1924 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1925 assert_eq!(
1926 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1927 editor.selections.display_ranges(cx)
1928 );
1929
1930 // Moving to the beginning of the line again should be a no-op.
1931 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1932 assert_eq!(
1933 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1934 editor.selections.display_ranges(cx)
1935 );
1936
1937 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1938 // next display line.
1939 editor.move_to_end_of_line(&move_to_end, window, cx);
1940 assert_eq!(
1941 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1942 editor.selections.display_ranges(cx)
1943 );
1944
1945 // Moving to the end of the line again should be a no-op.
1946 editor.move_to_end_of_line(&move_to_end, window, cx);
1947 assert_eq!(
1948 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1949 editor.selections.display_ranges(cx)
1950 );
1951 });
1952}
1953
1954#[gpui::test]
1955fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1956 init_test(cx, |_| {});
1957
1958 let move_to_beg = MoveToBeginningOfLine {
1959 stop_at_soft_wraps: true,
1960 stop_at_indent: true,
1961 };
1962
1963 let select_to_beg = SelectToBeginningOfLine {
1964 stop_at_soft_wraps: true,
1965 stop_at_indent: true,
1966 };
1967
1968 let delete_to_beg = DeleteToBeginningOfLine {
1969 stop_at_indent: true,
1970 };
1971
1972 let move_to_end = MoveToEndOfLine {
1973 stop_at_soft_wraps: false,
1974 };
1975
1976 let editor = cx.add_window(|window, cx| {
1977 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1978 build_editor(buffer, window, cx)
1979 });
1980
1981 _ = editor.update(cx, |editor, window, cx| {
1982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1983 s.select_display_ranges([
1984 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1985 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1986 ]);
1987 });
1988
1989 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1990 // and the second cursor at the first non-whitespace character in the line.
1991 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[
1995 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1997 ]
1998 );
1999
2000 // Moving to the beginning of the line again should be a no-op for the first cursor,
2001 // and should move the second cursor to the beginning of the line.
2002 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[
2006 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2007 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2008 ]
2009 );
2010
2011 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2012 // and should move the second cursor back to the first non-whitespace character in the line.
2013 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[
2017 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2019 ]
2020 );
2021
2022 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2023 // and to the first non-whitespace character in the line for the second cursor.
2024 editor.move_to_end_of_line(&move_to_end, window, cx);
2025 editor.move_left(&MoveLeft, window, cx);
2026 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2027 assert_eq!(
2028 editor.selections.display_ranges(cx),
2029 &[
2030 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2031 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2032 ]
2033 );
2034
2035 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2036 // and should select to the beginning of the line for the second cursor.
2037 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2038 assert_eq!(
2039 editor.selections.display_ranges(cx),
2040 &[
2041 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2042 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2043 ]
2044 );
2045
2046 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2047 // and should delete to the first non-whitespace character in the line for the second cursor.
2048 editor.move_to_end_of_line(&move_to_end, window, cx);
2049 editor.move_left(&MoveLeft, window, cx);
2050 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2051 assert_eq!(editor.text(cx), "c\n f");
2052 });
2053}
2054
2055#[gpui::test]
2056fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2057 init_test(cx, |_| {});
2058
2059 let move_to_beg = MoveToBeginningOfLine {
2060 stop_at_soft_wraps: true,
2061 stop_at_indent: true,
2062 };
2063
2064 let editor = cx.add_window(|window, cx| {
2065 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2066 build_editor(buffer, window, cx)
2067 });
2068
2069 _ = editor.update(cx, |editor, window, cx| {
2070 // test cursor between line_start and indent_start
2071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2072 s.select_display_ranges([
2073 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2074 ]);
2075 });
2076
2077 // cursor should move to line_start
2078 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2079 assert_eq!(
2080 editor.selections.display_ranges(cx),
2081 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2082 );
2083
2084 // cursor should move to indent_start
2085 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2086 assert_eq!(
2087 editor.selections.display_ranges(cx),
2088 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2089 );
2090
2091 // cursor should move to back to line_start
2092 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2093 assert_eq!(
2094 editor.selections.display_ranges(cx),
2095 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2096 );
2097 });
2098}
2099
2100#[gpui::test]
2101fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2102 init_test(cx, |_| {});
2103
2104 let editor = cx.add_window(|window, cx| {
2105 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2106 build_editor(buffer, window, cx)
2107 });
2108 _ = editor.update(cx, |editor, window, cx| {
2109 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2110 s.select_display_ranges([
2111 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2112 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2113 ])
2114 });
2115 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2116 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2117
2118 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2119 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2120
2121 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2122 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2123
2124 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2125 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2126
2127 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2128 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2129
2130 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2131 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2132
2133 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2134 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2135
2136 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2137 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2138
2139 editor.move_right(&MoveRight, window, cx);
2140 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2141 assert_selection_ranges(
2142 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2143 editor,
2144 cx,
2145 );
2146
2147 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2148 assert_selection_ranges(
2149 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2150 editor,
2151 cx,
2152 );
2153
2154 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2155 assert_selection_ranges(
2156 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2157 editor,
2158 cx,
2159 );
2160 });
2161}
2162
2163#[gpui::test]
2164fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2165 init_test(cx, |_| {});
2166
2167 let editor = cx.add_window(|window, cx| {
2168 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2169 build_editor(buffer, window, cx)
2170 });
2171
2172 _ = editor.update(cx, |editor, window, cx| {
2173 editor.set_wrap_width(Some(140.0.into()), cx);
2174 assert_eq!(
2175 editor.display_text(cx),
2176 "use one::{\n two::three::\n four::five\n};"
2177 );
2178
2179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2180 s.select_display_ranges([
2181 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2182 ]);
2183 });
2184
2185 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2186 assert_eq!(
2187 editor.selections.display_ranges(cx),
2188 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2189 );
2190
2191 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2192 assert_eq!(
2193 editor.selections.display_ranges(cx),
2194 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2195 );
2196
2197 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2198 assert_eq!(
2199 editor.selections.display_ranges(cx),
2200 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2201 );
2202
2203 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2204 assert_eq!(
2205 editor.selections.display_ranges(cx),
2206 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2207 );
2208
2209 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2210 assert_eq!(
2211 editor.selections.display_ranges(cx),
2212 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2213 );
2214
2215 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2216 assert_eq!(
2217 editor.selections.display_ranges(cx),
2218 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2219 );
2220 });
2221}
2222
2223#[gpui::test]
2224async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2225 init_test(cx, |_| {});
2226 let mut cx = EditorTestContext::new(cx).await;
2227
2228 let line_height = cx.editor(|editor, window, _| {
2229 editor
2230 .style()
2231 .unwrap()
2232 .text
2233 .line_height_in_pixels(window.rem_size())
2234 });
2235 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2236
2237 cx.set_state(
2238 &r#"ˇone
2239 two
2240
2241 three
2242 fourˇ
2243 five
2244
2245 six"#
2246 .unindent(),
2247 );
2248
2249 cx.update_editor(|editor, window, cx| {
2250 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2251 });
2252 cx.assert_editor_state(
2253 &r#"one
2254 two
2255 ˇ
2256 three
2257 four
2258 five
2259 ˇ
2260 six"#
2261 .unindent(),
2262 );
2263
2264 cx.update_editor(|editor, window, cx| {
2265 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2266 });
2267 cx.assert_editor_state(
2268 &r#"one
2269 two
2270
2271 three
2272 four
2273 five
2274 ˇ
2275 sixˇ"#
2276 .unindent(),
2277 );
2278
2279 cx.update_editor(|editor, window, cx| {
2280 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2281 });
2282 cx.assert_editor_state(
2283 &r#"one
2284 two
2285
2286 three
2287 four
2288 five
2289
2290 sixˇ"#
2291 .unindent(),
2292 );
2293
2294 cx.update_editor(|editor, window, cx| {
2295 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2296 });
2297 cx.assert_editor_state(
2298 &r#"one
2299 two
2300
2301 three
2302 four
2303 five
2304 ˇ
2305 six"#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"one
2314 two
2315 ˇ
2316 three
2317 four
2318 five
2319
2320 six"#
2321 .unindent(),
2322 );
2323
2324 cx.update_editor(|editor, window, cx| {
2325 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2326 });
2327 cx.assert_editor_state(
2328 &r#"ˇone
2329 two
2330
2331 three
2332 four
2333 five
2334
2335 six"#
2336 .unindent(),
2337 );
2338}
2339
2340#[gpui::test]
2341async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2342 init_test(cx, |_| {});
2343 let mut cx = EditorTestContext::new(cx).await;
2344 let line_height = cx.editor(|editor, window, _| {
2345 editor
2346 .style()
2347 .unwrap()
2348 .text
2349 .line_height_in_pixels(window.rem_size())
2350 });
2351 let window = cx.window;
2352 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2353
2354 cx.set_state(
2355 r#"ˇone
2356 two
2357 three
2358 four
2359 five
2360 six
2361 seven
2362 eight
2363 nine
2364 ten
2365 "#,
2366 );
2367
2368 cx.update_editor(|editor, window, cx| {
2369 assert_eq!(
2370 editor.snapshot(window, cx).scroll_position(),
2371 gpui::Point::new(0., 0.)
2372 );
2373 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2374 assert_eq!(
2375 editor.snapshot(window, cx).scroll_position(),
2376 gpui::Point::new(0., 3.)
2377 );
2378 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2379 assert_eq!(
2380 editor.snapshot(window, cx).scroll_position(),
2381 gpui::Point::new(0., 6.)
2382 );
2383 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2384 assert_eq!(
2385 editor.snapshot(window, cx).scroll_position(),
2386 gpui::Point::new(0., 3.)
2387 );
2388
2389 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2390 assert_eq!(
2391 editor.snapshot(window, cx).scroll_position(),
2392 gpui::Point::new(0., 1.)
2393 );
2394 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2395 assert_eq!(
2396 editor.snapshot(window, cx).scroll_position(),
2397 gpui::Point::new(0., 3.)
2398 );
2399 });
2400}
2401
2402#[gpui::test]
2403async fn test_autoscroll(cx: &mut TestAppContext) {
2404 init_test(cx, |_| {});
2405 let mut cx = EditorTestContext::new(cx).await;
2406
2407 let line_height = cx.update_editor(|editor, window, cx| {
2408 editor.set_vertical_scroll_margin(2, cx);
2409 editor
2410 .style()
2411 .unwrap()
2412 .text
2413 .line_height_in_pixels(window.rem_size())
2414 });
2415 let window = cx.window;
2416 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2417
2418 cx.set_state(
2419 r#"ˇone
2420 two
2421 three
2422 four
2423 five
2424 six
2425 seven
2426 eight
2427 nine
2428 ten
2429 "#,
2430 );
2431 cx.update_editor(|editor, window, cx| {
2432 assert_eq!(
2433 editor.snapshot(window, cx).scroll_position(),
2434 gpui::Point::new(0., 0.0)
2435 );
2436 });
2437
2438 // Add a cursor below the visible area. Since both cursors cannot fit
2439 // on screen, the editor autoscrolls to reveal the newest cursor, and
2440 // allows the vertical scroll margin below that cursor.
2441 cx.update_editor(|editor, window, cx| {
2442 editor.change_selections(Default::default(), window, cx, |selections| {
2443 selections.select_ranges([
2444 Point::new(0, 0)..Point::new(0, 0),
2445 Point::new(6, 0)..Point::new(6, 0),
2446 ]);
2447 })
2448 });
2449 cx.update_editor(|editor, window, cx| {
2450 assert_eq!(
2451 editor.snapshot(window, cx).scroll_position(),
2452 gpui::Point::new(0., 3.0)
2453 );
2454 });
2455
2456 // Move down. The editor cursor scrolls down to track the newest cursor.
2457 cx.update_editor(|editor, window, cx| {
2458 editor.move_down(&Default::default(), window, cx);
2459 });
2460 cx.update_editor(|editor, window, cx| {
2461 assert_eq!(
2462 editor.snapshot(window, cx).scroll_position(),
2463 gpui::Point::new(0., 4.0)
2464 );
2465 });
2466
2467 // Add a cursor above the visible area. Since both cursors fit on screen,
2468 // the editor scrolls to show both.
2469 cx.update_editor(|editor, window, cx| {
2470 editor.change_selections(Default::default(), window, cx, |selections| {
2471 selections.select_ranges([
2472 Point::new(1, 0)..Point::new(1, 0),
2473 Point::new(6, 0)..Point::new(6, 0),
2474 ]);
2475 })
2476 });
2477 cx.update_editor(|editor, window, cx| {
2478 assert_eq!(
2479 editor.snapshot(window, cx).scroll_position(),
2480 gpui::Point::new(0., 1.0)
2481 );
2482 });
2483}
2484
2485#[gpui::test]
2486async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2487 init_test(cx, |_| {});
2488 let mut cx = EditorTestContext::new(cx).await;
2489
2490 let line_height = cx.editor(|editor, window, _cx| {
2491 editor
2492 .style()
2493 .unwrap()
2494 .text
2495 .line_height_in_pixels(window.rem_size())
2496 });
2497 let window = cx.window;
2498 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2499 cx.set_state(
2500 &r#"
2501 ˇone
2502 two
2503 threeˇ
2504 four
2505 five
2506 six
2507 seven
2508 eight
2509 nine
2510 ten
2511 "#
2512 .unindent(),
2513 );
2514
2515 cx.update_editor(|editor, window, cx| {
2516 editor.move_page_down(&MovePageDown::default(), window, cx)
2517 });
2518 cx.assert_editor_state(
2519 &r#"
2520 one
2521 two
2522 three
2523 ˇfour
2524 five
2525 sixˇ
2526 seven
2527 eight
2528 nine
2529 ten
2530 "#
2531 .unindent(),
2532 );
2533
2534 cx.update_editor(|editor, window, cx| {
2535 editor.move_page_down(&MovePageDown::default(), window, cx)
2536 });
2537 cx.assert_editor_state(
2538 &r#"
2539 one
2540 two
2541 three
2542 four
2543 five
2544 six
2545 ˇseven
2546 eight
2547 nineˇ
2548 ten
2549 "#
2550 .unindent(),
2551 );
2552
2553 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2554 cx.assert_editor_state(
2555 &r#"
2556 one
2557 two
2558 three
2559 ˇfour
2560 five
2561 sixˇ
2562 seven
2563 eight
2564 nine
2565 ten
2566 "#
2567 .unindent(),
2568 );
2569
2570 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2571 cx.assert_editor_state(
2572 &r#"
2573 ˇone
2574 two
2575 threeˇ
2576 four
2577 five
2578 six
2579 seven
2580 eight
2581 nine
2582 ten
2583 "#
2584 .unindent(),
2585 );
2586
2587 // Test select collapsing
2588 cx.update_editor(|editor, window, cx| {
2589 editor.move_page_down(&MovePageDown::default(), window, cx);
2590 editor.move_page_down(&MovePageDown::default(), window, cx);
2591 editor.move_page_down(&MovePageDown::default(), window, cx);
2592 });
2593 cx.assert_editor_state(
2594 &r#"
2595 one
2596 two
2597 three
2598 four
2599 five
2600 six
2601 seven
2602 eight
2603 nine
2604 ˇten
2605 ˇ"#
2606 .unindent(),
2607 );
2608}
2609
2610#[gpui::test]
2611async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2612 init_test(cx, |_| {});
2613 let mut cx = EditorTestContext::new(cx).await;
2614 cx.set_state("one «two threeˇ» four");
2615 cx.update_editor(|editor, window, cx| {
2616 editor.delete_to_beginning_of_line(
2617 &DeleteToBeginningOfLine {
2618 stop_at_indent: false,
2619 },
2620 window,
2621 cx,
2622 );
2623 assert_eq!(editor.text(cx), " four");
2624 });
2625}
2626
2627#[gpui::test]
2628async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2629 init_test(cx, |_| {});
2630
2631 let mut cx = EditorTestContext::new(cx).await;
2632
2633 // For an empty selection, the preceding word fragment is deleted.
2634 // For non-empty selections, only selected characters are deleted.
2635 cx.set_state("onˇe two t«hreˇ»e four");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_previous_word_start(
2638 &DeleteToPreviousWordStart {
2639 ignore_newlines: false,
2640 ignore_brackets: false,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 cx.assert_editor_state("ˇe two tˇe four");
2647
2648 cx.set_state("e tˇwo te «fˇ»our");
2649 cx.update_editor(|editor, window, cx| {
2650 editor.delete_to_next_word_end(
2651 &DeleteToNextWordEnd {
2652 ignore_newlines: false,
2653 ignore_brackets: false,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 cx.assert_editor_state("e tˇ te ˇour");
2660}
2661
2662#[gpui::test]
2663async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2664 init_test(cx, |_| {});
2665
2666 let mut cx = EditorTestContext::new(cx).await;
2667
2668 cx.set_state("here is some text ˇwith a space");
2669 cx.update_editor(|editor, window, cx| {
2670 editor.delete_to_previous_word_start(
2671 &DeleteToPreviousWordStart {
2672 ignore_newlines: false,
2673 ignore_brackets: true,
2674 },
2675 window,
2676 cx,
2677 );
2678 });
2679 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2680 cx.assert_editor_state("here is some textˇwith a space");
2681
2682 cx.set_state("here is some text ˇwith a space");
2683 cx.update_editor(|editor, window, cx| {
2684 editor.delete_to_previous_word_start(
2685 &DeleteToPreviousWordStart {
2686 ignore_newlines: false,
2687 ignore_brackets: false,
2688 },
2689 window,
2690 cx,
2691 );
2692 });
2693 cx.assert_editor_state("here is some textˇwith a space");
2694
2695 cx.set_state("here is some textˇ with a space");
2696 cx.update_editor(|editor, window, cx| {
2697 editor.delete_to_next_word_end(
2698 &DeleteToNextWordEnd {
2699 ignore_newlines: false,
2700 ignore_brackets: true,
2701 },
2702 window,
2703 cx,
2704 );
2705 });
2706 // Same happens in the other direction.
2707 cx.assert_editor_state("here is some textˇwith a space");
2708
2709 cx.set_state("here is some textˇ with a space");
2710 cx.update_editor(|editor, window, cx| {
2711 editor.delete_to_next_word_end(
2712 &DeleteToNextWordEnd {
2713 ignore_newlines: false,
2714 ignore_brackets: false,
2715 },
2716 window,
2717 cx,
2718 );
2719 });
2720 cx.assert_editor_state("here is some textˇwith a space");
2721
2722 cx.set_state("here is some textˇ with a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_next_word_end(
2725 &DeleteToNextWordEnd {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here is some textˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 cx.assert_editor_state("here is some ˇwith a space");
2745 cx.update_editor(|editor, window, cx| {
2746 editor.delete_to_previous_word_start(
2747 &DeleteToPreviousWordStart {
2748 ignore_newlines: true,
2749 ignore_brackets: false,
2750 },
2751 window,
2752 cx,
2753 );
2754 });
2755 // Single whitespaces are removed with the word behind them.
2756 cx.assert_editor_state("here is ˇwith a space");
2757 cx.update_editor(|editor, window, cx| {
2758 editor.delete_to_previous_word_start(
2759 &DeleteToPreviousWordStart {
2760 ignore_newlines: true,
2761 ignore_brackets: false,
2762 },
2763 window,
2764 cx,
2765 );
2766 });
2767 cx.assert_editor_state("here ˇwith a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_previous_word_start(
2770 &DeleteToPreviousWordStart {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇwith a space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_previous_word_start(
2781 &DeleteToPreviousWordStart {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state("ˇwith a space");
2790 cx.update_editor(|editor, window, cx| {
2791 editor.delete_to_next_word_end(
2792 &DeleteToNextWordEnd {
2793 ignore_newlines: true,
2794 ignore_brackets: false,
2795 },
2796 window,
2797 cx,
2798 );
2799 });
2800 // Same happens in the other direction.
2801 cx.assert_editor_state("ˇ a space");
2802 cx.update_editor(|editor, window, cx| {
2803 editor.delete_to_next_word_end(
2804 &DeleteToNextWordEnd {
2805 ignore_newlines: true,
2806 ignore_brackets: false,
2807 },
2808 window,
2809 cx,
2810 );
2811 });
2812 cx.assert_editor_state("ˇ space");
2813 cx.update_editor(|editor, window, cx| {
2814 editor.delete_to_next_word_end(
2815 &DeleteToNextWordEnd {
2816 ignore_newlines: true,
2817 ignore_brackets: false,
2818 },
2819 window,
2820 cx,
2821 );
2822 });
2823 cx.assert_editor_state("ˇ");
2824 cx.update_editor(|editor, window, cx| {
2825 editor.delete_to_next_word_end(
2826 &DeleteToNextWordEnd {
2827 ignore_newlines: true,
2828 ignore_brackets: false,
2829 },
2830 window,
2831 cx,
2832 );
2833 });
2834 cx.assert_editor_state("ˇ");
2835 cx.update_editor(|editor, window, cx| {
2836 editor.delete_to_previous_word_start(
2837 &DeleteToPreviousWordStart {
2838 ignore_newlines: true,
2839 ignore_brackets: false,
2840 },
2841 window,
2842 cx,
2843 );
2844 });
2845 cx.assert_editor_state("ˇ");
2846}
2847
2848#[gpui::test]
2849async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2850 init_test(cx, |_| {});
2851
2852 let language = Arc::new(
2853 Language::new(
2854 LanguageConfig {
2855 brackets: BracketPairConfig {
2856 pairs: vec![
2857 BracketPair {
2858 start: "\"".to_string(),
2859 end: "\"".to_string(),
2860 close: true,
2861 surround: true,
2862 newline: false,
2863 },
2864 BracketPair {
2865 start: "(".to_string(),
2866 end: ")".to_string(),
2867 close: true,
2868 surround: true,
2869 newline: true,
2870 },
2871 ],
2872 ..BracketPairConfig::default()
2873 },
2874 ..LanguageConfig::default()
2875 },
2876 Some(tree_sitter_rust::LANGUAGE.into()),
2877 )
2878 .with_brackets_query(
2879 r#"
2880 ("(" @open ")" @close)
2881 ("\"" @open "\"" @close)
2882 "#,
2883 )
2884 .unwrap(),
2885 );
2886
2887 let mut cx = EditorTestContext::new(cx).await;
2888 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2889
2890 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2891 cx.update_editor(|editor, window, cx| {
2892 editor.delete_to_previous_word_start(
2893 &DeleteToPreviousWordStart {
2894 ignore_newlines: true,
2895 ignore_brackets: false,
2896 },
2897 window,
2898 cx,
2899 );
2900 });
2901 // Deletion stops before brackets if asked to not ignore them.
2902 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2903 cx.update_editor(|editor, window, cx| {
2904 editor.delete_to_previous_word_start(
2905 &DeleteToPreviousWordStart {
2906 ignore_newlines: true,
2907 ignore_brackets: false,
2908 },
2909 window,
2910 cx,
2911 );
2912 });
2913 // Deletion has to remove a single bracket and then stop again.
2914 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2915
2916 cx.update_editor(|editor, window, cx| {
2917 editor.delete_to_previous_word_start(
2918 &DeleteToPreviousWordStart {
2919 ignore_newlines: true,
2920 ignore_brackets: false,
2921 },
2922 window,
2923 cx,
2924 );
2925 });
2926 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2927
2928 cx.update_editor(|editor, window, cx| {
2929 editor.delete_to_previous_word_start(
2930 &DeleteToPreviousWordStart {
2931 ignore_newlines: true,
2932 ignore_brackets: false,
2933 },
2934 window,
2935 cx,
2936 );
2937 });
2938 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2939
2940 cx.update_editor(|editor, window, cx| {
2941 editor.delete_to_previous_word_start(
2942 &DeleteToPreviousWordStart {
2943 ignore_newlines: true,
2944 ignore_brackets: false,
2945 },
2946 window,
2947 cx,
2948 );
2949 });
2950 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2951
2952 cx.update_editor(|editor, window, cx| {
2953 editor.delete_to_next_word_end(
2954 &DeleteToNextWordEnd {
2955 ignore_newlines: true,
2956 ignore_brackets: false,
2957 },
2958 window,
2959 cx,
2960 );
2961 });
2962 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2963 cx.assert_editor_state(r#"ˇ");"#);
2964
2965 cx.update_editor(|editor, window, cx| {
2966 editor.delete_to_next_word_end(
2967 &DeleteToNextWordEnd {
2968 ignore_newlines: true,
2969 ignore_brackets: false,
2970 },
2971 window,
2972 cx,
2973 );
2974 });
2975 cx.assert_editor_state(r#"ˇ"#);
2976
2977 cx.update_editor(|editor, window, cx| {
2978 editor.delete_to_next_word_end(
2979 &DeleteToNextWordEnd {
2980 ignore_newlines: true,
2981 ignore_brackets: false,
2982 },
2983 window,
2984 cx,
2985 );
2986 });
2987 cx.assert_editor_state(r#"ˇ"#);
2988
2989 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2990 cx.update_editor(|editor, window, cx| {
2991 editor.delete_to_previous_word_start(
2992 &DeleteToPreviousWordStart {
2993 ignore_newlines: true,
2994 ignore_brackets: true,
2995 },
2996 window,
2997 cx,
2998 );
2999 });
3000 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3001}
3002
3003#[gpui::test]
3004fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3005 init_test(cx, |_| {});
3006
3007 let editor = cx.add_window(|window, cx| {
3008 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3009 build_editor(buffer, window, cx)
3010 });
3011 let del_to_prev_word_start = DeleteToPreviousWordStart {
3012 ignore_newlines: false,
3013 ignore_brackets: false,
3014 };
3015 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3016 ignore_newlines: true,
3017 ignore_brackets: false,
3018 };
3019
3020 _ = editor.update(cx, |editor, window, cx| {
3021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3022 s.select_display_ranges([
3023 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3024 ])
3025 });
3026 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3027 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3028 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3029 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3030 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3031 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3032 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3033 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3034 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3035 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3036 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3037 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3038 });
3039}
3040
3041#[gpui::test]
3042fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3043 init_test(cx, |_| {});
3044
3045 let editor = cx.add_window(|window, cx| {
3046 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3047 build_editor(buffer, window, cx)
3048 });
3049 let del_to_next_word_end = DeleteToNextWordEnd {
3050 ignore_newlines: false,
3051 ignore_brackets: false,
3052 };
3053 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3054 ignore_newlines: true,
3055 ignore_brackets: false,
3056 };
3057
3058 _ = editor.update(cx, |editor, window, cx| {
3059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3060 s.select_display_ranges([
3061 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3062 ])
3063 });
3064 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3065 assert_eq!(
3066 editor.buffer.read(cx).read(cx).text(),
3067 "one\n two\nthree\n four"
3068 );
3069 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3070 assert_eq!(
3071 editor.buffer.read(cx).read(cx).text(),
3072 "\n two\nthree\n four"
3073 );
3074 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3075 assert_eq!(
3076 editor.buffer.read(cx).read(cx).text(),
3077 "two\nthree\n four"
3078 );
3079 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3080 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3081 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3082 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3083 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3084 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3085 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3086 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3087 });
3088}
3089
3090#[gpui::test]
3091fn test_newline(cx: &mut TestAppContext) {
3092 init_test(cx, |_| {});
3093
3094 let editor = cx.add_window(|window, cx| {
3095 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3096 build_editor(buffer, window, cx)
3097 });
3098
3099 _ = editor.update(cx, |editor, window, cx| {
3100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3101 s.select_display_ranges([
3102 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3103 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3104 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3105 ])
3106 });
3107
3108 editor.newline(&Newline, window, cx);
3109 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3110 });
3111}
3112
3113#[gpui::test]
3114fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3115 init_test(cx, |_| {});
3116
3117 let editor = cx.add_window(|window, cx| {
3118 let buffer = MultiBuffer::build_simple(
3119 "
3120 a
3121 b(
3122 X
3123 )
3124 c(
3125 X
3126 )
3127 "
3128 .unindent()
3129 .as_str(),
3130 cx,
3131 );
3132 let mut editor = build_editor(buffer, window, cx);
3133 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3134 s.select_ranges([
3135 Point::new(2, 4)..Point::new(2, 5),
3136 Point::new(5, 4)..Point::new(5, 5),
3137 ])
3138 });
3139 editor
3140 });
3141
3142 _ = editor.update(cx, |editor, window, cx| {
3143 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3144 editor.buffer.update(cx, |buffer, cx| {
3145 buffer.edit(
3146 [
3147 (Point::new(1, 2)..Point::new(3, 0), ""),
3148 (Point::new(4, 2)..Point::new(6, 0), ""),
3149 ],
3150 None,
3151 cx,
3152 );
3153 assert_eq!(
3154 buffer.read(cx).text(),
3155 "
3156 a
3157 b()
3158 c()
3159 "
3160 .unindent()
3161 );
3162 });
3163 assert_eq!(
3164 editor.selections.ranges(cx),
3165 &[
3166 Point::new(1, 2)..Point::new(1, 2),
3167 Point::new(2, 2)..Point::new(2, 2),
3168 ],
3169 );
3170
3171 editor.newline(&Newline, window, cx);
3172 assert_eq!(
3173 editor.text(cx),
3174 "
3175 a
3176 b(
3177 )
3178 c(
3179 )
3180 "
3181 .unindent()
3182 );
3183
3184 // The selections are moved after the inserted newlines
3185 assert_eq!(
3186 editor.selections.ranges(cx),
3187 &[
3188 Point::new(2, 0)..Point::new(2, 0),
3189 Point::new(4, 0)..Point::new(4, 0),
3190 ],
3191 );
3192 });
3193}
3194
3195#[gpui::test]
3196async fn test_newline_above(cx: &mut TestAppContext) {
3197 init_test(cx, |settings| {
3198 settings.defaults.tab_size = NonZeroU32::new(4)
3199 });
3200
3201 let language = Arc::new(
3202 Language::new(
3203 LanguageConfig::default(),
3204 Some(tree_sitter_rust::LANGUAGE.into()),
3205 )
3206 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3207 .unwrap(),
3208 );
3209
3210 let mut cx = EditorTestContext::new(cx).await;
3211 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3212 cx.set_state(indoc! {"
3213 const a: ˇA = (
3214 (ˇ
3215 «const_functionˇ»(ˇ),
3216 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3217 )ˇ
3218 ˇ);ˇ
3219 "});
3220
3221 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3222 cx.assert_editor_state(indoc! {"
3223 ˇ
3224 const a: A = (
3225 ˇ
3226 (
3227 ˇ
3228 ˇ
3229 const_function(),
3230 ˇ
3231 ˇ
3232 ˇ
3233 ˇ
3234 something_else,
3235 ˇ
3236 )
3237 ˇ
3238 ˇ
3239 );
3240 "});
3241}
3242
3243#[gpui::test]
3244async fn test_newline_below(cx: &mut TestAppContext) {
3245 init_test(cx, |settings| {
3246 settings.defaults.tab_size = NonZeroU32::new(4)
3247 });
3248
3249 let language = Arc::new(
3250 Language::new(
3251 LanguageConfig::default(),
3252 Some(tree_sitter_rust::LANGUAGE.into()),
3253 )
3254 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3255 .unwrap(),
3256 );
3257
3258 let mut cx = EditorTestContext::new(cx).await;
3259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3260 cx.set_state(indoc! {"
3261 const a: ˇA = (
3262 (ˇ
3263 «const_functionˇ»(ˇ),
3264 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3265 )ˇ
3266 ˇ);ˇ
3267 "});
3268
3269 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 const a: A = (
3272 ˇ
3273 (
3274 ˇ
3275 const_function(),
3276 ˇ
3277 ˇ
3278 something_else,
3279 ˇ
3280 ˇ
3281 ˇ
3282 ˇ
3283 )
3284 ˇ
3285 );
3286 ˇ
3287 ˇ
3288 "});
3289}
3290
3291#[gpui::test]
3292async fn test_newline_comments(cx: &mut TestAppContext) {
3293 init_test(cx, |settings| {
3294 settings.defaults.tab_size = NonZeroU32::new(4)
3295 });
3296
3297 let language = Arc::new(Language::new(
3298 LanguageConfig {
3299 line_comments: vec!["// ".into()],
3300 ..LanguageConfig::default()
3301 },
3302 None,
3303 ));
3304 {
3305 let mut cx = EditorTestContext::new(cx).await;
3306 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3307 cx.set_state(indoc! {"
3308 // Fooˇ
3309 "});
3310
3311 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3312 cx.assert_editor_state(indoc! {"
3313 // Foo
3314 // ˇ
3315 "});
3316 // Ensure that we add comment prefix when existing line contains space
3317 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3318 cx.assert_editor_state(
3319 indoc! {"
3320 // Foo
3321 //s
3322 // ˇ
3323 "}
3324 .replace("s", " ") // s is used as space placeholder to prevent format on save
3325 .as_str(),
3326 );
3327 // Ensure that we add comment prefix when existing line does not contain space
3328 cx.set_state(indoc! {"
3329 // Foo
3330 //ˇ
3331 "});
3332 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3333 cx.assert_editor_state(indoc! {"
3334 // Foo
3335 //
3336 // ˇ
3337 "});
3338 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3339 cx.set_state(indoc! {"
3340 ˇ// Foo
3341 "});
3342 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3343 cx.assert_editor_state(indoc! {"
3344
3345 ˇ// Foo
3346 "});
3347 }
3348 // Ensure that comment continuations can be disabled.
3349 update_test_language_settings(cx, |settings| {
3350 settings.defaults.extend_comment_on_newline = Some(false);
3351 });
3352 let mut cx = EditorTestContext::new(cx).await;
3353 cx.set_state(indoc! {"
3354 // Fooˇ
3355 "});
3356 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3357 cx.assert_editor_state(indoc! {"
3358 // Foo
3359 ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(Language::new(
3370 LanguageConfig {
3371 line_comments: vec!["// ".into(), "/// ".into()],
3372 ..LanguageConfig::default()
3373 },
3374 None,
3375 ));
3376 {
3377 let mut cx = EditorTestContext::new(cx).await;
3378 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3379 cx.set_state(indoc! {"
3380 //ˇ
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 //
3385 // ˇ
3386 "});
3387
3388 cx.set_state(indoc! {"
3389 ///ˇ
3390 "});
3391 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3392 cx.assert_editor_state(indoc! {"
3393 ///
3394 /// ˇ
3395 "});
3396 }
3397}
3398
3399#[gpui::test]
3400async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3401 init_test(cx, |settings| {
3402 settings.defaults.tab_size = NonZeroU32::new(4)
3403 });
3404
3405 let language = Arc::new(
3406 Language::new(
3407 LanguageConfig {
3408 documentation_comment: Some(language::BlockCommentConfig {
3409 start: "/**".into(),
3410 end: "*/".into(),
3411 prefix: "* ".into(),
3412 tab_size: 1,
3413 }),
3414
3415 ..LanguageConfig::default()
3416 },
3417 Some(tree_sitter_rust::LANGUAGE.into()),
3418 )
3419 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3420 .unwrap(),
3421 );
3422
3423 {
3424 let mut cx = EditorTestContext::new(cx).await;
3425 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3426 cx.set_state(indoc! {"
3427 /**ˇ
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 /**
3433 * ˇ
3434 "});
3435 // Ensure that if cursor is before the comment start,
3436 // we do not actually insert a comment prefix.
3437 cx.set_state(indoc! {"
3438 ˇ/**
3439 "});
3440 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3441 cx.assert_editor_state(indoc! {"
3442
3443 ˇ/**
3444 "});
3445 // Ensure that if cursor is between it doesn't add comment prefix.
3446 cx.set_state(indoc! {"
3447 /*ˇ*
3448 "});
3449 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3450 cx.assert_editor_state(indoc! {"
3451 /*
3452 ˇ*
3453 "});
3454 // Ensure that if suffix exists on same line after cursor it adds new line.
3455 cx.set_state(indoc! {"
3456 /**ˇ*/
3457 "});
3458 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 /**
3461 * ˇ
3462 */
3463 "});
3464 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3465 cx.set_state(indoc! {"
3466 /**ˇ */
3467 "});
3468 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3469 cx.assert_editor_state(indoc! {"
3470 /**
3471 * ˇ
3472 */
3473 "});
3474 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3475 cx.set_state(indoc! {"
3476 /** ˇ*/
3477 "});
3478 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3479 cx.assert_editor_state(
3480 indoc! {"
3481 /**s
3482 * ˇ
3483 */
3484 "}
3485 .replace("s", " ") // s is used as space placeholder to prevent format on save
3486 .as_str(),
3487 );
3488 // Ensure that delimiter space is preserved when newline on already
3489 // spaced delimiter.
3490 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3491 cx.assert_editor_state(
3492 indoc! {"
3493 /**s
3494 *s
3495 * ˇ
3496 */
3497 "}
3498 .replace("s", " ") // s is used as space placeholder to prevent format on save
3499 .as_str(),
3500 );
3501 // Ensure that delimiter space is preserved when space is not
3502 // on existing delimiter.
3503 cx.set_state(indoc! {"
3504 /**
3505 *ˇ
3506 */
3507 "});
3508 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 /**
3511 *
3512 * ˇ
3513 */
3514 "});
3515 // Ensure that if suffix exists on same line after cursor it
3516 // doesn't add extra new line if prefix is not on same line.
3517 cx.set_state(indoc! {"
3518 /**
3519 ˇ*/
3520 "});
3521 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3522 cx.assert_editor_state(indoc! {"
3523 /**
3524
3525 ˇ*/
3526 "});
3527 // Ensure that it detects suffix after existing prefix.
3528 cx.set_state(indoc! {"
3529 /**ˇ/
3530 "});
3531 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3532 cx.assert_editor_state(indoc! {"
3533 /**
3534 ˇ/
3535 "});
3536 // Ensure that if suffix exists on same line before
3537 // cursor it does not add comment prefix.
3538 cx.set_state(indoc! {"
3539 /** */ˇ
3540 "});
3541 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3542 cx.assert_editor_state(indoc! {"
3543 /** */
3544 ˇ
3545 "});
3546 // Ensure that if suffix exists on same line before
3547 // cursor it does not add comment prefix.
3548 cx.set_state(indoc! {"
3549 /**
3550 *
3551 */ˇ
3552 "});
3553 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 /**
3556 *
3557 */
3558 ˇ
3559 "});
3560
3561 // Ensure that inline comment followed by code
3562 // doesn't add comment prefix on newline
3563 cx.set_state(indoc! {"
3564 /** */ textˇ
3565 "});
3566 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3567 cx.assert_editor_state(indoc! {"
3568 /** */ text
3569 ˇ
3570 "});
3571
3572 // Ensure that text after comment end tag
3573 // doesn't add comment prefix on newline
3574 cx.set_state(indoc! {"
3575 /**
3576 *
3577 */ˇtext
3578 "});
3579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 /**
3582 *
3583 */
3584 ˇtext
3585 "});
3586
3587 // Ensure if not comment block it doesn't
3588 // 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 comment continuations can be disabled.
3599 update_test_language_settings(cx, |settings| {
3600 settings.defaults.extend_comment_on_newline = Some(false);
3601 });
3602 let mut cx = EditorTestContext::new(cx).await;
3603 cx.set_state(indoc! {"
3604 /**ˇ
3605 "});
3606 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 /**
3609 ˇ
3610 "});
3611}
3612
3613#[gpui::test]
3614async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3615 init_test(cx, |settings| {
3616 settings.defaults.tab_size = NonZeroU32::new(4)
3617 });
3618
3619 let lua_language = Arc::new(Language::new(
3620 LanguageConfig {
3621 line_comments: vec!["--".into()],
3622 block_comment: Some(language::BlockCommentConfig {
3623 start: "--[[".into(),
3624 prefix: "".into(),
3625 end: "]]".into(),
3626 tab_size: 0,
3627 }),
3628 ..LanguageConfig::default()
3629 },
3630 None,
3631 ));
3632
3633 let mut cx = EditorTestContext::new(cx).await;
3634 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3635
3636 // Line with line comment should extend
3637 cx.set_state(indoc! {"
3638 --ˇ
3639 "});
3640 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3641 cx.assert_editor_state(indoc! {"
3642 --
3643 --ˇ
3644 "});
3645
3646 // Line with block comment that matches line comment should not extend
3647 cx.set_state(indoc! {"
3648 --[[ˇ
3649 "});
3650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 --[[
3653 ˇ
3654 "});
3655}
3656
3657#[gpui::test]
3658fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3659 init_test(cx, |_| {});
3660
3661 let editor = cx.add_window(|window, cx| {
3662 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3663 let mut editor = build_editor(buffer, window, cx);
3664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3665 s.select_ranges([3..4, 11..12, 19..20])
3666 });
3667 editor
3668 });
3669
3670 _ = editor.update(cx, |editor, window, cx| {
3671 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3672 editor.buffer.update(cx, |buffer, cx| {
3673 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3674 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3675 });
3676 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3677
3678 editor.insert("Z", window, cx);
3679 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3680
3681 // The selections are moved after the inserted characters
3682 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3683 });
3684}
3685
3686#[gpui::test]
3687async fn test_tab(cx: &mut TestAppContext) {
3688 init_test(cx, |settings| {
3689 settings.defaults.tab_size = NonZeroU32::new(3)
3690 });
3691
3692 let mut cx = EditorTestContext::new(cx).await;
3693 cx.set_state(indoc! {"
3694 ˇabˇc
3695 ˇ🏀ˇ🏀ˇefg
3696 dˇ
3697 "});
3698 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3699 cx.assert_editor_state(indoc! {"
3700 ˇab ˇc
3701 ˇ🏀 ˇ🏀 ˇefg
3702 d ˇ
3703 "});
3704
3705 cx.set_state(indoc! {"
3706 a
3707 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3708 "});
3709 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3710 cx.assert_editor_state(indoc! {"
3711 a
3712 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3713 "});
3714}
3715
3716#[gpui::test]
3717async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3718 init_test(cx, |_| {});
3719
3720 let mut cx = EditorTestContext::new(cx).await;
3721 let language = Arc::new(
3722 Language::new(
3723 LanguageConfig::default(),
3724 Some(tree_sitter_rust::LANGUAGE.into()),
3725 )
3726 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3727 .unwrap(),
3728 );
3729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3730
3731 // test when all cursors are not at suggested indent
3732 // then simply move to their suggested indent location
3733 cx.set_state(indoc! {"
3734 const a: B = (
3735 c(
3736 ˇ
3737 ˇ )
3738 );
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 const a: B = (
3743 c(
3744 ˇ
3745 ˇ)
3746 );
3747 "});
3748
3749 // test cursor already at suggested indent not moving when
3750 // other cursors are yet to reach their suggested indents
3751 cx.set_state(indoc! {"
3752 ˇ
3753 const a: B = (
3754 c(
3755 d(
3756 ˇ
3757 )
3758 ˇ
3759 ˇ )
3760 );
3761 "});
3762 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3763 cx.assert_editor_state(indoc! {"
3764 ˇ
3765 const a: B = (
3766 c(
3767 d(
3768 ˇ
3769 )
3770 ˇ
3771 ˇ)
3772 );
3773 "});
3774 // test when all cursors are at suggested indent then tab is inserted
3775 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3776 cx.assert_editor_state(indoc! {"
3777 ˇ
3778 const a: B = (
3779 c(
3780 d(
3781 ˇ
3782 )
3783 ˇ
3784 ˇ)
3785 );
3786 "});
3787
3788 // test when current indent is less than suggested indent,
3789 // we adjust line to match suggested indent and move cursor to it
3790 //
3791 // when no other cursor is at word boundary, all of them should move
3792 cx.set_state(indoc! {"
3793 const a: B = (
3794 c(
3795 d(
3796 ˇ
3797 ˇ )
3798 ˇ )
3799 );
3800 "});
3801 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3802 cx.assert_editor_state(indoc! {"
3803 const a: B = (
3804 c(
3805 d(
3806 ˇ
3807 ˇ)
3808 ˇ)
3809 );
3810 "});
3811
3812 // test when current indent is less than suggested indent,
3813 // we adjust line to match suggested indent and move cursor to it
3814 //
3815 // when some other cursor is at word boundary, it should not move
3816 cx.set_state(indoc! {"
3817 const a: B = (
3818 c(
3819 d(
3820 ˇ
3821 ˇ )
3822 ˇ)
3823 );
3824 "});
3825 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3826 cx.assert_editor_state(indoc! {"
3827 const a: B = (
3828 c(
3829 d(
3830 ˇ
3831 ˇ)
3832 ˇ)
3833 );
3834 "});
3835
3836 // test when current indent is more than suggested indent,
3837 // we just move cursor to current indent instead of suggested indent
3838 //
3839 // when no other cursor is at word boundary, all of them should move
3840 cx.set_state(indoc! {"
3841 const a: B = (
3842 c(
3843 d(
3844 ˇ
3845 ˇ )
3846 ˇ )
3847 );
3848 "});
3849 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3850 cx.assert_editor_state(indoc! {"
3851 const a: B = (
3852 c(
3853 d(
3854 ˇ
3855 ˇ)
3856 ˇ)
3857 );
3858 "});
3859 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3860 cx.assert_editor_state(indoc! {"
3861 const a: B = (
3862 c(
3863 d(
3864 ˇ
3865 ˇ)
3866 ˇ)
3867 );
3868 "});
3869
3870 // test when current indent is more than suggested indent,
3871 // we just move cursor to current indent instead of suggested indent
3872 //
3873 // when some other cursor is at word boundary, it doesn't move
3874 cx.set_state(indoc! {"
3875 const a: B = (
3876 c(
3877 d(
3878 ˇ
3879 ˇ )
3880 ˇ)
3881 );
3882 "});
3883 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3884 cx.assert_editor_state(indoc! {"
3885 const a: B = (
3886 c(
3887 d(
3888 ˇ
3889 ˇ)
3890 ˇ)
3891 );
3892 "});
3893
3894 // handle auto-indent when there are multiple cursors on the same line
3895 cx.set_state(indoc! {"
3896 const a: B = (
3897 c(
3898 ˇ ˇ
3899 ˇ )
3900 );
3901 "});
3902 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3903 cx.assert_editor_state(indoc! {"
3904 const a: B = (
3905 c(
3906 ˇ
3907 ˇ)
3908 );
3909 "});
3910}
3911
3912#[gpui::test]
3913async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3914 init_test(cx, |settings| {
3915 settings.defaults.tab_size = NonZeroU32::new(3)
3916 });
3917
3918 let mut cx = EditorTestContext::new(cx).await;
3919 cx.set_state(indoc! {"
3920 ˇ
3921 \t ˇ
3922 \t ˇ
3923 \t ˇ
3924 \t \t\t \t \t\t \t\t \t \t ˇ
3925 "});
3926
3927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3928 cx.assert_editor_state(indoc! {"
3929 ˇ
3930 \t ˇ
3931 \t ˇ
3932 \t ˇ
3933 \t \t\t \t \t\t \t\t \t \t ˇ
3934 "});
3935}
3936
3937#[gpui::test]
3938async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3939 init_test(cx, |settings| {
3940 settings.defaults.tab_size = NonZeroU32::new(4)
3941 });
3942
3943 let language = Arc::new(
3944 Language::new(
3945 LanguageConfig::default(),
3946 Some(tree_sitter_rust::LANGUAGE.into()),
3947 )
3948 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3949 .unwrap(),
3950 );
3951
3952 let mut cx = EditorTestContext::new(cx).await;
3953 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3954 cx.set_state(indoc! {"
3955 fn a() {
3956 if b {
3957 \t ˇc
3958 }
3959 }
3960 "});
3961
3962 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 fn a() {
3965 if b {
3966 ˇc
3967 }
3968 }
3969 "});
3970}
3971
3972#[gpui::test]
3973async fn test_indent_outdent(cx: &mut TestAppContext) {
3974 init_test(cx, |settings| {
3975 settings.defaults.tab_size = NonZeroU32::new(4);
3976 });
3977
3978 let mut cx = EditorTestContext::new(cx).await;
3979
3980 cx.set_state(indoc! {"
3981 «oneˇ» «twoˇ»
3982 three
3983 four
3984 "});
3985 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3986 cx.assert_editor_state(indoc! {"
3987 «oneˇ» «twoˇ»
3988 three
3989 four
3990 "});
3991
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 «oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998
3999 // select across line ending
4000 cx.set_state(indoc! {"
4001 one two
4002 t«hree
4003 ˇ» four
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 one two
4008 t«hree
4009 ˇ» four
4010 "});
4011
4012 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4013 cx.assert_editor_state(indoc! {"
4014 one two
4015 t«hree
4016 ˇ» four
4017 "});
4018
4019 // Ensure that indenting/outdenting works when the cursor is at column 0.
4020 cx.set_state(indoc! {"
4021 one two
4022 ˇthree
4023 four
4024 "});
4025 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4026 cx.assert_editor_state(indoc! {"
4027 one two
4028 ˇthree
4029 four
4030 "});
4031
4032 cx.set_state(indoc! {"
4033 one two
4034 ˇ three
4035 four
4036 "});
4037 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 ˇthree
4041 four
4042 "});
4043}
4044
4045#[gpui::test]
4046async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4047 // This is a regression test for issue #33761
4048 init_test(cx, |_| {});
4049
4050 let mut cx = EditorTestContext::new(cx).await;
4051 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4053
4054 cx.set_state(
4055 r#"ˇ# ingress:
4056ˇ# api:
4057ˇ# enabled: false
4058ˇ# pathType: Prefix
4059ˇ# console:
4060ˇ# enabled: false
4061ˇ# pathType: Prefix
4062"#,
4063 );
4064
4065 // Press tab to indent all lines
4066 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4067
4068 cx.assert_editor_state(
4069 r#" ˇ# ingress:
4070 ˇ# api:
4071 ˇ# enabled: false
4072 ˇ# pathType: Prefix
4073 ˇ# console:
4074 ˇ# enabled: false
4075 ˇ# pathType: Prefix
4076"#,
4077 );
4078}
4079
4080#[gpui::test]
4081async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4082 // This is a test to make sure our fix for issue #33761 didn't break anything
4083 init_test(cx, |_| {});
4084
4085 let mut cx = EditorTestContext::new(cx).await;
4086 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4087 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4088
4089 cx.set_state(
4090 r#"ˇingress:
4091ˇ api:
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"#,
4106 );
4107}
4108
4109#[gpui::test]
4110async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4111 init_test(cx, |settings| {
4112 settings.defaults.hard_tabs = Some(true);
4113 });
4114
4115 let mut cx = EditorTestContext::new(cx).await;
4116
4117 // select two ranges on one line
4118 cx.set_state(indoc! {"
4119 «oneˇ» «twoˇ»
4120 three
4121 four
4122 "});
4123 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4124 cx.assert_editor_state(indoc! {"
4125 \t«oneˇ» «twoˇ»
4126 three
4127 four
4128 "});
4129 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4130 cx.assert_editor_state(indoc! {"
4131 \t\t«oneˇ» «twoˇ»
4132 three
4133 four
4134 "});
4135 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 \t«oneˇ» «twoˇ»
4138 three
4139 four
4140 "});
4141 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4142 cx.assert_editor_state(indoc! {"
4143 «oneˇ» «twoˇ»
4144 three
4145 four
4146 "});
4147
4148 // select across a line ending
4149 cx.set_state(indoc! {"
4150 one two
4151 t«hree
4152 ˇ»four
4153 "});
4154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4155 cx.assert_editor_state(indoc! {"
4156 one two
4157 \tt«hree
4158 ˇ»four
4159 "});
4160 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4161 cx.assert_editor_state(indoc! {"
4162 one two
4163 \t\tt«hree
4164 ˇ»four
4165 "});
4166 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 one two
4169 \tt«hree
4170 ˇ»four
4171 "});
4172 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 one two
4175 t«hree
4176 ˇ»four
4177 "});
4178
4179 // Ensure that indenting/outdenting works when the cursor is at column 0.
4180 cx.set_state(indoc! {"
4181 one two
4182 ˇthree
4183 four
4184 "});
4185 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4186 cx.assert_editor_state(indoc! {"
4187 one two
4188 ˇthree
4189 four
4190 "});
4191 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 one two
4194 \tˇthree
4195 four
4196 "});
4197 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 ˇthree
4201 four
4202 "});
4203}
4204
4205#[gpui::test]
4206fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4207 init_test(cx, |settings| {
4208 settings.languages.0.extend([
4209 (
4210 "TOML".into(),
4211 LanguageSettingsContent {
4212 tab_size: NonZeroU32::new(2),
4213 ..Default::default()
4214 },
4215 ),
4216 (
4217 "Rust".into(),
4218 LanguageSettingsContent {
4219 tab_size: NonZeroU32::new(4),
4220 ..Default::default()
4221 },
4222 ),
4223 ]);
4224 });
4225
4226 let toml_language = Arc::new(Language::new(
4227 LanguageConfig {
4228 name: "TOML".into(),
4229 ..Default::default()
4230 },
4231 None,
4232 ));
4233 let rust_language = Arc::new(Language::new(
4234 LanguageConfig {
4235 name: "Rust".into(),
4236 ..Default::default()
4237 },
4238 None,
4239 ));
4240
4241 let toml_buffer =
4242 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4243 let rust_buffer =
4244 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4245 let multibuffer = cx.new(|cx| {
4246 let mut multibuffer = MultiBuffer::new(ReadWrite);
4247 multibuffer.push_excerpts(
4248 toml_buffer.clone(),
4249 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4250 cx,
4251 );
4252 multibuffer.push_excerpts(
4253 rust_buffer.clone(),
4254 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4255 cx,
4256 );
4257 multibuffer
4258 });
4259
4260 cx.add_window(|window, cx| {
4261 let mut editor = build_editor(multibuffer, window, cx);
4262
4263 assert_eq!(
4264 editor.text(cx),
4265 indoc! {"
4266 a = 1
4267 b = 2
4268
4269 const c: usize = 3;
4270 "}
4271 );
4272
4273 select_ranges(
4274 &mut editor,
4275 indoc! {"
4276 «aˇ» = 1
4277 b = 2
4278
4279 «const c:ˇ» usize = 3;
4280 "},
4281 window,
4282 cx,
4283 );
4284
4285 editor.tab(&Tab, window, cx);
4286 assert_text_with_selections(
4287 &mut editor,
4288 indoc! {"
4289 «aˇ» = 1
4290 b = 2
4291
4292 «const c:ˇ» usize = 3;
4293 "},
4294 cx,
4295 );
4296 editor.backtab(&Backtab, window, cx);
4297 assert_text_with_selections(
4298 &mut editor,
4299 indoc! {"
4300 «aˇ» = 1
4301 b = 2
4302
4303 «const c:ˇ» usize = 3;
4304 "},
4305 cx,
4306 );
4307
4308 editor
4309 });
4310}
4311
4312#[gpui::test]
4313async fn test_backspace(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // Basic backspace
4319 cx.set_state(indoc! {"
4320 onˇe two three
4321 fou«rˇ» five six
4322 seven «ˇeight nine
4323 »ten
4324 "});
4325 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4326 cx.assert_editor_state(indoc! {"
4327 oˇe two three
4328 fouˇ five six
4329 seven ˇten
4330 "});
4331
4332 // Test backspace inside and around indents
4333 cx.set_state(indoc! {"
4334 zero
4335 ˇone
4336 ˇtwo
4337 ˇ ˇ ˇ three
4338 ˇ ˇ four
4339 "});
4340 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4341 cx.assert_editor_state(indoc! {"
4342 zero
4343 ˇone
4344 ˇtwo
4345 ˇ threeˇ four
4346 "});
4347}
4348
4349#[gpui::test]
4350async fn test_delete(cx: &mut TestAppContext) {
4351 init_test(cx, |_| {});
4352
4353 let mut cx = EditorTestContext::new(cx).await;
4354 cx.set_state(indoc! {"
4355 onˇe two three
4356 fou«rˇ» five six
4357 seven «ˇeight nine
4358 »ten
4359 "});
4360 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4361 cx.assert_editor_state(indoc! {"
4362 onˇ two three
4363 fouˇ five six
4364 seven ˇten
4365 "});
4366}
4367
4368#[gpui::test]
4369fn test_delete_line(cx: &mut TestAppContext) {
4370 init_test(cx, |_| {});
4371
4372 let editor = cx.add_window(|window, cx| {
4373 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4374 build_editor(buffer, window, cx)
4375 });
4376 _ = editor.update(cx, |editor, window, cx| {
4377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4378 s.select_display_ranges([
4379 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4380 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4381 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4382 ])
4383 });
4384 editor.delete_line(&DeleteLine, window, cx);
4385 assert_eq!(editor.display_text(cx), "ghi");
4386 assert_eq!(
4387 editor.selections.display_ranges(cx),
4388 vec![
4389 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4390 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4391 ]
4392 );
4393 });
4394
4395 let editor = cx.add_window(|window, cx| {
4396 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4397 build_editor(buffer, window, cx)
4398 });
4399 _ = editor.update(cx, |editor, window, cx| {
4400 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4401 s.select_display_ranges([
4402 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4403 ])
4404 });
4405 editor.delete_line(&DeleteLine, window, cx);
4406 assert_eq!(editor.display_text(cx), "ghi\n");
4407 assert_eq!(
4408 editor.selections.display_ranges(cx),
4409 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4410 );
4411 });
4412
4413 let editor = cx.add_window(|window, cx| {
4414 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4415 build_editor(buffer, window, cx)
4416 });
4417 _ = editor.update(cx, |editor, window, cx| {
4418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4419 s.select_display_ranges([
4420 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4421 ])
4422 });
4423 editor.delete_line(&DeleteLine, window, cx);
4424 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4425 assert_eq!(
4426 editor.selections.display_ranges(cx),
4427 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4428 );
4429 });
4430}
4431
4432#[gpui::test]
4433fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4434 init_test(cx, |_| {});
4435
4436 cx.add_window(|window, cx| {
4437 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4438 let mut editor = build_editor(buffer.clone(), window, cx);
4439 let buffer = buffer.read(cx).as_singleton().unwrap();
4440
4441 assert_eq!(
4442 editor.selections.ranges::<Point>(cx),
4443 &[Point::new(0, 0)..Point::new(0, 0)]
4444 );
4445
4446 // When on single line, replace newline at end by space
4447 editor.join_lines(&JoinLines, window, cx);
4448 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4449 assert_eq!(
4450 editor.selections.ranges::<Point>(cx),
4451 &[Point::new(0, 3)..Point::new(0, 3)]
4452 );
4453
4454 // When multiple lines are selected, remove newlines that are spanned by the selection
4455 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4456 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4457 });
4458 editor.join_lines(&JoinLines, window, cx);
4459 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4460 assert_eq!(
4461 editor.selections.ranges::<Point>(cx),
4462 &[Point::new(0, 11)..Point::new(0, 11)]
4463 );
4464
4465 // Undo should be transactional
4466 editor.undo(&Undo, window, cx);
4467 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4468 assert_eq!(
4469 editor.selections.ranges::<Point>(cx),
4470 &[Point::new(0, 5)..Point::new(2, 2)]
4471 );
4472
4473 // When joining an empty line don't insert a space
4474 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4475 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4476 });
4477 editor.join_lines(&JoinLines, window, cx);
4478 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4479 assert_eq!(
4480 editor.selections.ranges::<Point>(cx),
4481 [Point::new(2, 3)..Point::new(2, 3)]
4482 );
4483
4484 // We can remove trailing newlines
4485 editor.join_lines(&JoinLines, window, cx);
4486 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4487 assert_eq!(
4488 editor.selections.ranges::<Point>(cx),
4489 [Point::new(2, 3)..Point::new(2, 3)]
4490 );
4491
4492 // We don't blow up on the last line
4493 editor.join_lines(&JoinLines, window, cx);
4494 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4495 assert_eq!(
4496 editor.selections.ranges::<Point>(cx),
4497 [Point::new(2, 3)..Point::new(2, 3)]
4498 );
4499
4500 // reset to test indentation
4501 editor.buffer.update(cx, |buffer, cx| {
4502 buffer.edit(
4503 [
4504 (Point::new(1, 0)..Point::new(1, 2), " "),
4505 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4506 ],
4507 None,
4508 cx,
4509 )
4510 });
4511
4512 // We remove any leading spaces
4513 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4515 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4516 });
4517 editor.join_lines(&JoinLines, window, cx);
4518 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4519
4520 // We don't insert a space for a line containing only spaces
4521 editor.join_lines(&JoinLines, window, cx);
4522 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4523
4524 // We ignore any leading tabs
4525 editor.join_lines(&JoinLines, window, cx);
4526 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4527
4528 editor
4529 });
4530}
4531
4532#[gpui::test]
4533fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4534 init_test(cx, |_| {});
4535
4536 cx.add_window(|window, cx| {
4537 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4538 let mut editor = build_editor(buffer.clone(), window, cx);
4539 let buffer = buffer.read(cx).as_singleton().unwrap();
4540
4541 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4542 s.select_ranges([
4543 Point::new(0, 2)..Point::new(1, 1),
4544 Point::new(1, 2)..Point::new(1, 2),
4545 Point::new(3, 1)..Point::new(3, 2),
4546 ])
4547 });
4548
4549 editor.join_lines(&JoinLines, window, cx);
4550 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4551
4552 assert_eq!(
4553 editor.selections.ranges::<Point>(cx),
4554 [
4555 Point::new(0, 7)..Point::new(0, 7),
4556 Point::new(1, 3)..Point::new(1, 3)
4557 ]
4558 );
4559 editor
4560 });
4561}
4562
4563#[gpui::test]
4564async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4565 init_test(cx, |_| {});
4566
4567 let mut cx = EditorTestContext::new(cx).await;
4568
4569 let diff_base = r#"
4570 Line 0
4571 Line 1
4572 Line 2
4573 Line 3
4574 "#
4575 .unindent();
4576
4577 cx.set_state(
4578 &r#"
4579 ˇLine 0
4580 Line 1
4581 Line 2
4582 Line 3
4583 "#
4584 .unindent(),
4585 );
4586
4587 cx.set_head_text(&diff_base);
4588 executor.run_until_parked();
4589
4590 // Join lines
4591 cx.update_editor(|editor, window, cx| {
4592 editor.join_lines(&JoinLines, window, cx);
4593 });
4594 executor.run_until_parked();
4595
4596 cx.assert_editor_state(
4597 &r#"
4598 Line 0ˇ Line 1
4599 Line 2
4600 Line 3
4601 "#
4602 .unindent(),
4603 );
4604 // Join again
4605 cx.update_editor(|editor, window, cx| {
4606 editor.join_lines(&JoinLines, window, cx);
4607 });
4608 executor.run_until_parked();
4609
4610 cx.assert_editor_state(
4611 &r#"
4612 Line 0 Line 1ˇ Line 2
4613 Line 3
4614 "#
4615 .unindent(),
4616 );
4617}
4618
4619#[gpui::test]
4620async fn test_custom_newlines_cause_no_false_positive_diffs(
4621 executor: BackgroundExecutor,
4622 cx: &mut TestAppContext,
4623) {
4624 init_test(cx, |_| {});
4625 let mut cx = EditorTestContext::new(cx).await;
4626 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4627 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4628 executor.run_until_parked();
4629
4630 cx.update_editor(|editor, window, cx| {
4631 let snapshot = editor.snapshot(window, cx);
4632 assert_eq!(
4633 snapshot
4634 .buffer_snapshot()
4635 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4636 .collect::<Vec<_>>(),
4637 Vec::new(),
4638 "Should not have any diffs for files with custom newlines"
4639 );
4640 });
4641}
4642
4643#[gpui::test]
4644async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4645 init_test(cx, |_| {});
4646
4647 let mut cx = EditorTestContext::new(cx).await;
4648
4649 // Test sort_lines_case_insensitive()
4650 cx.set_state(indoc! {"
4651 «z
4652 y
4653 x
4654 Z
4655 Y
4656 Xˇ»
4657 "});
4658 cx.update_editor(|e, window, cx| {
4659 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4660 });
4661 cx.assert_editor_state(indoc! {"
4662 «x
4663 X
4664 y
4665 Y
4666 z
4667 Zˇ»
4668 "});
4669
4670 // Test sort_lines_by_length()
4671 //
4672 // Demonstrates:
4673 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4674 // - sort is stable
4675 cx.set_state(indoc! {"
4676 «123
4677 æ
4678 12
4679 ∞
4680 1
4681 æˇ»
4682 "});
4683 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4684 cx.assert_editor_state(indoc! {"
4685 «æ
4686 ∞
4687 1
4688 æ
4689 12
4690 123ˇ»
4691 "});
4692
4693 // Test reverse_lines()
4694 cx.set_state(indoc! {"
4695 «5
4696 4
4697 3
4698 2
4699 1ˇ»
4700 "});
4701 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4702 cx.assert_editor_state(indoc! {"
4703 «1
4704 2
4705 3
4706 4
4707 5ˇ»
4708 "});
4709
4710 // Skip testing shuffle_line()
4711
4712 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4713 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4714
4715 // Don't manipulate when cursor is on single line, but expand the selection
4716 cx.set_state(indoc! {"
4717 ddˇdd
4718 ccc
4719 bb
4720 a
4721 "});
4722 cx.update_editor(|e, window, cx| {
4723 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4724 });
4725 cx.assert_editor_state(indoc! {"
4726 «ddddˇ»
4727 ccc
4728 bb
4729 a
4730 "});
4731
4732 // Basic manipulate case
4733 // Start selection moves to column 0
4734 // End of selection shrinks to fit shorter line
4735 cx.set_state(indoc! {"
4736 dd«d
4737 ccc
4738 bb
4739 aaaaaˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| {
4742 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4743 });
4744 cx.assert_editor_state(indoc! {"
4745 «aaaaa
4746 bb
4747 ccc
4748 dddˇ»
4749 "});
4750
4751 // Manipulate case with newlines
4752 cx.set_state(indoc! {"
4753 dd«d
4754 ccc
4755
4756 bb
4757 aaaaa
4758
4759 ˇ»
4760 "});
4761 cx.update_editor(|e, window, cx| {
4762 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4763 });
4764 cx.assert_editor_state(indoc! {"
4765 «
4766
4767 aaaaa
4768 bb
4769 ccc
4770 dddˇ»
4771
4772 "});
4773
4774 // Adding new line
4775 cx.set_state(indoc! {"
4776 aa«a
4777 bbˇ»b
4778 "});
4779 cx.update_editor(|e, window, cx| {
4780 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4781 });
4782 cx.assert_editor_state(indoc! {"
4783 «aaa
4784 bbb
4785 added_lineˇ»
4786 "});
4787
4788 // Removing line
4789 cx.set_state(indoc! {"
4790 aa«a
4791 bbbˇ»
4792 "});
4793 cx.update_editor(|e, window, cx| {
4794 e.manipulate_immutable_lines(window, cx, |lines| {
4795 lines.pop();
4796 })
4797 });
4798 cx.assert_editor_state(indoc! {"
4799 «aaaˇ»
4800 "});
4801
4802 // Removing all lines
4803 cx.set_state(indoc! {"
4804 aa«a
4805 bbbˇ»
4806 "});
4807 cx.update_editor(|e, window, cx| {
4808 e.manipulate_immutable_lines(window, cx, |lines| {
4809 lines.drain(..);
4810 })
4811 });
4812 cx.assert_editor_state(indoc! {"
4813 ˇ
4814 "});
4815}
4816
4817#[gpui::test]
4818async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4819 init_test(cx, |_| {});
4820
4821 let mut cx = EditorTestContext::new(cx).await;
4822
4823 // Consider continuous selection as single selection
4824 cx.set_state(indoc! {"
4825 Aaa«aa
4826 cˇ»c«c
4827 bb
4828 aaaˇ»aa
4829 "});
4830 cx.update_editor(|e, window, cx| {
4831 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4832 });
4833 cx.assert_editor_state(indoc! {"
4834 «Aaaaa
4835 ccc
4836 bb
4837 aaaaaˇ»
4838 "});
4839
4840 cx.set_state(indoc! {"
4841 Aaa«aa
4842 cˇ»c«c
4843 bb
4844 aaaˇ»aa
4845 "});
4846 cx.update_editor(|e, window, cx| {
4847 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4848 });
4849 cx.assert_editor_state(indoc! {"
4850 «Aaaaa
4851 ccc
4852 bbˇ»
4853 "});
4854
4855 // Consider non continuous selection as distinct dedup operations
4856 cx.set_state(indoc! {"
4857 «aaaaa
4858 bb
4859 aaaaa
4860 aaaaaˇ»
4861
4862 aaa«aaˇ»
4863 "});
4864 cx.update_editor(|e, window, cx| {
4865 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4866 });
4867 cx.assert_editor_state(indoc! {"
4868 «aaaaa
4869 bbˇ»
4870
4871 «aaaaaˇ»
4872 "});
4873}
4874
4875#[gpui::test]
4876async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4877 init_test(cx, |_| {});
4878
4879 let mut cx = EditorTestContext::new(cx).await;
4880
4881 cx.set_state(indoc! {"
4882 «Aaa
4883 aAa
4884 Aaaˇ»
4885 "});
4886 cx.update_editor(|e, window, cx| {
4887 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4888 });
4889 cx.assert_editor_state(indoc! {"
4890 «Aaa
4891 aAaˇ»
4892 "});
4893
4894 cx.set_state(indoc! {"
4895 «Aaa
4896 aAa
4897 aaAˇ»
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «Aaaˇ»
4904 "});
4905}
4906
4907#[gpui::test]
4908async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4909 init_test(cx, |_| {});
4910
4911 let mut cx = EditorTestContext::new(cx).await;
4912
4913 let js_language = Arc::new(Language::new(
4914 LanguageConfig {
4915 name: "JavaScript".into(),
4916 wrap_characters: Some(language::WrapCharactersConfig {
4917 start_prefix: "<".into(),
4918 start_suffix: ">".into(),
4919 end_prefix: "</".into(),
4920 end_suffix: ">".into(),
4921 }),
4922 ..LanguageConfig::default()
4923 },
4924 None,
4925 ));
4926
4927 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4928
4929 cx.set_state(indoc! {"
4930 «testˇ»
4931 "});
4932 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4933 cx.assert_editor_state(indoc! {"
4934 <«ˇ»>test</«ˇ»>
4935 "});
4936
4937 cx.set_state(indoc! {"
4938 «test
4939 testˇ»
4940 "});
4941 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4942 cx.assert_editor_state(indoc! {"
4943 <«ˇ»>test
4944 test</«ˇ»>
4945 "});
4946
4947 cx.set_state(indoc! {"
4948 teˇst
4949 "});
4950 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4951 cx.assert_editor_state(indoc! {"
4952 te<«ˇ»></«ˇ»>st
4953 "});
4954}
4955
4956#[gpui::test]
4957async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4958 init_test(cx, |_| {});
4959
4960 let mut cx = EditorTestContext::new(cx).await;
4961
4962 let js_language = Arc::new(Language::new(
4963 LanguageConfig {
4964 name: "JavaScript".into(),
4965 wrap_characters: Some(language::WrapCharactersConfig {
4966 start_prefix: "<".into(),
4967 start_suffix: ">".into(),
4968 end_prefix: "</".into(),
4969 end_suffix: ">".into(),
4970 }),
4971 ..LanguageConfig::default()
4972 },
4973 None,
4974 ));
4975
4976 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4977
4978 cx.set_state(indoc! {"
4979 «testˇ»
4980 «testˇ» «testˇ»
4981 «testˇ»
4982 "});
4983 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4984 cx.assert_editor_state(indoc! {"
4985 <«ˇ»>test</«ˇ»>
4986 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4987 <«ˇ»>test</«ˇ»>
4988 "});
4989
4990 cx.set_state(indoc! {"
4991 «test
4992 testˇ»
4993 «test
4994 testˇ»
4995 "});
4996 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4997 cx.assert_editor_state(indoc! {"
4998 <«ˇ»>test
4999 test</«ˇ»>
5000 <«ˇ»>test
5001 test</«ˇ»>
5002 "});
5003}
5004
5005#[gpui::test]
5006async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5007 init_test(cx, |_| {});
5008
5009 let mut cx = EditorTestContext::new(cx).await;
5010
5011 let plaintext_language = Arc::new(Language::new(
5012 LanguageConfig {
5013 name: "Plain Text".into(),
5014 ..LanguageConfig::default()
5015 },
5016 None,
5017 ));
5018
5019 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5020
5021 cx.set_state(indoc! {"
5022 «testˇ»
5023 "});
5024 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5025 cx.assert_editor_state(indoc! {"
5026 «testˇ»
5027 "});
5028}
5029
5030#[gpui::test]
5031async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5032 init_test(cx, |_| {});
5033
5034 let mut cx = EditorTestContext::new(cx).await;
5035
5036 // Manipulate with multiple selections on a single line
5037 cx.set_state(indoc! {"
5038 dd«dd
5039 cˇ»c«c
5040 bb
5041 aaaˇ»aa
5042 "});
5043 cx.update_editor(|e, window, cx| {
5044 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5045 });
5046 cx.assert_editor_state(indoc! {"
5047 «aaaaa
5048 bb
5049 ccc
5050 ddddˇ»
5051 "});
5052
5053 // Manipulate with multiple disjoin selections
5054 cx.set_state(indoc! {"
5055 5«
5056 4
5057 3
5058 2
5059 1ˇ»
5060
5061 dd«dd
5062 ccc
5063 bb
5064 aaaˇ»aa
5065 "});
5066 cx.update_editor(|e, window, cx| {
5067 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5068 });
5069 cx.assert_editor_state(indoc! {"
5070 «1
5071 2
5072 3
5073 4
5074 5ˇ»
5075
5076 «aaaaa
5077 bb
5078 ccc
5079 ddddˇ»
5080 "});
5081
5082 // Adding lines on each selection
5083 cx.set_state(indoc! {"
5084 2«
5085 1ˇ»
5086
5087 bb«bb
5088 aaaˇ»aa
5089 "});
5090 cx.update_editor(|e, window, cx| {
5091 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5092 });
5093 cx.assert_editor_state(indoc! {"
5094 «2
5095 1
5096 added lineˇ»
5097
5098 «bbbb
5099 aaaaa
5100 added lineˇ»
5101 "});
5102
5103 // Removing lines on each selection
5104 cx.set_state(indoc! {"
5105 2«
5106 1ˇ»
5107
5108 bb«bb
5109 aaaˇ»aa
5110 "});
5111 cx.update_editor(|e, window, cx| {
5112 e.manipulate_immutable_lines(window, cx, |lines| {
5113 lines.pop();
5114 })
5115 });
5116 cx.assert_editor_state(indoc! {"
5117 «2ˇ»
5118
5119 «bbbbˇ»
5120 "});
5121}
5122
5123#[gpui::test]
5124async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5125 init_test(cx, |settings| {
5126 settings.defaults.tab_size = NonZeroU32::new(3)
5127 });
5128
5129 let mut cx = EditorTestContext::new(cx).await;
5130
5131 // MULTI SELECTION
5132 // Ln.1 "«" tests empty lines
5133 // Ln.9 tests just leading whitespace
5134 cx.set_state(indoc! {"
5135 «
5136 abc // No indentationˇ»
5137 «\tabc // 1 tabˇ»
5138 \t\tabc « ˇ» // 2 tabs
5139 \t ab«c // Tab followed by space
5140 \tabc // Space followed by tab (3 spaces should be the result)
5141 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5142 abˇ»ˇc ˇ ˇ // Already space indented«
5143 \t
5144 \tabc\tdef // Only the leading tab is manipulatedˇ»
5145 "});
5146 cx.update_editor(|e, window, cx| {
5147 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5148 });
5149 cx.assert_editor_state(
5150 indoc! {"
5151 «
5152 abc // No indentation
5153 abc // 1 tab
5154 abc // 2 tabs
5155 abc // Tab followed by space
5156 abc // Space followed by tab (3 spaces should be the result)
5157 abc // Mixed indentation (tab conversion depends on the column)
5158 abc // Already space indented
5159 ·
5160 abc\tdef // Only the leading tab is manipulatedˇ»
5161 "}
5162 .replace("·", "")
5163 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5164 );
5165
5166 // Test on just a few lines, the others should remain unchanged
5167 // Only lines (3, 5, 10, 11) should change
5168 cx.set_state(
5169 indoc! {"
5170 ·
5171 abc // No indentation
5172 \tabcˇ // 1 tab
5173 \t\tabc // 2 tabs
5174 \t abcˇ // Tab followed by space
5175 \tabc // Space followed by tab (3 spaces should be the result)
5176 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5177 abc // Already space indented
5178 «\t
5179 \tabc\tdef // Only the leading tab is manipulatedˇ»
5180 "}
5181 .replace("·", "")
5182 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5183 );
5184 cx.update_editor(|e, window, cx| {
5185 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5186 });
5187 cx.assert_editor_state(
5188 indoc! {"
5189 ·
5190 abc // No indentation
5191 « abc // 1 tabˇ»
5192 \t\tabc // 2 tabs
5193 « abc // Tab followed by spaceˇ»
5194 \tabc // Space followed by tab (3 spaces should be the result)
5195 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5196 abc // Already space indented
5197 « ·
5198 abc\tdef // Only the leading tab is manipulatedˇ»
5199 "}
5200 .replace("·", "")
5201 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5202 );
5203
5204 // SINGLE SELECTION
5205 // Ln.1 "«" tests empty lines
5206 // Ln.9 tests just leading whitespace
5207 cx.set_state(indoc! {"
5208 «
5209 abc // No indentation
5210 \tabc // 1 tab
5211 \t\tabc // 2 tabs
5212 \t abc // Tab followed by space
5213 \tabc // Space followed by tab (3 spaces should be the result)
5214 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5215 abc // Already space indented
5216 \t
5217 \tabc\tdef // Only the leading tab is manipulatedˇ»
5218 "});
5219 cx.update_editor(|e, window, cx| {
5220 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5221 });
5222 cx.assert_editor_state(
5223 indoc! {"
5224 «
5225 abc // No indentation
5226 abc // 1 tab
5227 abc // 2 tabs
5228 abc // Tab followed by space
5229 abc // Space followed by tab (3 spaces should be the result)
5230 abc // Mixed indentation (tab conversion depends on the column)
5231 abc // Already space indented
5232 ·
5233 abc\tdef // Only the leading tab is manipulatedˇ»
5234 "}
5235 .replace("·", "")
5236 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5237 );
5238}
5239
5240#[gpui::test]
5241async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5242 init_test(cx, |settings| {
5243 settings.defaults.tab_size = NonZeroU32::new(3)
5244 });
5245
5246 let mut cx = EditorTestContext::new(cx).await;
5247
5248 // MULTI SELECTION
5249 // Ln.1 "«" tests empty lines
5250 // Ln.11 tests just leading whitespace
5251 cx.set_state(indoc! {"
5252 «
5253 abˇ»ˇc // No indentation
5254 abc ˇ ˇ // 1 space (< 3 so dont convert)
5255 abc « // 2 spaces (< 3 so dont convert)
5256 abc // 3 spaces (convert)
5257 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5258 «\tˇ»\t«\tˇ»abc // Already tab indented
5259 «\t abc // Tab followed by space
5260 \tabc // Space followed by tab (should be consumed due to tab)
5261 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5262 \tˇ» «\t
5263 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5264 "});
5265 cx.update_editor(|e, window, cx| {
5266 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5267 });
5268 cx.assert_editor_state(indoc! {"
5269 «
5270 abc // No indentation
5271 abc // 1 space (< 3 so dont convert)
5272 abc // 2 spaces (< 3 so dont convert)
5273 \tabc // 3 spaces (convert)
5274 \t abc // 5 spaces (1 tab + 2 spaces)
5275 \t\t\tabc // Already tab indented
5276 \t abc // Tab followed by space
5277 \tabc // Space followed by tab (should be consumed due to tab)
5278 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5279 \t\t\t
5280 \tabc \t // Only the leading spaces should be convertedˇ»
5281 "});
5282
5283 // Test on just a few lines, the other should remain unchanged
5284 // Only lines (4, 8, 11, 12) should change
5285 cx.set_state(
5286 indoc! {"
5287 ·
5288 abc // No indentation
5289 abc // 1 space (< 3 so dont convert)
5290 abc // 2 spaces (< 3 so dont convert)
5291 « abc // 3 spaces (convert)ˇ»
5292 abc // 5 spaces (1 tab + 2 spaces)
5293 \t\t\tabc // Already tab indented
5294 \t abc // Tab followed by space
5295 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5296 \t\t \tabc // Mixed indentation
5297 \t \t \t \tabc // Mixed indentation
5298 \t \tˇ
5299 « abc \t // Only the leading spaces should be convertedˇ»
5300 "}
5301 .replace("·", "")
5302 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5303 );
5304 cx.update_editor(|e, window, cx| {
5305 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5306 });
5307 cx.assert_editor_state(
5308 indoc! {"
5309 ·
5310 abc // No indentation
5311 abc // 1 space (< 3 so dont convert)
5312 abc // 2 spaces (< 3 so dont convert)
5313 «\tabc // 3 spaces (convert)ˇ»
5314 abc // 5 spaces (1 tab + 2 spaces)
5315 \t\t\tabc // Already tab indented
5316 \t abc // Tab followed by space
5317 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5318 \t\t \tabc // Mixed indentation
5319 \t \t \t \tabc // Mixed indentation
5320 «\t\t\t
5321 \tabc \t // Only the leading spaces should be convertedˇ»
5322 "}
5323 .replace("·", "")
5324 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5325 );
5326
5327 // SINGLE SELECTION
5328 // Ln.1 "«" tests empty lines
5329 // Ln.11 tests just leading whitespace
5330 cx.set_state(indoc! {"
5331 «
5332 abc // No indentation
5333 abc // 1 space (< 3 so dont convert)
5334 abc // 2 spaces (< 3 so dont convert)
5335 abc // 3 spaces (convert)
5336 abc // 5 spaces (1 tab + 2 spaces)
5337 \t\t\tabc // Already tab indented
5338 \t abc // Tab followed by space
5339 \tabc // Space followed by tab (should be consumed due to tab)
5340 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5341 \t \t
5342 abc \t // Only the leading spaces should be convertedˇ»
5343 "});
5344 cx.update_editor(|e, window, cx| {
5345 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5346 });
5347 cx.assert_editor_state(indoc! {"
5348 «
5349 abc // No indentation
5350 abc // 1 space (< 3 so dont convert)
5351 abc // 2 spaces (< 3 so dont convert)
5352 \tabc // 3 spaces (convert)
5353 \t abc // 5 spaces (1 tab + 2 spaces)
5354 \t\t\tabc // Already tab indented
5355 \t abc // Tab followed by space
5356 \tabc // Space followed by tab (should be consumed due to tab)
5357 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5358 \t\t\t
5359 \tabc \t // Only the leading spaces should be convertedˇ»
5360 "});
5361}
5362
5363#[gpui::test]
5364async fn test_toggle_case(cx: &mut TestAppContext) {
5365 init_test(cx, |_| {});
5366
5367 let mut cx = EditorTestContext::new(cx).await;
5368
5369 // If all lower case -> upper case
5370 cx.set_state(indoc! {"
5371 «hello worldˇ»
5372 "});
5373 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5374 cx.assert_editor_state(indoc! {"
5375 «HELLO WORLDˇ»
5376 "});
5377
5378 // If all upper case -> lower case
5379 cx.set_state(indoc! {"
5380 «HELLO WORLDˇ»
5381 "});
5382 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5383 cx.assert_editor_state(indoc! {"
5384 «hello worldˇ»
5385 "});
5386
5387 // If any upper case characters are identified -> lower case
5388 // This matches JetBrains IDEs
5389 cx.set_state(indoc! {"
5390 «hEllo worldˇ»
5391 "});
5392 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5393 cx.assert_editor_state(indoc! {"
5394 «hello worldˇ»
5395 "});
5396}
5397
5398#[gpui::test]
5399async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5400 init_test(cx, |_| {});
5401
5402 let mut cx = EditorTestContext::new(cx).await;
5403
5404 cx.set_state(indoc! {"
5405 «implement-windows-supportˇ»
5406 "});
5407 cx.update_editor(|e, window, cx| {
5408 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5409 });
5410 cx.assert_editor_state(indoc! {"
5411 «Implement windows supportˇ»
5412 "});
5413}
5414
5415#[gpui::test]
5416async fn test_manipulate_text(cx: &mut TestAppContext) {
5417 init_test(cx, |_| {});
5418
5419 let mut cx = EditorTestContext::new(cx).await;
5420
5421 // Test convert_to_upper_case()
5422 cx.set_state(indoc! {"
5423 «hello worldˇ»
5424 "});
5425 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5426 cx.assert_editor_state(indoc! {"
5427 «HELLO WORLDˇ»
5428 "});
5429
5430 // Test convert_to_lower_case()
5431 cx.set_state(indoc! {"
5432 «HELLO WORLDˇ»
5433 "});
5434 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5435 cx.assert_editor_state(indoc! {"
5436 «hello worldˇ»
5437 "});
5438
5439 // Test multiple line, single selection case
5440 cx.set_state(indoc! {"
5441 «The quick brown
5442 fox jumps over
5443 the lazy dogˇ»
5444 "});
5445 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5446 cx.assert_editor_state(indoc! {"
5447 «The Quick Brown
5448 Fox Jumps Over
5449 The Lazy Dogˇ»
5450 "});
5451
5452 // Test multiple line, single selection case
5453 cx.set_state(indoc! {"
5454 «The quick brown
5455 fox jumps over
5456 the lazy dogˇ»
5457 "});
5458 cx.update_editor(|e, window, cx| {
5459 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5460 });
5461 cx.assert_editor_state(indoc! {"
5462 «TheQuickBrown
5463 FoxJumpsOver
5464 TheLazyDogˇ»
5465 "});
5466
5467 // From here on out, test more complex cases of manipulate_text()
5468
5469 // Test no selection case - should affect words cursors are in
5470 // Cursor at beginning, middle, and end of word
5471 cx.set_state(indoc! {"
5472 ˇhello big beauˇtiful worldˇ
5473 "});
5474 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5475 cx.assert_editor_state(indoc! {"
5476 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5477 "});
5478
5479 // Test multiple selections on a single line and across multiple lines
5480 cx.set_state(indoc! {"
5481 «Theˇ» quick «brown
5482 foxˇ» jumps «overˇ»
5483 the «lazyˇ» dog
5484 "});
5485 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5486 cx.assert_editor_state(indoc! {"
5487 «THEˇ» quick «BROWN
5488 FOXˇ» jumps «OVERˇ»
5489 the «LAZYˇ» dog
5490 "});
5491
5492 // Test case where text length grows
5493 cx.set_state(indoc! {"
5494 «tschüߡ»
5495 "});
5496 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5497 cx.assert_editor_state(indoc! {"
5498 «TSCHÜSSˇ»
5499 "});
5500
5501 // Test to make sure we don't crash when text shrinks
5502 cx.set_state(indoc! {"
5503 aaa_bbbˇ
5504 "});
5505 cx.update_editor(|e, window, cx| {
5506 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5507 });
5508 cx.assert_editor_state(indoc! {"
5509 «aaaBbbˇ»
5510 "});
5511
5512 // Test to make sure we all aware of the fact that each word can grow and shrink
5513 // Final selections should be aware of this fact
5514 cx.set_state(indoc! {"
5515 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5516 "});
5517 cx.update_editor(|e, window, cx| {
5518 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5519 });
5520 cx.assert_editor_state(indoc! {"
5521 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5522 "});
5523
5524 cx.set_state(indoc! {"
5525 «hElLo, WoRld!ˇ»
5526 "});
5527 cx.update_editor(|e, window, cx| {
5528 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5529 });
5530 cx.assert_editor_state(indoc! {"
5531 «HeLlO, wOrLD!ˇ»
5532 "});
5533
5534 // Test selections with `line_mode() = true`.
5535 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5536 cx.set_state(indoc! {"
5537 «The quick brown
5538 fox jumps over
5539 tˇ»he lazy dog
5540 "});
5541 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5542 cx.assert_editor_state(indoc! {"
5543 «THE QUICK BROWN
5544 FOX JUMPS OVER
5545 THE LAZY DOGˇ»
5546 "});
5547}
5548
5549#[gpui::test]
5550fn test_duplicate_line(cx: &mut TestAppContext) {
5551 init_test(cx, |_| {});
5552
5553 let editor = cx.add_window(|window, cx| {
5554 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5555 build_editor(buffer, window, cx)
5556 });
5557 _ = editor.update(cx, |editor, window, cx| {
5558 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5559 s.select_display_ranges([
5560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5563 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5564 ])
5565 });
5566 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5567 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5568 assert_eq!(
5569 editor.selections.display_ranges(cx),
5570 vec![
5571 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5573 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5574 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5575 ]
5576 );
5577 });
5578
5579 let editor = cx.add_window(|window, cx| {
5580 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5581 build_editor(buffer, window, cx)
5582 });
5583 _ = editor.update(cx, |editor, window, cx| {
5584 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5585 s.select_display_ranges([
5586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5587 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5588 ])
5589 });
5590 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5591 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5592 assert_eq!(
5593 editor.selections.display_ranges(cx),
5594 vec![
5595 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5596 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5597 ]
5598 );
5599 });
5600
5601 // With `move_upwards` the selections stay in place, except for
5602 // the lines inserted above them
5603 let editor = cx.add_window(|window, cx| {
5604 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5605 build_editor(buffer, window, cx)
5606 });
5607 _ = editor.update(cx, |editor, window, cx| {
5608 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5609 s.select_display_ranges([
5610 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5611 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5612 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5613 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5614 ])
5615 });
5616 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5617 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5618 assert_eq!(
5619 editor.selections.display_ranges(cx),
5620 vec![
5621 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5622 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5623 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5624 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5625 ]
5626 );
5627 });
5628
5629 let editor = cx.add_window(|window, cx| {
5630 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5631 build_editor(buffer, window, cx)
5632 });
5633 _ = editor.update(cx, |editor, window, cx| {
5634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5635 s.select_display_ranges([
5636 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5637 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5638 ])
5639 });
5640 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5641 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5642 assert_eq!(
5643 editor.selections.display_ranges(cx),
5644 vec![
5645 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5646 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5647 ]
5648 );
5649 });
5650
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), 1)..DisplayPoint::new(DisplayRow(1), 1),
5659 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5660 ])
5661 });
5662 editor.duplicate_selection(&DuplicateSelection, window, cx);
5663 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5664 assert_eq!(
5665 editor.selections.display_ranges(cx),
5666 vec![
5667 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5668 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5669 ]
5670 );
5671 });
5672}
5673
5674#[gpui::test]
5675fn test_move_line_up_down(cx: &mut TestAppContext) {
5676 init_test(cx, |_| {});
5677
5678 let editor = cx.add_window(|window, cx| {
5679 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5680 build_editor(buffer, window, cx)
5681 });
5682 _ = editor.update(cx, |editor, window, cx| {
5683 editor.fold_creases(
5684 vec![
5685 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5686 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5687 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5688 ],
5689 true,
5690 window,
5691 cx,
5692 );
5693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5694 s.select_display_ranges([
5695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5696 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5697 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5698 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5699 ])
5700 });
5701 assert_eq!(
5702 editor.display_text(cx),
5703 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5704 );
5705
5706 editor.move_line_up(&MoveLineUp, window, cx);
5707 assert_eq!(
5708 editor.display_text(cx),
5709 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5710 );
5711 assert_eq!(
5712 editor.selections.display_ranges(cx),
5713 vec![
5714 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5715 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5716 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5717 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5718 ]
5719 );
5720 });
5721
5722 _ = editor.update(cx, |editor, window, cx| {
5723 editor.move_line_down(&MoveLineDown, window, cx);
5724 assert_eq!(
5725 editor.display_text(cx),
5726 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5727 );
5728 assert_eq!(
5729 editor.selections.display_ranges(cx),
5730 vec![
5731 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5732 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5733 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5734 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5735 ]
5736 );
5737 });
5738
5739 _ = editor.update(cx, |editor, window, cx| {
5740 editor.move_line_down(&MoveLineDown, window, cx);
5741 assert_eq!(
5742 editor.display_text(cx),
5743 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5744 );
5745 assert_eq!(
5746 editor.selections.display_ranges(cx),
5747 vec![
5748 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5749 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5750 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5751 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5752 ]
5753 );
5754 });
5755
5756 _ = editor.update(cx, |editor, window, cx| {
5757 editor.move_line_up(&MoveLineUp, window, cx);
5758 assert_eq!(
5759 editor.display_text(cx),
5760 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5761 );
5762 assert_eq!(
5763 editor.selections.display_ranges(cx),
5764 vec![
5765 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5766 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5767 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5768 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5769 ]
5770 );
5771 });
5772}
5773
5774#[gpui::test]
5775fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5776 init_test(cx, |_| {});
5777 let editor = cx.add_window(|window, cx| {
5778 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5779 build_editor(buffer, window, cx)
5780 });
5781 _ = editor.update(cx, |editor, window, cx| {
5782 editor.fold_creases(
5783 vec![Crease::simple(
5784 Point::new(6, 4)..Point::new(7, 4),
5785 FoldPlaceholder::test(),
5786 )],
5787 true,
5788 window,
5789 cx,
5790 );
5791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5792 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5793 });
5794 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5795 editor.move_line_up(&MoveLineUp, window, cx);
5796 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5797 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5798 });
5799}
5800
5801#[gpui::test]
5802fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5803 init_test(cx, |_| {});
5804
5805 let editor = cx.add_window(|window, cx| {
5806 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5807 build_editor(buffer, window, cx)
5808 });
5809 _ = editor.update(cx, |editor, window, cx| {
5810 let snapshot = editor.buffer.read(cx).snapshot(cx);
5811 editor.insert_blocks(
5812 [BlockProperties {
5813 style: BlockStyle::Fixed,
5814 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5815 height: Some(1),
5816 render: Arc::new(|_| div().into_any()),
5817 priority: 0,
5818 }],
5819 Some(Autoscroll::fit()),
5820 cx,
5821 );
5822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5823 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5824 });
5825 editor.move_line_down(&MoveLineDown, window, cx);
5826 });
5827}
5828
5829#[gpui::test]
5830async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5831 init_test(cx, |_| {});
5832
5833 let mut cx = EditorTestContext::new(cx).await;
5834 cx.set_state(
5835 &"
5836 ˇzero
5837 one
5838 two
5839 three
5840 four
5841 five
5842 "
5843 .unindent(),
5844 );
5845
5846 // Create a four-line block that replaces three lines of text.
5847 cx.update_editor(|editor, window, cx| {
5848 let snapshot = editor.snapshot(window, cx);
5849 let snapshot = &snapshot.buffer_snapshot();
5850 let placement = BlockPlacement::Replace(
5851 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5852 );
5853 editor.insert_blocks(
5854 [BlockProperties {
5855 placement,
5856 height: Some(4),
5857 style: BlockStyle::Sticky,
5858 render: Arc::new(|_| gpui::div().into_any_element()),
5859 priority: 0,
5860 }],
5861 None,
5862 cx,
5863 );
5864 });
5865
5866 // Move down so that the cursor touches the block.
5867 cx.update_editor(|editor, window, cx| {
5868 editor.move_down(&Default::default(), window, cx);
5869 });
5870 cx.assert_editor_state(
5871 &"
5872 zero
5873 «one
5874 two
5875 threeˇ»
5876 four
5877 five
5878 "
5879 .unindent(),
5880 );
5881
5882 // Move down past the block.
5883 cx.update_editor(|editor, window, cx| {
5884 editor.move_down(&Default::default(), window, cx);
5885 });
5886 cx.assert_editor_state(
5887 &"
5888 zero
5889 one
5890 two
5891 three
5892 ˇfour
5893 five
5894 "
5895 .unindent(),
5896 );
5897}
5898
5899#[gpui::test]
5900fn test_transpose(cx: &mut TestAppContext) {
5901 init_test(cx, |_| {});
5902
5903 _ = cx.add_window(|window, cx| {
5904 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5905 editor.set_style(EditorStyle::default(), window, cx);
5906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5907 s.select_ranges([1..1])
5908 });
5909 editor.transpose(&Default::default(), window, cx);
5910 assert_eq!(editor.text(cx), "bac");
5911 assert_eq!(editor.selections.ranges(cx), [2..2]);
5912
5913 editor.transpose(&Default::default(), window, cx);
5914 assert_eq!(editor.text(cx), "bca");
5915 assert_eq!(editor.selections.ranges(cx), [3..3]);
5916
5917 editor.transpose(&Default::default(), window, cx);
5918 assert_eq!(editor.text(cx), "bac");
5919 assert_eq!(editor.selections.ranges(cx), [3..3]);
5920
5921 editor
5922 });
5923
5924 _ = cx.add_window(|window, cx| {
5925 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5926 editor.set_style(EditorStyle::default(), window, cx);
5927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5928 s.select_ranges([3..3])
5929 });
5930 editor.transpose(&Default::default(), window, cx);
5931 assert_eq!(editor.text(cx), "acb\nde");
5932 assert_eq!(editor.selections.ranges(cx), [3..3]);
5933
5934 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5935 s.select_ranges([4..4])
5936 });
5937 editor.transpose(&Default::default(), window, cx);
5938 assert_eq!(editor.text(cx), "acbd\ne");
5939 assert_eq!(editor.selections.ranges(cx), [5..5]);
5940
5941 editor.transpose(&Default::default(), window, cx);
5942 assert_eq!(editor.text(cx), "acbde\n");
5943 assert_eq!(editor.selections.ranges(cx), [6..6]);
5944
5945 editor.transpose(&Default::default(), window, cx);
5946 assert_eq!(editor.text(cx), "acbd\ne");
5947 assert_eq!(editor.selections.ranges(cx), [6..6]);
5948
5949 editor
5950 });
5951
5952 _ = cx.add_window(|window, cx| {
5953 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5954 editor.set_style(EditorStyle::default(), window, cx);
5955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5956 s.select_ranges([1..1, 2..2, 4..4])
5957 });
5958 editor.transpose(&Default::default(), window, cx);
5959 assert_eq!(editor.text(cx), "bacd\ne");
5960 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5961
5962 editor.transpose(&Default::default(), window, cx);
5963 assert_eq!(editor.text(cx), "bcade\n");
5964 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5965
5966 editor.transpose(&Default::default(), window, cx);
5967 assert_eq!(editor.text(cx), "bcda\ne");
5968 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5969
5970 editor.transpose(&Default::default(), window, cx);
5971 assert_eq!(editor.text(cx), "bcade\n");
5972 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5973
5974 editor.transpose(&Default::default(), window, cx);
5975 assert_eq!(editor.text(cx), "bcaed\n");
5976 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5977
5978 editor
5979 });
5980
5981 _ = cx.add_window(|window, cx| {
5982 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", 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([4..4])
5986 });
5987 editor.transpose(&Default::default(), window, cx);
5988 assert_eq!(editor.text(cx), "🏀🍐✋");
5989 assert_eq!(editor.selections.ranges(cx), [8..8]);
5990
5991 editor.transpose(&Default::default(), window, cx);
5992 assert_eq!(editor.text(cx), "🏀✋🍐");
5993 assert_eq!(editor.selections.ranges(cx), [11..11]);
5994
5995 editor.transpose(&Default::default(), window, cx);
5996 assert_eq!(editor.text(cx), "🏀🍐✋");
5997 assert_eq!(editor.selections.ranges(cx), [11..11]);
5998
5999 editor
6000 });
6001}
6002
6003#[gpui::test]
6004async fn test_rewrap(cx: &mut TestAppContext) {
6005 init_test(cx, |settings| {
6006 settings.languages.0.extend([
6007 (
6008 "Markdown".into(),
6009 LanguageSettingsContent {
6010 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6011 preferred_line_length: Some(40),
6012 ..Default::default()
6013 },
6014 ),
6015 (
6016 "Plain Text".into(),
6017 LanguageSettingsContent {
6018 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6019 preferred_line_length: Some(40),
6020 ..Default::default()
6021 },
6022 ),
6023 (
6024 "C++".into(),
6025 LanguageSettingsContent {
6026 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6027 preferred_line_length: Some(40),
6028 ..Default::default()
6029 },
6030 ),
6031 (
6032 "Python".into(),
6033 LanguageSettingsContent {
6034 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6035 preferred_line_length: Some(40),
6036 ..Default::default()
6037 },
6038 ),
6039 (
6040 "Rust".into(),
6041 LanguageSettingsContent {
6042 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6043 preferred_line_length: Some(40),
6044 ..Default::default()
6045 },
6046 ),
6047 ])
6048 });
6049
6050 let mut cx = EditorTestContext::new(cx).await;
6051
6052 let cpp_language = Arc::new(Language::new(
6053 LanguageConfig {
6054 name: "C++".into(),
6055 line_comments: vec!["// ".into()],
6056 ..LanguageConfig::default()
6057 },
6058 None,
6059 ));
6060 let python_language = Arc::new(Language::new(
6061 LanguageConfig {
6062 name: "Python".into(),
6063 line_comments: vec!["# ".into()],
6064 ..LanguageConfig::default()
6065 },
6066 None,
6067 ));
6068 let markdown_language = Arc::new(Language::new(
6069 LanguageConfig {
6070 name: "Markdown".into(),
6071 rewrap_prefixes: vec![
6072 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6073 regex::Regex::new("[-*+]\\s+").unwrap(),
6074 ],
6075 ..LanguageConfig::default()
6076 },
6077 None,
6078 ));
6079 let rust_language = Arc::new(
6080 Language::new(
6081 LanguageConfig {
6082 name: "Rust".into(),
6083 line_comments: vec!["// ".into(), "/// ".into()],
6084 ..LanguageConfig::default()
6085 },
6086 Some(tree_sitter_rust::LANGUAGE.into()),
6087 )
6088 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6089 .unwrap(),
6090 );
6091
6092 let plaintext_language = Arc::new(Language::new(
6093 LanguageConfig {
6094 name: "Plain Text".into(),
6095 ..LanguageConfig::default()
6096 },
6097 None,
6098 ));
6099
6100 // Test basic rewrapping of a long line with a cursor
6101 assert_rewrap(
6102 indoc! {"
6103 // ˇThis is a long comment that needs to be wrapped.
6104 "},
6105 indoc! {"
6106 // ˇThis is a long comment that needs to
6107 // be wrapped.
6108 "},
6109 cpp_language.clone(),
6110 &mut cx,
6111 );
6112
6113 // Test rewrapping a full selection
6114 assert_rewrap(
6115 indoc! {"
6116 «// This selected long comment needs to be wrapped.ˇ»"
6117 },
6118 indoc! {"
6119 «// This selected long comment needs to
6120 // be wrapped.ˇ»"
6121 },
6122 cpp_language.clone(),
6123 &mut cx,
6124 );
6125
6126 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6127 assert_rewrap(
6128 indoc! {"
6129 // ˇThis is the first line.
6130 // Thisˇ is the second line.
6131 // This is the thirdˇ line, all part of one paragraph.
6132 "},
6133 indoc! {"
6134 // ˇThis is the first line. Thisˇ is the
6135 // second line. This is the thirdˇ line,
6136 // all part of one paragraph.
6137 "},
6138 cpp_language.clone(),
6139 &mut cx,
6140 );
6141
6142 // Test multiple cursors in different paragraphs trigger separate rewraps
6143 assert_rewrap(
6144 indoc! {"
6145 // ˇThis is the first paragraph, first line.
6146 // ˇThis is the first paragraph, second line.
6147
6148 // ˇThis is the second paragraph, first line.
6149 // ˇThis is the second paragraph, second line.
6150 "},
6151 indoc! {"
6152 // ˇThis is the first paragraph, first
6153 // line. ˇThis is the first paragraph,
6154 // second line.
6155
6156 // ˇThis is the second paragraph, first
6157 // line. ˇThis is the second paragraph,
6158 // second line.
6159 "},
6160 cpp_language.clone(),
6161 &mut cx,
6162 );
6163
6164 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6165 assert_rewrap(
6166 indoc! {"
6167 «// A regular long long comment to be wrapped.
6168 /// A documentation long comment to be wrapped.ˇ»
6169 "},
6170 indoc! {"
6171 «// A regular long long comment to be
6172 // wrapped.
6173 /// A documentation long comment to be
6174 /// wrapped.ˇ»
6175 "},
6176 rust_language.clone(),
6177 &mut cx,
6178 );
6179
6180 // Test that change in indentation level trigger seperate rewraps
6181 assert_rewrap(
6182 indoc! {"
6183 fn foo() {
6184 «// This is a long comment at the base indent.
6185 // This is a long comment at the next indent.ˇ»
6186 }
6187 "},
6188 indoc! {"
6189 fn foo() {
6190 «// This is a long comment at the
6191 // base indent.
6192 // This is a long comment at the
6193 // next indent.ˇ»
6194 }
6195 "},
6196 rust_language.clone(),
6197 &mut cx,
6198 );
6199
6200 // Test that different comment prefix characters (e.g., '#') are handled correctly
6201 assert_rewrap(
6202 indoc! {"
6203 # ˇThis is a long comment using a pound sign.
6204 "},
6205 indoc! {"
6206 # ˇThis is a long comment using a pound
6207 # sign.
6208 "},
6209 python_language,
6210 &mut cx,
6211 );
6212
6213 // Test rewrapping only affects comments, not code even when selected
6214 assert_rewrap(
6215 indoc! {"
6216 «/// This doc comment is long and should be wrapped.
6217 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6218 "},
6219 indoc! {"
6220 «/// This doc comment is long and should
6221 /// be wrapped.
6222 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6223 "},
6224 rust_language.clone(),
6225 &mut cx,
6226 );
6227
6228 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6229 assert_rewrap(
6230 indoc! {"
6231 # Header
6232
6233 A long long long line of markdown text to wrap.ˇ
6234 "},
6235 indoc! {"
6236 # Header
6237
6238 A long long long line of markdown text
6239 to wrap.ˇ
6240 "},
6241 markdown_language.clone(),
6242 &mut cx,
6243 );
6244
6245 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6246 assert_rewrap(
6247 indoc! {"
6248 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6249 2. This is a numbered list item that is very long and needs to be wrapped properly.
6250 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6251 "},
6252 indoc! {"
6253 «1. This is a numbered list item that is
6254 very long and needs to be wrapped
6255 properly.
6256 2. This is a numbered list item that is
6257 very long and needs to be wrapped
6258 properly.
6259 - This is an unordered list item that is
6260 also very long and should not merge
6261 with the numbered item.ˇ»
6262 "},
6263 markdown_language.clone(),
6264 &mut cx,
6265 );
6266
6267 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6268 assert_rewrap(
6269 indoc! {"
6270 «1. This is a numbered list item that is
6271 very long and needs to be wrapped
6272 properly.
6273 2. This is a numbered list item that is
6274 very long and needs to be wrapped
6275 properly.
6276 - This is an unordered list item that is
6277 also very long and should not merge with
6278 the numbered item.ˇ»
6279 "},
6280 indoc! {"
6281 «1. This is a numbered list item that is
6282 very long and needs to be wrapped
6283 properly.
6284 2. This is a numbered list item that is
6285 very long and needs to be wrapped
6286 properly.
6287 - This is an unordered list item that is
6288 also very long and should not merge
6289 with the numbered item.ˇ»
6290 "},
6291 markdown_language.clone(),
6292 &mut cx,
6293 );
6294
6295 // Test that rewrapping maintain indents even when they already exists.
6296 assert_rewrap(
6297 indoc! {"
6298 «1. This is a numbered list
6299 item that is very long and needs to be wrapped properly.
6300 2. This is a numbered list
6301 item that is very long and needs to be wrapped properly.
6302 - This is an unordered list item that is also very long and
6303 should not merge with the numbered item.ˇ»
6304 "},
6305 indoc! {"
6306 «1. This is a numbered list item that is
6307 very long and needs to be wrapped
6308 properly.
6309 2. This is a numbered list item that is
6310 very long and needs to be wrapped
6311 properly.
6312 - This is an unordered list item that is
6313 also very long and should not merge
6314 with the numbered item.ˇ»
6315 "},
6316 markdown_language,
6317 &mut cx,
6318 );
6319
6320 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6321 assert_rewrap(
6322 indoc! {"
6323 ˇThis is a very long line of plain text that will be wrapped.
6324 "},
6325 indoc! {"
6326 ˇThis is a very long line of plain text
6327 that will be wrapped.
6328 "},
6329 plaintext_language.clone(),
6330 &mut cx,
6331 );
6332
6333 // Test that non-commented code acts as a paragraph boundary within a selection
6334 assert_rewrap(
6335 indoc! {"
6336 «// This is the first long comment block to be wrapped.
6337 fn my_func(a: u32);
6338 // This is the second long comment block to be wrapped.ˇ»
6339 "},
6340 indoc! {"
6341 «// This is the first long comment block
6342 // to be wrapped.
6343 fn my_func(a: u32);
6344 // This is the second long comment block
6345 // to be wrapped.ˇ»
6346 "},
6347 rust_language,
6348 &mut cx,
6349 );
6350
6351 // Test rewrapping multiple selections, including ones with blank lines or tabs
6352 assert_rewrap(
6353 indoc! {"
6354 «ˇThis is a very long line that will be wrapped.
6355
6356 This is another paragraph in the same selection.»
6357
6358 «\tThis is a very long indented line that will be wrapped.ˇ»
6359 "},
6360 indoc! {"
6361 «ˇThis is a very long line that will be
6362 wrapped.
6363
6364 This is another paragraph in the same
6365 selection.»
6366
6367 «\tThis is a very long indented line
6368 \tthat will be wrapped.ˇ»
6369 "},
6370 plaintext_language,
6371 &mut cx,
6372 );
6373
6374 // Test that an empty comment line acts as a paragraph boundary
6375 assert_rewrap(
6376 indoc! {"
6377 // ˇThis is a long comment that will be wrapped.
6378 //
6379 // And this is another long comment that will also be wrapped.ˇ
6380 "},
6381 indoc! {"
6382 // ˇThis is a long comment that will be
6383 // wrapped.
6384 //
6385 // And this is another long comment that
6386 // will also be wrapped.ˇ
6387 "},
6388 cpp_language,
6389 &mut cx,
6390 );
6391
6392 #[track_caller]
6393 fn assert_rewrap(
6394 unwrapped_text: &str,
6395 wrapped_text: &str,
6396 language: Arc<Language>,
6397 cx: &mut EditorTestContext,
6398 ) {
6399 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6400 cx.set_state(unwrapped_text);
6401 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6402 cx.assert_editor_state(wrapped_text);
6403 }
6404}
6405
6406#[gpui::test]
6407async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6408 init_test(cx, |settings| {
6409 settings.languages.0.extend([(
6410 "Rust".into(),
6411 LanguageSettingsContent {
6412 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6413 preferred_line_length: Some(40),
6414 ..Default::default()
6415 },
6416 )])
6417 });
6418
6419 let mut cx = EditorTestContext::new(cx).await;
6420
6421 let rust_lang = Arc::new(
6422 Language::new(
6423 LanguageConfig {
6424 name: "Rust".into(),
6425 line_comments: vec!["// ".into()],
6426 block_comment: Some(BlockCommentConfig {
6427 start: "/*".into(),
6428 end: "*/".into(),
6429 prefix: "* ".into(),
6430 tab_size: 1,
6431 }),
6432 documentation_comment: Some(BlockCommentConfig {
6433 start: "/**".into(),
6434 end: "*/".into(),
6435 prefix: "* ".into(),
6436 tab_size: 1,
6437 }),
6438
6439 ..LanguageConfig::default()
6440 },
6441 Some(tree_sitter_rust::LANGUAGE.into()),
6442 )
6443 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6444 .unwrap(),
6445 );
6446
6447 // regular block comment
6448 assert_rewrap(
6449 indoc! {"
6450 /*
6451 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6452 */
6453 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6454 "},
6455 indoc! {"
6456 /*
6457 *ˇ Lorem ipsum dolor sit amet,
6458 * consectetur adipiscing elit.
6459 */
6460 /*
6461 *ˇ Lorem ipsum dolor sit amet,
6462 * consectetur adipiscing elit.
6463 */
6464 "},
6465 rust_lang.clone(),
6466 &mut cx,
6467 );
6468
6469 // indent is respected
6470 assert_rewrap(
6471 indoc! {"
6472 {}
6473 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6474 "},
6475 indoc! {"
6476 {}
6477 /*
6478 *ˇ Lorem ipsum dolor sit amet,
6479 * consectetur adipiscing elit.
6480 */
6481 "},
6482 rust_lang.clone(),
6483 &mut cx,
6484 );
6485
6486 // short block comments with inline delimiters
6487 assert_rewrap(
6488 indoc! {"
6489 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6490 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6491 */
6492 /*
6493 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6494 "},
6495 indoc! {"
6496 /*
6497 *ˇ Lorem ipsum dolor sit amet,
6498 * consectetur adipiscing elit.
6499 */
6500 /*
6501 *ˇ Lorem ipsum dolor sit amet,
6502 * consectetur adipiscing elit.
6503 */
6504 /*
6505 *ˇ Lorem ipsum dolor sit amet,
6506 * consectetur adipiscing elit.
6507 */
6508 "},
6509 rust_lang.clone(),
6510 &mut cx,
6511 );
6512
6513 // multiline block comment with inline start/end delimiters
6514 assert_rewrap(
6515 indoc! {"
6516 /*ˇ Lorem ipsum dolor sit amet,
6517 * consectetur adipiscing elit. */
6518 "},
6519 indoc! {"
6520 /*
6521 *ˇ Lorem ipsum dolor sit amet,
6522 * consectetur adipiscing elit.
6523 */
6524 "},
6525 rust_lang.clone(),
6526 &mut cx,
6527 );
6528
6529 // block comment rewrap still respects paragraph bounds
6530 assert_rewrap(
6531 indoc! {"
6532 /*
6533 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6534 *
6535 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6536 */
6537 "},
6538 indoc! {"
6539 /*
6540 *ˇ Lorem ipsum dolor sit amet,
6541 * consectetur adipiscing elit.
6542 *
6543 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6544 */
6545 "},
6546 rust_lang.clone(),
6547 &mut cx,
6548 );
6549
6550 // documentation comments
6551 assert_rewrap(
6552 indoc! {"
6553 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6554 /**
6555 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 */
6557 "},
6558 indoc! {"
6559 /**
6560 *ˇ Lorem ipsum dolor sit amet,
6561 * consectetur adipiscing elit.
6562 */
6563 /**
6564 *ˇ Lorem ipsum dolor sit amet,
6565 * consectetur adipiscing elit.
6566 */
6567 "},
6568 rust_lang.clone(),
6569 &mut cx,
6570 );
6571
6572 // different, adjacent comments
6573 assert_rewrap(
6574 indoc! {"
6575 /**
6576 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6577 */
6578 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6579 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6580 "},
6581 indoc! {"
6582 /**
6583 *ˇ Lorem ipsum dolor sit amet,
6584 * consectetur adipiscing elit.
6585 */
6586 /*
6587 *ˇ Lorem ipsum dolor sit amet,
6588 * consectetur adipiscing elit.
6589 */
6590 //ˇ Lorem ipsum dolor sit amet,
6591 // consectetur adipiscing elit.
6592 "},
6593 rust_lang.clone(),
6594 &mut cx,
6595 );
6596
6597 // selection w/ single short block comment
6598 assert_rewrap(
6599 indoc! {"
6600 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6601 "},
6602 indoc! {"
6603 «/*
6604 * Lorem ipsum dolor sit amet,
6605 * consectetur adipiscing elit.
6606 */ˇ»
6607 "},
6608 rust_lang.clone(),
6609 &mut cx,
6610 );
6611
6612 // rewrapping a single comment w/ abutting comments
6613 assert_rewrap(
6614 indoc! {"
6615 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6616 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6617 "},
6618 indoc! {"
6619 /*
6620 * ˇLorem ipsum dolor sit amet,
6621 * consectetur adipiscing elit.
6622 */
6623 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6624 "},
6625 rust_lang.clone(),
6626 &mut cx,
6627 );
6628
6629 // selection w/ non-abutting short block comments
6630 assert_rewrap(
6631 indoc! {"
6632 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6633
6634 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6635 "},
6636 indoc! {"
6637 «/*
6638 * Lorem ipsum dolor sit amet,
6639 * consectetur adipiscing elit.
6640 */
6641
6642 /*
6643 * Lorem ipsum dolor sit amet,
6644 * consectetur adipiscing elit.
6645 */ˇ»
6646 "},
6647 rust_lang.clone(),
6648 &mut cx,
6649 );
6650
6651 // selection of multiline block comments
6652 assert_rewrap(
6653 indoc! {"
6654 «/* Lorem ipsum dolor sit amet,
6655 * consectetur adipiscing elit. */ˇ»
6656 "},
6657 indoc! {"
6658 «/*
6659 * Lorem ipsum dolor sit amet,
6660 * consectetur adipiscing elit.
6661 */ˇ»
6662 "},
6663 rust_lang.clone(),
6664 &mut cx,
6665 );
6666
6667 // partial selection of multiline block comments
6668 assert_rewrap(
6669 indoc! {"
6670 «/* Lorem ipsum dolor sit amet,ˇ»
6671 * consectetur adipiscing elit. */
6672 /* Lorem ipsum dolor sit amet,
6673 «* consectetur adipiscing elit. */ˇ»
6674 "},
6675 indoc! {"
6676 «/*
6677 * Lorem ipsum dolor sit amet,ˇ»
6678 * consectetur adipiscing elit. */
6679 /* Lorem ipsum dolor sit amet,
6680 «* consectetur adipiscing elit.
6681 */ˇ»
6682 "},
6683 rust_lang.clone(),
6684 &mut cx,
6685 );
6686
6687 // selection w/ abutting short block comments
6688 // TODO: should not be combined; should rewrap as 2 comments
6689 assert_rewrap(
6690 indoc! {"
6691 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6692 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6693 "},
6694 // desired behavior:
6695 // indoc! {"
6696 // «/*
6697 // * Lorem ipsum dolor sit amet,
6698 // * consectetur adipiscing elit.
6699 // */
6700 // /*
6701 // * Lorem ipsum dolor sit amet,
6702 // * consectetur adipiscing elit.
6703 // */ˇ»
6704 // "},
6705 // actual behaviour:
6706 indoc! {"
6707 «/*
6708 * Lorem ipsum dolor sit amet,
6709 * consectetur adipiscing elit. Lorem
6710 * ipsum dolor sit amet, consectetur
6711 * adipiscing elit.
6712 */ˇ»
6713 "},
6714 rust_lang.clone(),
6715 &mut cx,
6716 );
6717
6718 // TODO: same as above, but with delimiters on separate line
6719 // assert_rewrap(
6720 // indoc! {"
6721 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6722 // */
6723 // /*
6724 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6725 // "},
6726 // // desired:
6727 // // indoc! {"
6728 // // «/*
6729 // // * Lorem ipsum dolor sit amet,
6730 // // * consectetur adipiscing elit.
6731 // // */
6732 // // /*
6733 // // * Lorem ipsum dolor sit amet,
6734 // // * consectetur adipiscing elit.
6735 // // */ˇ»
6736 // // "},
6737 // // actual: (but with trailing w/s on the empty lines)
6738 // indoc! {"
6739 // «/*
6740 // * Lorem ipsum dolor sit amet,
6741 // * consectetur adipiscing elit.
6742 // *
6743 // */
6744 // /*
6745 // *
6746 // * Lorem ipsum dolor sit amet,
6747 // * consectetur adipiscing elit.
6748 // */ˇ»
6749 // "},
6750 // rust_lang.clone(),
6751 // &mut cx,
6752 // );
6753
6754 // TODO these are unhandled edge cases; not correct, just documenting known issues
6755 assert_rewrap(
6756 indoc! {"
6757 /*
6758 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6759 */
6760 /*
6761 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6762 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6763 "},
6764 // desired:
6765 // indoc! {"
6766 // /*
6767 // *ˇ Lorem ipsum dolor sit amet,
6768 // * consectetur adipiscing elit.
6769 // */
6770 // /*
6771 // *ˇ Lorem ipsum dolor sit amet,
6772 // * consectetur adipiscing elit.
6773 // */
6774 // /*
6775 // *ˇ Lorem ipsum dolor sit amet
6776 // */ /* consectetur adipiscing elit. */
6777 // "},
6778 // actual:
6779 indoc! {"
6780 /*
6781 //ˇ Lorem ipsum dolor sit amet,
6782 // consectetur adipiscing elit.
6783 */
6784 /*
6785 * //ˇ Lorem ipsum dolor sit amet,
6786 * consectetur adipiscing elit.
6787 */
6788 /*
6789 *ˇ Lorem ipsum dolor sit amet */ /*
6790 * consectetur adipiscing elit.
6791 */
6792 "},
6793 rust_lang,
6794 &mut cx,
6795 );
6796
6797 #[track_caller]
6798 fn assert_rewrap(
6799 unwrapped_text: &str,
6800 wrapped_text: &str,
6801 language: Arc<Language>,
6802 cx: &mut EditorTestContext,
6803 ) {
6804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6805 cx.set_state(unwrapped_text);
6806 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6807 cx.assert_editor_state(wrapped_text);
6808 }
6809}
6810
6811#[gpui::test]
6812async fn test_hard_wrap(cx: &mut TestAppContext) {
6813 init_test(cx, |_| {});
6814 let mut cx = EditorTestContext::new(cx).await;
6815
6816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6817 cx.update_editor(|editor, _, cx| {
6818 editor.set_hard_wrap(Some(14), cx);
6819 });
6820
6821 cx.set_state(indoc!(
6822 "
6823 one two three ˇ
6824 "
6825 ));
6826 cx.simulate_input("four");
6827 cx.run_until_parked();
6828
6829 cx.assert_editor_state(indoc!(
6830 "
6831 one two three
6832 fourˇ
6833 "
6834 ));
6835
6836 cx.update_editor(|editor, window, cx| {
6837 editor.newline(&Default::default(), window, cx);
6838 });
6839 cx.run_until_parked();
6840 cx.assert_editor_state(indoc!(
6841 "
6842 one two three
6843 four
6844 ˇ
6845 "
6846 ));
6847
6848 cx.simulate_input("five");
6849 cx.run_until_parked();
6850 cx.assert_editor_state(indoc!(
6851 "
6852 one two three
6853 four
6854 fiveˇ
6855 "
6856 ));
6857
6858 cx.update_editor(|editor, window, cx| {
6859 editor.newline(&Default::default(), window, cx);
6860 });
6861 cx.run_until_parked();
6862 cx.simulate_input("# ");
6863 cx.run_until_parked();
6864 cx.assert_editor_state(indoc!(
6865 "
6866 one two three
6867 four
6868 five
6869 # ˇ
6870 "
6871 ));
6872
6873 cx.update_editor(|editor, window, cx| {
6874 editor.newline(&Default::default(), window, cx);
6875 });
6876 cx.run_until_parked();
6877 cx.assert_editor_state(indoc!(
6878 "
6879 one two three
6880 four
6881 five
6882 #\x20
6883 #ˇ
6884 "
6885 ));
6886
6887 cx.simulate_input(" 6");
6888 cx.run_until_parked();
6889 cx.assert_editor_state(indoc!(
6890 "
6891 one two three
6892 four
6893 five
6894 #
6895 # 6ˇ
6896 "
6897 ));
6898}
6899
6900#[gpui::test]
6901async fn test_cut_line_ends(cx: &mut TestAppContext) {
6902 init_test(cx, |_| {});
6903
6904 let mut cx = EditorTestContext::new(cx).await;
6905
6906 cx.set_state(indoc! {"The quick brownˇ"});
6907 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6908 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6909
6910 cx.set_state(indoc! {"The emacs foxˇ"});
6911 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6912 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6913
6914 cx.set_state(indoc! {"
6915 The quick« brownˇ»
6916 fox jumps overˇ
6917 the lazy dog"});
6918 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6919 cx.assert_editor_state(indoc! {"
6920 The quickˇ
6921 ˇthe lazy dog"});
6922
6923 cx.set_state(indoc! {"
6924 The quick« brownˇ»
6925 fox jumps overˇ
6926 the lazy dog"});
6927 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6928 cx.assert_editor_state(indoc! {"
6929 The quickˇ
6930 fox jumps overˇthe lazy dog"});
6931
6932 cx.set_state(indoc! {"
6933 The quick« brownˇ»
6934 fox jumps overˇ
6935 the lazy dog"});
6936 cx.update_editor(|e, window, cx| {
6937 e.cut_to_end_of_line(
6938 &CutToEndOfLine {
6939 stop_at_newlines: true,
6940 },
6941 window,
6942 cx,
6943 )
6944 });
6945 cx.assert_editor_state(indoc! {"
6946 The quickˇ
6947 fox jumps overˇ
6948 the lazy dog"});
6949
6950 cx.set_state(indoc! {"
6951 The quick« brownˇ»
6952 fox jumps overˇ
6953 the lazy dog"});
6954 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6955 cx.assert_editor_state(indoc! {"
6956 The quickˇ
6957 fox jumps overˇthe lazy dog"});
6958}
6959
6960#[gpui::test]
6961async fn test_clipboard(cx: &mut TestAppContext) {
6962 init_test(cx, |_| {});
6963
6964 let mut cx = EditorTestContext::new(cx).await;
6965
6966 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6967 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6968 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6969
6970 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6971 cx.set_state("two ˇfour ˇsix ˇ");
6972 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6973 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6974
6975 // Paste again but with only two cursors. Since the number of cursors doesn't
6976 // match the number of slices in the clipboard, the entire clipboard text
6977 // is pasted at each cursor.
6978 cx.set_state("ˇtwo one✅ four three six five ˇ");
6979 cx.update_editor(|e, window, cx| {
6980 e.handle_input("( ", window, cx);
6981 e.paste(&Paste, window, cx);
6982 e.handle_input(") ", window, cx);
6983 });
6984 cx.assert_editor_state(
6985 &([
6986 "( one✅ ",
6987 "three ",
6988 "five ) ˇtwo one✅ four three six five ( one✅ ",
6989 "three ",
6990 "five ) ˇ",
6991 ]
6992 .join("\n")),
6993 );
6994
6995 // Cut with three selections, one of which is full-line.
6996 cx.set_state(indoc! {"
6997 1«2ˇ»3
6998 4ˇ567
6999 «8ˇ»9"});
7000 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7001 cx.assert_editor_state(indoc! {"
7002 1ˇ3
7003 ˇ9"});
7004
7005 // Paste with three selections, noticing how the copied selection that was full-line
7006 // gets inserted before the second cursor.
7007 cx.set_state(indoc! {"
7008 1ˇ3
7009 9ˇ
7010 «oˇ»ne"});
7011 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7012 cx.assert_editor_state(indoc! {"
7013 12ˇ3
7014 4567
7015 9ˇ
7016 8ˇne"});
7017
7018 // Copy with a single cursor only, which writes the whole line into the clipboard.
7019 cx.set_state(indoc! {"
7020 The quick brown
7021 fox juˇmps over
7022 the lazy dog"});
7023 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7024 assert_eq!(
7025 cx.read_from_clipboard()
7026 .and_then(|item| item.text().as_deref().map(str::to_string)),
7027 Some("fox jumps over\n".to_string())
7028 );
7029
7030 // Paste with three selections, noticing how the copied full-line selection is inserted
7031 // before the empty selections but replaces the selection that is non-empty.
7032 cx.set_state(indoc! {"
7033 Tˇhe quick brown
7034 «foˇ»x jumps over
7035 tˇhe lazy dog"});
7036 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7037 cx.assert_editor_state(indoc! {"
7038 fox jumps over
7039 Tˇhe quick brown
7040 fox jumps over
7041 ˇx jumps over
7042 fox jumps over
7043 tˇhe lazy dog"});
7044}
7045
7046#[gpui::test]
7047async fn test_copy_trim(cx: &mut TestAppContext) {
7048 init_test(cx, |_| {});
7049
7050 let mut cx = EditorTestContext::new(cx).await;
7051 cx.set_state(
7052 r#" «for selection in selections.iter() {
7053 let mut start = selection.start;
7054 let mut end = selection.end;
7055 let is_entire_line = selection.is_empty();
7056 if is_entire_line {
7057 start = Point::new(start.row, 0);ˇ»
7058 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7059 }
7060 "#,
7061 );
7062 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7063 assert_eq!(
7064 cx.read_from_clipboard()
7065 .and_then(|item| item.text().as_deref().map(str::to_string)),
7066 Some(
7067 "for selection in selections.iter() {
7068 let mut start = selection.start;
7069 let mut end = selection.end;
7070 let is_entire_line = selection.is_empty();
7071 if is_entire_line {
7072 start = Point::new(start.row, 0);"
7073 .to_string()
7074 ),
7075 "Regular copying preserves all indentation selected",
7076 );
7077 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7078 assert_eq!(
7079 cx.read_from_clipboard()
7080 .and_then(|item| item.text().as_deref().map(str::to_string)),
7081 Some(
7082 "for selection in selections.iter() {
7083let mut start = selection.start;
7084let mut end = selection.end;
7085let is_entire_line = selection.is_empty();
7086if is_entire_line {
7087 start = Point::new(start.row, 0);"
7088 .to_string()
7089 ),
7090 "Copying with stripping should strip all leading whitespaces"
7091 );
7092
7093 cx.set_state(
7094 r#" « for selection in selections.iter() {
7095 let mut start = selection.start;
7096 let mut end = selection.end;
7097 let is_entire_line = selection.is_empty();
7098 if is_entire_line {
7099 start = Point::new(start.row, 0);ˇ»
7100 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7101 }
7102 "#,
7103 );
7104 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7105 assert_eq!(
7106 cx.read_from_clipboard()
7107 .and_then(|item| item.text().as_deref().map(str::to_string)),
7108 Some(
7109 " for selection in selections.iter() {
7110 let mut start = selection.start;
7111 let mut end = selection.end;
7112 let is_entire_line = selection.is_empty();
7113 if is_entire_line {
7114 start = Point::new(start.row, 0);"
7115 .to_string()
7116 ),
7117 "Regular copying preserves all indentation selected",
7118 );
7119 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7120 assert_eq!(
7121 cx.read_from_clipboard()
7122 .and_then(|item| item.text().as_deref().map(str::to_string)),
7123 Some(
7124 "for selection in selections.iter() {
7125let mut start = selection.start;
7126let mut end = selection.end;
7127let is_entire_line = selection.is_empty();
7128if is_entire_line {
7129 start = Point::new(start.row, 0);"
7130 .to_string()
7131 ),
7132 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7133 );
7134
7135 cx.set_state(
7136 r#" «ˇ for selection in selections.iter() {
7137 let mut start = selection.start;
7138 let mut end = selection.end;
7139 let is_entire_line = selection.is_empty();
7140 if is_entire_line {
7141 start = Point::new(start.row, 0);»
7142 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7143 }
7144 "#,
7145 );
7146 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7147 assert_eq!(
7148 cx.read_from_clipboard()
7149 .and_then(|item| item.text().as_deref().map(str::to_string)),
7150 Some(
7151 " for selection in selections.iter() {
7152 let mut start = selection.start;
7153 let mut end = selection.end;
7154 let is_entire_line = selection.is_empty();
7155 if is_entire_line {
7156 start = Point::new(start.row, 0);"
7157 .to_string()
7158 ),
7159 "Regular copying for reverse selection works the same",
7160 );
7161 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7162 assert_eq!(
7163 cx.read_from_clipboard()
7164 .and_then(|item| item.text().as_deref().map(str::to_string)),
7165 Some(
7166 "for selection in selections.iter() {
7167let mut start = selection.start;
7168let mut end = selection.end;
7169let is_entire_line = selection.is_empty();
7170if is_entire_line {
7171 start = Point::new(start.row, 0);"
7172 .to_string()
7173 ),
7174 "Copying with stripping for reverse selection works the same"
7175 );
7176
7177 cx.set_state(
7178 r#" for selection «in selections.iter() {
7179 let mut start = selection.start;
7180 let mut end = selection.end;
7181 let is_entire_line = selection.is_empty();
7182 if is_entire_line {
7183 start = Point::new(start.row, 0);ˇ»
7184 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7185 }
7186 "#,
7187 );
7188 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7189 assert_eq!(
7190 cx.read_from_clipboard()
7191 .and_then(|item| item.text().as_deref().map(str::to_string)),
7192 Some(
7193 "in selections.iter() {
7194 let mut start = selection.start;
7195 let mut end = selection.end;
7196 let is_entire_line = selection.is_empty();
7197 if is_entire_line {
7198 start = Point::new(start.row, 0);"
7199 .to_string()
7200 ),
7201 "When selecting past the indent, the copying works as usual",
7202 );
7203 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7204 assert_eq!(
7205 cx.read_from_clipboard()
7206 .and_then(|item| item.text().as_deref().map(str::to_string)),
7207 Some(
7208 "in selections.iter() {
7209 let mut start = selection.start;
7210 let mut end = selection.end;
7211 let is_entire_line = selection.is_empty();
7212 if is_entire_line {
7213 start = Point::new(start.row, 0);"
7214 .to_string()
7215 ),
7216 "When selecting past the indent, nothing is trimmed"
7217 );
7218
7219 cx.set_state(
7220 r#" «for selection in selections.iter() {
7221 let mut start = selection.start;
7222
7223 let mut end = selection.end;
7224 let is_entire_line = selection.is_empty();
7225 if is_entire_line {
7226 start = Point::new(start.row, 0);
7227ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7228 }
7229 "#,
7230 );
7231 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7232 assert_eq!(
7233 cx.read_from_clipboard()
7234 .and_then(|item| item.text().as_deref().map(str::to_string)),
7235 Some(
7236 "for selection in selections.iter() {
7237let mut start = selection.start;
7238
7239let mut end = selection.end;
7240let is_entire_line = selection.is_empty();
7241if is_entire_line {
7242 start = Point::new(start.row, 0);
7243"
7244 .to_string()
7245 ),
7246 "Copying with stripping should ignore empty lines"
7247 );
7248}
7249
7250#[gpui::test]
7251async fn test_paste_multiline(cx: &mut TestAppContext) {
7252 init_test(cx, |_| {});
7253
7254 let mut cx = EditorTestContext::new(cx).await;
7255 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7256
7257 // Cut an indented block, without the leading whitespace.
7258 cx.set_state(indoc! {"
7259 const a: B = (
7260 c(),
7261 «d(
7262 e,
7263 f
7264 )ˇ»
7265 );
7266 "});
7267 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7268 cx.assert_editor_state(indoc! {"
7269 const a: B = (
7270 c(),
7271 ˇ
7272 );
7273 "});
7274
7275 // Paste it at the same position.
7276 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7277 cx.assert_editor_state(indoc! {"
7278 const a: B = (
7279 c(),
7280 d(
7281 e,
7282 f
7283 )ˇ
7284 );
7285 "});
7286
7287 // Paste it at a line with a lower indent level.
7288 cx.set_state(indoc! {"
7289 ˇ
7290 const a: B = (
7291 c(),
7292 );
7293 "});
7294 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7295 cx.assert_editor_state(indoc! {"
7296 d(
7297 e,
7298 f
7299 )ˇ
7300 const a: B = (
7301 c(),
7302 );
7303 "});
7304
7305 // Cut an indented block, with the leading whitespace.
7306 cx.set_state(indoc! {"
7307 const a: B = (
7308 c(),
7309 « d(
7310 e,
7311 f
7312 )
7313 ˇ»);
7314 "});
7315 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7316 cx.assert_editor_state(indoc! {"
7317 const a: B = (
7318 c(),
7319 ˇ);
7320 "});
7321
7322 // Paste it at the same position.
7323 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7324 cx.assert_editor_state(indoc! {"
7325 const a: B = (
7326 c(),
7327 d(
7328 e,
7329 f
7330 )
7331 ˇ);
7332 "});
7333
7334 // Paste it at a line with a higher indent level.
7335 cx.set_state(indoc! {"
7336 const a: B = (
7337 c(),
7338 d(
7339 e,
7340 fˇ
7341 )
7342 );
7343 "});
7344 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7345 cx.assert_editor_state(indoc! {"
7346 const a: B = (
7347 c(),
7348 d(
7349 e,
7350 f d(
7351 e,
7352 f
7353 )
7354 ˇ
7355 )
7356 );
7357 "});
7358
7359 // Copy an indented block, starting mid-line
7360 cx.set_state(indoc! {"
7361 const a: B = (
7362 c(),
7363 somethin«g(
7364 e,
7365 f
7366 )ˇ»
7367 );
7368 "});
7369 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7370
7371 // Paste it on a line with a lower indent level
7372 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7373 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7374 cx.assert_editor_state(indoc! {"
7375 const a: B = (
7376 c(),
7377 something(
7378 e,
7379 f
7380 )
7381 );
7382 g(
7383 e,
7384 f
7385 )ˇ"});
7386}
7387
7388#[gpui::test]
7389async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7390 init_test(cx, |_| {});
7391
7392 cx.write_to_clipboard(ClipboardItem::new_string(
7393 " d(\n e\n );\n".into(),
7394 ));
7395
7396 let mut cx = EditorTestContext::new(cx).await;
7397 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7398
7399 cx.set_state(indoc! {"
7400 fn a() {
7401 b();
7402 if c() {
7403 ˇ
7404 }
7405 }
7406 "});
7407
7408 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7409 cx.assert_editor_state(indoc! {"
7410 fn a() {
7411 b();
7412 if c() {
7413 d(
7414 e
7415 );
7416 ˇ
7417 }
7418 }
7419 "});
7420
7421 cx.set_state(indoc! {"
7422 fn a() {
7423 b();
7424 ˇ
7425 }
7426 "});
7427
7428 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7429 cx.assert_editor_state(indoc! {"
7430 fn a() {
7431 b();
7432 d(
7433 e
7434 );
7435 ˇ
7436 }
7437 "});
7438}
7439
7440#[gpui::test]
7441fn test_select_all(cx: &mut TestAppContext) {
7442 init_test(cx, |_| {});
7443
7444 let editor = cx.add_window(|window, cx| {
7445 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7446 build_editor(buffer, window, cx)
7447 });
7448 _ = editor.update(cx, |editor, window, cx| {
7449 editor.select_all(&SelectAll, window, cx);
7450 assert_eq!(
7451 editor.selections.display_ranges(cx),
7452 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7453 );
7454 });
7455}
7456
7457#[gpui::test]
7458fn test_select_line(cx: &mut TestAppContext) {
7459 init_test(cx, |_| {});
7460
7461 let editor = cx.add_window(|window, cx| {
7462 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7463 build_editor(buffer, window, cx)
7464 });
7465 _ = editor.update(cx, |editor, window, cx| {
7466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7467 s.select_display_ranges([
7468 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7469 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7470 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7471 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7472 ])
7473 });
7474 editor.select_line(&SelectLine, window, cx);
7475 assert_eq!(
7476 editor.selections.display_ranges(cx),
7477 vec![
7478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7479 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7480 ]
7481 );
7482 });
7483
7484 _ = editor.update(cx, |editor, window, cx| {
7485 editor.select_line(&SelectLine, window, cx);
7486 assert_eq!(
7487 editor.selections.display_ranges(cx),
7488 vec![
7489 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7490 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7491 ]
7492 );
7493 });
7494
7495 _ = editor.update(cx, |editor, window, cx| {
7496 editor.select_line(&SelectLine, window, cx);
7497 assert_eq!(
7498 editor.selections.display_ranges(cx),
7499 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7500 );
7501 });
7502}
7503
7504#[gpui::test]
7505async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7506 init_test(cx, |_| {});
7507 let mut cx = EditorTestContext::new(cx).await;
7508
7509 #[track_caller]
7510 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7511 cx.set_state(initial_state);
7512 cx.update_editor(|e, window, cx| {
7513 e.split_selection_into_lines(&Default::default(), window, cx)
7514 });
7515 cx.assert_editor_state(expected_state);
7516 }
7517
7518 // Selection starts and ends at the middle of lines, left-to-right
7519 test(
7520 &mut cx,
7521 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7522 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7523 );
7524 // Same thing, right-to-left
7525 test(
7526 &mut cx,
7527 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7528 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7529 );
7530
7531 // Whole buffer, left-to-right, last line *doesn't* end with newline
7532 test(
7533 &mut cx,
7534 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7535 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7536 );
7537 // Same thing, right-to-left
7538 test(
7539 &mut cx,
7540 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7541 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7542 );
7543
7544 // Whole buffer, left-to-right, last line ends with newline
7545 test(
7546 &mut cx,
7547 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7548 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7549 );
7550 // Same thing, right-to-left
7551 test(
7552 &mut cx,
7553 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7554 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7555 );
7556
7557 // Starts at the end of a line, ends at the start of another
7558 test(
7559 &mut cx,
7560 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7561 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7562 );
7563}
7564
7565#[gpui::test]
7566async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7567 init_test(cx, |_| {});
7568
7569 let editor = cx.add_window(|window, cx| {
7570 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7571 build_editor(buffer, window, cx)
7572 });
7573
7574 // setup
7575 _ = editor.update(cx, |editor, window, cx| {
7576 editor.fold_creases(
7577 vec![
7578 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7579 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7580 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7581 ],
7582 true,
7583 window,
7584 cx,
7585 );
7586 assert_eq!(
7587 editor.display_text(cx),
7588 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7589 );
7590 });
7591
7592 _ = editor.update(cx, |editor, window, cx| {
7593 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7594 s.select_display_ranges([
7595 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7596 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7597 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7598 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7599 ])
7600 });
7601 editor.split_selection_into_lines(&Default::default(), window, cx);
7602 assert_eq!(
7603 editor.display_text(cx),
7604 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7605 );
7606 });
7607 EditorTestContext::for_editor(editor, cx)
7608 .await
7609 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7610
7611 _ = editor.update(cx, |editor, window, cx| {
7612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7613 s.select_display_ranges([
7614 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7615 ])
7616 });
7617 editor.split_selection_into_lines(&Default::default(), window, cx);
7618 assert_eq!(
7619 editor.display_text(cx),
7620 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7621 );
7622 assert_eq!(
7623 editor.selections.display_ranges(cx),
7624 [
7625 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7626 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7627 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7628 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7629 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7630 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7631 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7632 ]
7633 );
7634 });
7635 EditorTestContext::for_editor(editor, cx)
7636 .await
7637 .assert_editor_state(
7638 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7639 );
7640}
7641
7642#[gpui::test]
7643async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7644 init_test(cx, |_| {});
7645
7646 let mut cx = EditorTestContext::new(cx).await;
7647
7648 cx.set_state(indoc!(
7649 r#"abc
7650 defˇghi
7651
7652 jk
7653 nlmo
7654 "#
7655 ));
7656
7657 cx.update_editor(|editor, window, cx| {
7658 editor.add_selection_above(&Default::default(), window, cx);
7659 });
7660
7661 cx.assert_editor_state(indoc!(
7662 r#"abcˇ
7663 defˇghi
7664
7665 jk
7666 nlmo
7667 "#
7668 ));
7669
7670 cx.update_editor(|editor, window, cx| {
7671 editor.add_selection_above(&Default::default(), window, cx);
7672 });
7673
7674 cx.assert_editor_state(indoc!(
7675 r#"abcˇ
7676 defˇghi
7677
7678 jk
7679 nlmo
7680 "#
7681 ));
7682
7683 cx.update_editor(|editor, window, cx| {
7684 editor.add_selection_below(&Default::default(), window, cx);
7685 });
7686
7687 cx.assert_editor_state(indoc!(
7688 r#"abc
7689 defˇghi
7690
7691 jk
7692 nlmo
7693 "#
7694 ));
7695
7696 cx.update_editor(|editor, window, cx| {
7697 editor.undo_selection(&Default::default(), window, cx);
7698 });
7699
7700 cx.assert_editor_state(indoc!(
7701 r#"abcˇ
7702 defˇghi
7703
7704 jk
7705 nlmo
7706 "#
7707 ));
7708
7709 cx.update_editor(|editor, window, cx| {
7710 editor.redo_selection(&Default::default(), window, cx);
7711 });
7712
7713 cx.assert_editor_state(indoc!(
7714 r#"abc
7715 defˇghi
7716
7717 jk
7718 nlmo
7719 "#
7720 ));
7721
7722 cx.update_editor(|editor, window, cx| {
7723 editor.add_selection_below(&Default::default(), window, cx);
7724 });
7725
7726 cx.assert_editor_state(indoc!(
7727 r#"abc
7728 defˇghi
7729 ˇ
7730 jk
7731 nlmo
7732 "#
7733 ));
7734
7735 cx.update_editor(|editor, window, cx| {
7736 editor.add_selection_below(&Default::default(), window, cx);
7737 });
7738
7739 cx.assert_editor_state(indoc!(
7740 r#"abc
7741 defˇghi
7742 ˇ
7743 jkˇ
7744 nlmo
7745 "#
7746 ));
7747
7748 cx.update_editor(|editor, window, cx| {
7749 editor.add_selection_below(&Default::default(), window, cx);
7750 });
7751
7752 cx.assert_editor_state(indoc!(
7753 r#"abc
7754 defˇghi
7755 ˇ
7756 jkˇ
7757 nlmˇo
7758 "#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_below(&Default::default(), window, cx);
7763 });
7764
7765 cx.assert_editor_state(indoc!(
7766 r#"abc
7767 defˇghi
7768 ˇ
7769 jkˇ
7770 nlmˇo
7771 ˇ"#
7772 ));
7773
7774 // change selections
7775 cx.set_state(indoc!(
7776 r#"abc
7777 def«ˇg»hi
7778
7779 jk
7780 nlmo
7781 "#
7782 ));
7783
7784 cx.update_editor(|editor, window, cx| {
7785 editor.add_selection_below(&Default::default(), window, cx);
7786 });
7787
7788 cx.assert_editor_state(indoc!(
7789 r#"abc
7790 def«ˇg»hi
7791
7792 jk
7793 nlm«ˇo»
7794 "#
7795 ));
7796
7797 cx.update_editor(|editor, window, cx| {
7798 editor.add_selection_below(&Default::default(), window, cx);
7799 });
7800
7801 cx.assert_editor_state(indoc!(
7802 r#"abc
7803 def«ˇg»hi
7804
7805 jk
7806 nlm«ˇo»
7807 "#
7808 ));
7809
7810 cx.update_editor(|editor, window, cx| {
7811 editor.add_selection_above(&Default::default(), window, cx);
7812 });
7813
7814 cx.assert_editor_state(indoc!(
7815 r#"abc
7816 def«ˇg»hi
7817
7818 jk
7819 nlmo
7820 "#
7821 ));
7822
7823 cx.update_editor(|editor, window, cx| {
7824 editor.add_selection_above(&Default::default(), window, cx);
7825 });
7826
7827 cx.assert_editor_state(indoc!(
7828 r#"abc
7829 def«ˇg»hi
7830
7831 jk
7832 nlmo
7833 "#
7834 ));
7835
7836 // Change selections again
7837 cx.set_state(indoc!(
7838 r#"a«bc
7839 defgˇ»hi
7840
7841 jk
7842 nlmo
7843 "#
7844 ));
7845
7846 cx.update_editor(|editor, window, cx| {
7847 editor.add_selection_below(&Default::default(), window, cx);
7848 });
7849
7850 cx.assert_editor_state(indoc!(
7851 r#"a«bcˇ»
7852 d«efgˇ»hi
7853
7854 j«kˇ»
7855 nlmo
7856 "#
7857 ));
7858
7859 cx.update_editor(|editor, window, cx| {
7860 editor.add_selection_below(&Default::default(), window, cx);
7861 });
7862 cx.assert_editor_state(indoc!(
7863 r#"a«bcˇ»
7864 d«efgˇ»hi
7865
7866 j«kˇ»
7867 n«lmoˇ»
7868 "#
7869 ));
7870 cx.update_editor(|editor, window, cx| {
7871 editor.add_selection_above(&Default::default(), window, cx);
7872 });
7873
7874 cx.assert_editor_state(indoc!(
7875 r#"a«bcˇ»
7876 d«efgˇ»hi
7877
7878 j«kˇ»
7879 nlmo
7880 "#
7881 ));
7882
7883 // Change selections again
7884 cx.set_state(indoc!(
7885 r#"abc
7886 d«ˇefghi
7887
7888 jk
7889 nlm»o
7890 "#
7891 ));
7892
7893 cx.update_editor(|editor, window, cx| {
7894 editor.add_selection_above(&Default::default(), window, cx);
7895 });
7896
7897 cx.assert_editor_state(indoc!(
7898 r#"a«ˇbc»
7899 d«ˇef»ghi
7900
7901 j«ˇk»
7902 n«ˇlm»o
7903 "#
7904 ));
7905
7906 cx.update_editor(|editor, window, cx| {
7907 editor.add_selection_below(&Default::default(), window, cx);
7908 });
7909
7910 cx.assert_editor_state(indoc!(
7911 r#"abc
7912 d«ˇef»ghi
7913
7914 j«ˇk»
7915 n«ˇlm»o
7916 "#
7917 ));
7918}
7919
7920#[gpui::test]
7921async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7922 init_test(cx, |_| {});
7923 let mut cx = EditorTestContext::new(cx).await;
7924
7925 cx.set_state(indoc!(
7926 r#"line onˇe
7927 liˇne two
7928 line three
7929 line four"#
7930 ));
7931
7932 cx.update_editor(|editor, window, cx| {
7933 editor.add_selection_below(&Default::default(), window, cx);
7934 });
7935
7936 // test multiple cursors expand in the same direction
7937 cx.assert_editor_state(indoc!(
7938 r#"line onˇe
7939 liˇne twˇo
7940 liˇne three
7941 line four"#
7942 ));
7943
7944 cx.update_editor(|editor, window, cx| {
7945 editor.add_selection_below(&Default::default(), window, cx);
7946 });
7947
7948 cx.update_editor(|editor, window, cx| {
7949 editor.add_selection_below(&Default::default(), window, cx);
7950 });
7951
7952 // test multiple cursors expand below overflow
7953 cx.assert_editor_state(indoc!(
7954 r#"line onˇe
7955 liˇne twˇo
7956 liˇne thˇree
7957 liˇne foˇur"#
7958 ));
7959
7960 cx.update_editor(|editor, window, cx| {
7961 editor.add_selection_above(&Default::default(), window, cx);
7962 });
7963
7964 // test multiple cursors retrieves back correctly
7965 cx.assert_editor_state(indoc!(
7966 r#"line onˇe
7967 liˇne twˇo
7968 liˇne thˇree
7969 line four"#
7970 ));
7971
7972 cx.update_editor(|editor, window, cx| {
7973 editor.add_selection_above(&Default::default(), window, cx);
7974 });
7975
7976 cx.update_editor(|editor, window, cx| {
7977 editor.add_selection_above(&Default::default(), window, cx);
7978 });
7979
7980 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7981 cx.assert_editor_state(indoc!(
7982 r#"liˇne onˇe
7983 liˇne two
7984 line three
7985 line four"#
7986 ));
7987
7988 cx.update_editor(|editor, window, cx| {
7989 editor.undo_selection(&Default::default(), window, cx);
7990 });
7991
7992 // test undo
7993 cx.assert_editor_state(indoc!(
7994 r#"line onˇe
7995 liˇne twˇo
7996 line three
7997 line four"#
7998 ));
7999
8000 cx.update_editor(|editor, window, cx| {
8001 editor.redo_selection(&Default::default(), window, cx);
8002 });
8003
8004 // test redo
8005 cx.assert_editor_state(indoc!(
8006 r#"liˇne onˇe
8007 liˇne two
8008 line three
8009 line four"#
8010 ));
8011
8012 cx.set_state(indoc!(
8013 r#"abcd
8014 ef«ghˇ»
8015 ijkl
8016 «mˇ»nop"#
8017 ));
8018
8019 cx.update_editor(|editor, window, cx| {
8020 editor.add_selection_above(&Default::default(), window, cx);
8021 });
8022
8023 // test multiple selections expand in the same direction
8024 cx.assert_editor_state(indoc!(
8025 r#"ab«cdˇ»
8026 ef«ghˇ»
8027 «iˇ»jkl
8028 «mˇ»nop"#
8029 ));
8030
8031 cx.update_editor(|editor, window, cx| {
8032 editor.add_selection_above(&Default::default(), window, cx);
8033 });
8034
8035 // test multiple selection upward overflow
8036 cx.assert_editor_state(indoc!(
8037 r#"ab«cdˇ»
8038 «eˇ»f«ghˇ»
8039 «iˇ»jkl
8040 «mˇ»nop"#
8041 ));
8042
8043 cx.update_editor(|editor, window, cx| {
8044 editor.add_selection_below(&Default::default(), window, cx);
8045 });
8046
8047 // test multiple selection retrieves back correctly
8048 cx.assert_editor_state(indoc!(
8049 r#"abcd
8050 ef«ghˇ»
8051 «iˇ»jkl
8052 «mˇ»nop"#
8053 ));
8054
8055 cx.update_editor(|editor, window, cx| {
8056 editor.add_selection_below(&Default::default(), window, cx);
8057 });
8058
8059 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8060 cx.assert_editor_state(indoc!(
8061 r#"abcd
8062 ef«ghˇ»
8063 ij«klˇ»
8064 «mˇ»nop"#
8065 ));
8066
8067 cx.update_editor(|editor, window, cx| {
8068 editor.undo_selection(&Default::default(), window, cx);
8069 });
8070
8071 // test undo
8072 cx.assert_editor_state(indoc!(
8073 r#"abcd
8074 ef«ghˇ»
8075 «iˇ»jkl
8076 «mˇ»nop"#
8077 ));
8078
8079 cx.update_editor(|editor, window, cx| {
8080 editor.redo_selection(&Default::default(), window, cx);
8081 });
8082
8083 // test redo
8084 cx.assert_editor_state(indoc!(
8085 r#"abcd
8086 ef«ghˇ»
8087 ij«klˇ»
8088 «mˇ»nop"#
8089 ));
8090}
8091
8092#[gpui::test]
8093async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8094 init_test(cx, |_| {});
8095 let mut cx = EditorTestContext::new(cx).await;
8096
8097 cx.set_state(indoc!(
8098 r#"line onˇe
8099 liˇne two
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.add_selection_below(&Default::default(), window, cx);
8106 editor.add_selection_below(&Default::default(), window, cx);
8107 editor.add_selection_below(&Default::default(), window, cx);
8108 });
8109
8110 // initial state with two multi cursor groups
8111 cx.assert_editor_state(indoc!(
8112 r#"line onˇe
8113 liˇne twˇo
8114 liˇne thˇree
8115 liˇne foˇur"#
8116 ));
8117
8118 // add single cursor in middle - simulate opt click
8119 cx.update_editor(|editor, window, cx| {
8120 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8121 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8122 editor.end_selection(window, cx);
8123 });
8124
8125 cx.assert_editor_state(indoc!(
8126 r#"line onˇe
8127 liˇne twˇo
8128 liˇneˇ thˇree
8129 liˇne foˇur"#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_above(&Default::default(), window, cx);
8134 });
8135
8136 // test new added selection expands above and existing selection shrinks
8137 cx.assert_editor_state(indoc!(
8138 r#"line onˇe
8139 liˇneˇ twˇo
8140 liˇneˇ thˇree
8141 line four"#
8142 ));
8143
8144 cx.update_editor(|editor, window, cx| {
8145 editor.add_selection_above(&Default::default(), window, cx);
8146 });
8147
8148 // test new added selection expands above and existing selection shrinks
8149 cx.assert_editor_state(indoc!(
8150 r#"lineˇ onˇe
8151 liˇneˇ twˇo
8152 lineˇ three
8153 line four"#
8154 ));
8155
8156 // intial state with two selection groups
8157 cx.set_state(indoc!(
8158 r#"abcd
8159 ef«ghˇ»
8160 ijkl
8161 «mˇ»nop"#
8162 ));
8163
8164 cx.update_editor(|editor, window, cx| {
8165 editor.add_selection_above(&Default::default(), window, cx);
8166 editor.add_selection_above(&Default::default(), window, cx);
8167 });
8168
8169 cx.assert_editor_state(indoc!(
8170 r#"ab«cdˇ»
8171 «eˇ»f«ghˇ»
8172 «iˇ»jkl
8173 «mˇ»nop"#
8174 ));
8175
8176 // add single selection in middle - simulate opt drag
8177 cx.update_editor(|editor, window, cx| {
8178 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8179 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8180 editor.update_selection(
8181 DisplayPoint::new(DisplayRow(2), 4),
8182 0,
8183 gpui::Point::<f32>::default(),
8184 window,
8185 cx,
8186 );
8187 editor.end_selection(window, cx);
8188 });
8189
8190 cx.assert_editor_state(indoc!(
8191 r#"ab«cdˇ»
8192 «eˇ»f«ghˇ»
8193 «iˇ»jk«lˇ»
8194 «mˇ»nop"#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 });
8200
8201 // test new added selection expands below, others shrinks from above
8202 cx.assert_editor_state(indoc!(
8203 r#"abcd
8204 ef«ghˇ»
8205 «iˇ»jk«lˇ»
8206 «mˇ»no«pˇ»"#
8207 ));
8208}
8209
8210#[gpui::test]
8211async fn test_select_next(cx: &mut TestAppContext) {
8212 init_test(cx, |_| {});
8213
8214 let mut cx = EditorTestContext::new(cx).await;
8215 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8216
8217 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8218 .unwrap();
8219 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8220
8221 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8222 .unwrap();
8223 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8224
8225 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8226 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8227
8228 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8229 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8230
8231 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8232 .unwrap();
8233 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8234
8235 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8236 .unwrap();
8237 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8238
8239 // Test selection direction should be preserved
8240 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8241
8242 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8243 .unwrap();
8244 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8245}
8246
8247#[gpui::test]
8248async fn test_select_all_matches(cx: &mut TestAppContext) {
8249 init_test(cx, |_| {});
8250
8251 let mut cx = EditorTestContext::new(cx).await;
8252
8253 // Test caret-only selections
8254 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8255 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8256 .unwrap();
8257 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8258
8259 // Test left-to-right selections
8260 cx.set_state("abc\n«abcˇ»\nabc");
8261 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8262 .unwrap();
8263 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8264
8265 // Test right-to-left selections
8266 cx.set_state("abc\n«ˇabc»\nabc");
8267 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8268 .unwrap();
8269 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8270
8271 // Test selecting whitespace with caret selection
8272 cx.set_state("abc\nˇ abc\nabc");
8273 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8274 .unwrap();
8275 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8276
8277 // Test selecting whitespace with left-to-right selection
8278 cx.set_state("abc\n«ˇ »abc\nabc");
8279 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8280 .unwrap();
8281 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8282
8283 // Test no matches with right-to-left selection
8284 cx.set_state("abc\n« ˇ»abc\nabc");
8285 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8286 .unwrap();
8287 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8288
8289 // Test with a single word and clip_at_line_ends=true (#29823)
8290 cx.set_state("aˇbc");
8291 cx.update_editor(|e, window, cx| {
8292 e.set_clip_at_line_ends(true, cx);
8293 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8294 e.set_clip_at_line_ends(false, cx);
8295 });
8296 cx.assert_editor_state("«abcˇ»");
8297}
8298
8299#[gpui::test]
8300async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8301 init_test(cx, |_| {});
8302
8303 let mut cx = EditorTestContext::new(cx).await;
8304
8305 let large_body_1 = "\nd".repeat(200);
8306 let large_body_2 = "\ne".repeat(200);
8307
8308 cx.set_state(&format!(
8309 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8310 ));
8311 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8312 let scroll_position = editor.scroll_position(cx);
8313 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8314 scroll_position
8315 });
8316
8317 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8318 .unwrap();
8319 cx.assert_editor_state(&format!(
8320 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8321 ));
8322 let scroll_position_after_selection =
8323 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8324 assert_eq!(
8325 initial_scroll_position, scroll_position_after_selection,
8326 "Scroll position should not change after selecting all matches"
8327 );
8328}
8329
8330#[gpui::test]
8331async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8332 init_test(cx, |_| {});
8333
8334 let mut cx = EditorLspTestContext::new_rust(
8335 lsp::ServerCapabilities {
8336 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8337 ..Default::default()
8338 },
8339 cx,
8340 )
8341 .await;
8342
8343 cx.set_state(indoc! {"
8344 line 1
8345 line 2
8346 linˇe 3
8347 line 4
8348 line 5
8349 "});
8350
8351 // Make an edit
8352 cx.update_editor(|editor, window, cx| {
8353 editor.handle_input("X", window, cx);
8354 });
8355
8356 // Move cursor to a different position
8357 cx.update_editor(|editor, window, cx| {
8358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8359 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8360 });
8361 });
8362
8363 cx.assert_editor_state(indoc! {"
8364 line 1
8365 line 2
8366 linXe 3
8367 line 4
8368 liˇne 5
8369 "});
8370
8371 cx.lsp
8372 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8373 Ok(Some(vec![lsp::TextEdit::new(
8374 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8375 "PREFIX ".to_string(),
8376 )]))
8377 });
8378
8379 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8380 .unwrap()
8381 .await
8382 .unwrap();
8383
8384 cx.assert_editor_state(indoc! {"
8385 PREFIX line 1
8386 line 2
8387 linXe 3
8388 line 4
8389 liˇne 5
8390 "});
8391
8392 // Undo formatting
8393 cx.update_editor(|editor, window, cx| {
8394 editor.undo(&Default::default(), window, cx);
8395 });
8396
8397 // Verify cursor moved back to position after edit
8398 cx.assert_editor_state(indoc! {"
8399 line 1
8400 line 2
8401 linXˇe 3
8402 line 4
8403 line 5
8404 "});
8405}
8406
8407#[gpui::test]
8408async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8409 init_test(cx, |_| {});
8410
8411 let mut cx = EditorTestContext::new(cx).await;
8412
8413 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8414 cx.update_editor(|editor, window, cx| {
8415 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8416 });
8417
8418 cx.set_state(indoc! {"
8419 line 1
8420 line 2
8421 linˇe 3
8422 line 4
8423 line 5
8424 line 6
8425 line 7
8426 line 8
8427 line 9
8428 line 10
8429 "});
8430
8431 let snapshot = cx.buffer_snapshot();
8432 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8433
8434 cx.update(|_, cx| {
8435 provider.update(cx, |provider, _| {
8436 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8437 id: None,
8438 edits: vec![(edit_position..edit_position, "X".into())],
8439 edit_preview: None,
8440 }))
8441 })
8442 });
8443
8444 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8445 cx.update_editor(|editor, window, cx| {
8446 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8447 });
8448
8449 cx.assert_editor_state(indoc! {"
8450 line 1
8451 line 2
8452 lineXˇ 3
8453 line 4
8454 line 5
8455 line 6
8456 line 7
8457 line 8
8458 line 9
8459 line 10
8460 "});
8461
8462 cx.update_editor(|editor, window, cx| {
8463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8464 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8465 });
8466 });
8467
8468 cx.assert_editor_state(indoc! {"
8469 line 1
8470 line 2
8471 lineX 3
8472 line 4
8473 line 5
8474 line 6
8475 line 7
8476 line 8
8477 line 9
8478 liˇne 10
8479 "});
8480
8481 cx.update_editor(|editor, window, cx| {
8482 editor.undo(&Default::default(), window, cx);
8483 });
8484
8485 cx.assert_editor_state(indoc! {"
8486 line 1
8487 line 2
8488 lineˇ 3
8489 line 4
8490 line 5
8491 line 6
8492 line 7
8493 line 8
8494 line 9
8495 line 10
8496 "});
8497}
8498
8499#[gpui::test]
8500async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8501 init_test(cx, |_| {});
8502
8503 let mut cx = EditorTestContext::new(cx).await;
8504 cx.set_state(
8505 r#"let foo = 2;
8506lˇet foo = 2;
8507let fooˇ = 2;
8508let foo = 2;
8509let foo = ˇ2;"#,
8510 );
8511
8512 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8513 .unwrap();
8514 cx.assert_editor_state(
8515 r#"let foo = 2;
8516«letˇ» foo = 2;
8517let «fooˇ» = 2;
8518let foo = 2;
8519let foo = «2ˇ»;"#,
8520 );
8521
8522 // noop for multiple selections with different contents
8523 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8524 .unwrap();
8525 cx.assert_editor_state(
8526 r#"let foo = 2;
8527«letˇ» foo = 2;
8528let «fooˇ» = 2;
8529let foo = 2;
8530let foo = «2ˇ»;"#,
8531 );
8532
8533 // Test last selection direction should be preserved
8534 cx.set_state(
8535 r#"let foo = 2;
8536let foo = 2;
8537let «fooˇ» = 2;
8538let «ˇfoo» = 2;
8539let foo = 2;"#,
8540 );
8541
8542 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8543 .unwrap();
8544 cx.assert_editor_state(
8545 r#"let foo = 2;
8546let foo = 2;
8547let «fooˇ» = 2;
8548let «ˇfoo» = 2;
8549let «ˇfoo» = 2;"#,
8550 );
8551}
8552
8553#[gpui::test]
8554async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8555 init_test(cx, |_| {});
8556
8557 let mut cx =
8558 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8559
8560 cx.assert_editor_state(indoc! {"
8561 ˇbbb
8562 ccc
8563
8564 bbb
8565 ccc
8566 "});
8567 cx.dispatch_action(SelectPrevious::default());
8568 cx.assert_editor_state(indoc! {"
8569 «bbbˇ»
8570 ccc
8571
8572 bbb
8573 ccc
8574 "});
8575 cx.dispatch_action(SelectPrevious::default());
8576 cx.assert_editor_state(indoc! {"
8577 «bbbˇ»
8578 ccc
8579
8580 «bbbˇ»
8581 ccc
8582 "});
8583}
8584
8585#[gpui::test]
8586async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8587 init_test(cx, |_| {});
8588
8589 let mut cx = EditorTestContext::new(cx).await;
8590 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8591
8592 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8593 .unwrap();
8594 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8595
8596 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8597 .unwrap();
8598 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8599
8600 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8601 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8602
8603 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8604 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8605
8606 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8607 .unwrap();
8608 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8609
8610 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8611 .unwrap();
8612 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8613}
8614
8615#[gpui::test]
8616async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8617 init_test(cx, |_| {});
8618
8619 let mut cx = EditorTestContext::new(cx).await;
8620 cx.set_state("aˇ");
8621
8622 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8623 .unwrap();
8624 cx.assert_editor_state("«aˇ»");
8625 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8626 .unwrap();
8627 cx.assert_editor_state("«aˇ»");
8628}
8629
8630#[gpui::test]
8631async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8632 init_test(cx, |_| {});
8633
8634 let mut cx = EditorTestContext::new(cx).await;
8635 cx.set_state(
8636 r#"let foo = 2;
8637lˇet foo = 2;
8638let fooˇ = 2;
8639let foo = 2;
8640let foo = ˇ2;"#,
8641 );
8642
8643 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8644 .unwrap();
8645 cx.assert_editor_state(
8646 r#"let foo = 2;
8647«letˇ» foo = 2;
8648let «fooˇ» = 2;
8649let foo = 2;
8650let foo = «2ˇ»;"#,
8651 );
8652
8653 // noop for multiple selections with different contents
8654 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8655 .unwrap();
8656 cx.assert_editor_state(
8657 r#"let foo = 2;
8658«letˇ» foo = 2;
8659let «fooˇ» = 2;
8660let foo = 2;
8661let foo = «2ˇ»;"#,
8662 );
8663}
8664
8665#[gpui::test]
8666async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8667 init_test(cx, |_| {});
8668
8669 let mut cx = EditorTestContext::new(cx).await;
8670 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8671
8672 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8673 .unwrap();
8674 // selection direction is preserved
8675 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8676
8677 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8678 .unwrap();
8679 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8680
8681 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8682 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8683
8684 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8685 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8686
8687 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8688 .unwrap();
8689 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8690
8691 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8692 .unwrap();
8693 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8694}
8695
8696#[gpui::test]
8697async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8698 init_test(cx, |_| {});
8699
8700 let language = Arc::new(Language::new(
8701 LanguageConfig::default(),
8702 Some(tree_sitter_rust::LANGUAGE.into()),
8703 ));
8704
8705 let text = r#"
8706 use mod1::mod2::{mod3, mod4};
8707
8708 fn fn_1(param1: bool, param2: &str) {
8709 let var1 = "text";
8710 }
8711 "#
8712 .unindent();
8713
8714 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8715 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8716 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8717
8718 editor
8719 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8720 .await;
8721
8722 editor.update_in(cx, |editor, window, cx| {
8723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8724 s.select_display_ranges([
8725 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8726 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8727 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8728 ]);
8729 });
8730 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8731 });
8732 editor.update(cx, |editor, cx| {
8733 assert_text_with_selections(
8734 editor,
8735 indoc! {r#"
8736 use mod1::mod2::{mod3, «mod4ˇ»};
8737
8738 fn fn_1«ˇ(param1: bool, param2: &str)» {
8739 let var1 = "«ˇtext»";
8740 }
8741 "#},
8742 cx,
8743 );
8744 });
8745
8746 editor.update_in(cx, |editor, window, cx| {
8747 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8748 });
8749 editor.update(cx, |editor, cx| {
8750 assert_text_with_selections(
8751 editor,
8752 indoc! {r#"
8753 use mod1::mod2::«{mod3, mod4}ˇ»;
8754
8755 «ˇfn fn_1(param1: bool, param2: &str) {
8756 let var1 = "text";
8757 }»
8758 "#},
8759 cx,
8760 );
8761 });
8762
8763 editor.update_in(cx, |editor, window, cx| {
8764 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8765 });
8766 assert_eq!(
8767 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8768 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8769 );
8770
8771 // Trying to expand the selected syntax node one more time has no effect.
8772 editor.update_in(cx, |editor, window, cx| {
8773 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8774 });
8775 assert_eq!(
8776 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8777 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8778 );
8779
8780 editor.update_in(cx, |editor, window, cx| {
8781 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8782 });
8783 editor.update(cx, |editor, cx| {
8784 assert_text_with_selections(
8785 editor,
8786 indoc! {r#"
8787 use mod1::mod2::«{mod3, mod4}ˇ»;
8788
8789 «ˇfn fn_1(param1: bool, param2: &str) {
8790 let var1 = "text";
8791 }»
8792 "#},
8793 cx,
8794 );
8795 });
8796
8797 editor.update_in(cx, |editor, window, cx| {
8798 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8799 });
8800 editor.update(cx, |editor, cx| {
8801 assert_text_with_selections(
8802 editor,
8803 indoc! {r#"
8804 use mod1::mod2::{mod3, «mod4ˇ»};
8805
8806 fn fn_1«ˇ(param1: bool, param2: &str)» {
8807 let var1 = "«ˇtext»";
8808 }
8809 "#},
8810 cx,
8811 );
8812 });
8813
8814 editor.update_in(cx, |editor, window, cx| {
8815 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8816 });
8817 editor.update(cx, |editor, cx| {
8818 assert_text_with_selections(
8819 editor,
8820 indoc! {r#"
8821 use mod1::mod2::{mod3, moˇd4};
8822
8823 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8824 let var1 = "teˇxt";
8825 }
8826 "#},
8827 cx,
8828 );
8829 });
8830
8831 // Trying to shrink the selected syntax node one more time has no effect.
8832 editor.update_in(cx, |editor, window, cx| {
8833 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8834 });
8835 editor.update_in(cx, |editor, _, cx| {
8836 assert_text_with_selections(
8837 editor,
8838 indoc! {r#"
8839 use mod1::mod2::{mod3, moˇd4};
8840
8841 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8842 let var1 = "teˇxt";
8843 }
8844 "#},
8845 cx,
8846 );
8847 });
8848
8849 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8850 // a fold.
8851 editor.update_in(cx, |editor, window, cx| {
8852 editor.fold_creases(
8853 vec![
8854 Crease::simple(
8855 Point::new(0, 21)..Point::new(0, 24),
8856 FoldPlaceholder::test(),
8857 ),
8858 Crease::simple(
8859 Point::new(3, 20)..Point::new(3, 22),
8860 FoldPlaceholder::test(),
8861 ),
8862 ],
8863 true,
8864 window,
8865 cx,
8866 );
8867 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8868 });
8869 editor.update(cx, |editor, cx| {
8870 assert_text_with_selections(
8871 editor,
8872 indoc! {r#"
8873 use mod1::mod2::«{mod3, mod4}ˇ»;
8874
8875 fn fn_1«ˇ(param1: bool, param2: &str)» {
8876 let var1 = "«ˇtext»";
8877 }
8878 "#},
8879 cx,
8880 );
8881 });
8882}
8883
8884#[gpui::test]
8885async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8886 init_test(cx, |_| {});
8887
8888 let language = Arc::new(Language::new(
8889 LanguageConfig::default(),
8890 Some(tree_sitter_rust::LANGUAGE.into()),
8891 ));
8892
8893 let text = "let a = 2;";
8894
8895 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8896 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8897 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8898
8899 editor
8900 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8901 .await;
8902
8903 // Test case 1: Cursor at end of word
8904 editor.update_in(cx, |editor, window, cx| {
8905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8906 s.select_display_ranges([
8907 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8908 ]);
8909 });
8910 });
8911 editor.update(cx, |editor, cx| {
8912 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8913 });
8914 editor.update_in(cx, |editor, window, cx| {
8915 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8916 });
8917 editor.update(cx, |editor, cx| {
8918 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8919 });
8920 editor.update_in(cx, |editor, window, cx| {
8921 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8922 });
8923 editor.update(cx, |editor, cx| {
8924 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8925 });
8926
8927 // Test case 2: Cursor at end of statement
8928 editor.update_in(cx, |editor, window, cx| {
8929 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8930 s.select_display_ranges([
8931 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8932 ]);
8933 });
8934 });
8935 editor.update(cx, |editor, cx| {
8936 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8937 });
8938 editor.update_in(cx, |editor, window, cx| {
8939 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8940 });
8941 editor.update(cx, |editor, cx| {
8942 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8943 });
8944}
8945
8946#[gpui::test]
8947async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8948 init_test(cx, |_| {});
8949
8950 let language = Arc::new(Language::new(
8951 LanguageConfig {
8952 name: "JavaScript".into(),
8953 ..Default::default()
8954 },
8955 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8956 ));
8957
8958 let text = r#"
8959 let a = {
8960 key: "value",
8961 };
8962 "#
8963 .unindent();
8964
8965 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8968
8969 editor
8970 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8971 .await;
8972
8973 // Test case 1: Cursor after '{'
8974 editor.update_in(cx, |editor, window, cx| {
8975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8976 s.select_display_ranges([
8977 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8978 ]);
8979 });
8980 });
8981 editor.update(cx, |editor, cx| {
8982 assert_text_with_selections(
8983 editor,
8984 indoc! {r#"
8985 let a = {ˇ
8986 key: "value",
8987 };
8988 "#},
8989 cx,
8990 );
8991 });
8992 editor.update_in(cx, |editor, window, cx| {
8993 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8994 });
8995 editor.update(cx, |editor, cx| {
8996 assert_text_with_selections(
8997 editor,
8998 indoc! {r#"
8999 let a = «ˇ{
9000 key: "value",
9001 }»;
9002 "#},
9003 cx,
9004 );
9005 });
9006
9007 // Test case 2: Cursor after ':'
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9010 s.select_display_ranges([
9011 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9012 ]);
9013 });
9014 });
9015 editor.update(cx, |editor, cx| {
9016 assert_text_with_selections(
9017 editor,
9018 indoc! {r#"
9019 let a = {
9020 key:ˇ "value",
9021 };
9022 "#},
9023 cx,
9024 );
9025 });
9026 editor.update_in(cx, |editor, window, cx| {
9027 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9028 });
9029 editor.update(cx, |editor, cx| {
9030 assert_text_with_selections(
9031 editor,
9032 indoc! {r#"
9033 let a = {
9034 «ˇkey: "value"»,
9035 };
9036 "#},
9037 cx,
9038 );
9039 });
9040 editor.update_in(cx, |editor, window, cx| {
9041 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9042 });
9043 editor.update(cx, |editor, cx| {
9044 assert_text_with_selections(
9045 editor,
9046 indoc! {r#"
9047 let a = «ˇ{
9048 key: "value",
9049 }»;
9050 "#},
9051 cx,
9052 );
9053 });
9054
9055 // Test case 3: Cursor after ','
9056 editor.update_in(cx, |editor, window, cx| {
9057 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9058 s.select_display_ranges([
9059 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9060 ]);
9061 });
9062 });
9063 editor.update(cx, |editor, cx| {
9064 assert_text_with_selections(
9065 editor,
9066 indoc! {r#"
9067 let a = {
9068 key: "value",ˇ
9069 };
9070 "#},
9071 cx,
9072 );
9073 });
9074 editor.update_in(cx, |editor, window, cx| {
9075 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9076 });
9077 editor.update(cx, |editor, cx| {
9078 assert_text_with_selections(
9079 editor,
9080 indoc! {r#"
9081 let a = «ˇ{
9082 key: "value",
9083 }»;
9084 "#},
9085 cx,
9086 );
9087 });
9088
9089 // Test case 4: Cursor after ';'
9090 editor.update_in(cx, |editor, window, cx| {
9091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9092 s.select_display_ranges([
9093 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9094 ]);
9095 });
9096 });
9097 editor.update(cx, |editor, cx| {
9098 assert_text_with_selections(
9099 editor,
9100 indoc! {r#"
9101 let a = {
9102 key: "value",
9103 };ˇ
9104 "#},
9105 cx,
9106 );
9107 });
9108 editor.update_in(cx, |editor, window, cx| {
9109 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9110 });
9111 editor.update(cx, |editor, cx| {
9112 assert_text_with_selections(
9113 editor,
9114 indoc! {r#"
9115 «ˇlet a = {
9116 key: "value",
9117 };
9118 »"#},
9119 cx,
9120 );
9121 });
9122}
9123
9124#[gpui::test]
9125async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9126 init_test(cx, |_| {});
9127
9128 let language = Arc::new(Language::new(
9129 LanguageConfig::default(),
9130 Some(tree_sitter_rust::LANGUAGE.into()),
9131 ));
9132
9133 let text = r#"
9134 use mod1::mod2::{mod3, mod4};
9135
9136 fn fn_1(param1: bool, param2: &str) {
9137 let var1 = "hello world";
9138 }
9139 "#
9140 .unindent();
9141
9142 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9143 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9144 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9145
9146 editor
9147 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9148 .await;
9149
9150 // Test 1: Cursor on a letter of a string word
9151 editor.update_in(cx, |editor, window, cx| {
9152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9153 s.select_display_ranges([
9154 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9155 ]);
9156 });
9157 });
9158 editor.update_in(cx, |editor, window, cx| {
9159 assert_text_with_selections(
9160 editor,
9161 indoc! {r#"
9162 use mod1::mod2::{mod3, mod4};
9163
9164 fn fn_1(param1: bool, param2: &str) {
9165 let var1 = "hˇello world";
9166 }
9167 "#},
9168 cx,
9169 );
9170 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 use mod1::mod2::{mod3, mod4};
9175
9176 fn fn_1(param1: bool, param2: &str) {
9177 let var1 = "«ˇhello» world";
9178 }
9179 "#},
9180 cx,
9181 );
9182 });
9183
9184 // Test 2: Partial selection within a word
9185 editor.update_in(cx, |editor, window, cx| {
9186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9187 s.select_display_ranges([
9188 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9189 ]);
9190 });
9191 });
9192 editor.update_in(cx, |editor, window, cx| {
9193 assert_text_with_selections(
9194 editor,
9195 indoc! {r#"
9196 use mod1::mod2::{mod3, mod4};
9197
9198 fn fn_1(param1: bool, param2: &str) {
9199 let var1 = "h«elˇ»lo world";
9200 }
9201 "#},
9202 cx,
9203 );
9204 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 use mod1::mod2::{mod3, mod4};
9209
9210 fn fn_1(param1: bool, param2: &str) {
9211 let var1 = "«ˇhello» world";
9212 }
9213 "#},
9214 cx,
9215 );
9216 });
9217
9218 // Test 3: Complete word already selected
9219 editor.update_in(cx, |editor, window, cx| {
9220 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9221 s.select_display_ranges([
9222 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9223 ]);
9224 });
9225 });
9226 editor.update_in(cx, |editor, window, cx| {
9227 assert_text_with_selections(
9228 editor,
9229 indoc! {r#"
9230 use mod1::mod2::{mod3, mod4};
9231
9232 fn fn_1(param1: bool, param2: &str) {
9233 let var1 = "«helloˇ» world";
9234 }
9235 "#},
9236 cx,
9237 );
9238 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9239 assert_text_with_selections(
9240 editor,
9241 indoc! {r#"
9242 use mod1::mod2::{mod3, mod4};
9243
9244 fn fn_1(param1: bool, param2: &str) {
9245 let var1 = "«hello worldˇ»";
9246 }
9247 "#},
9248 cx,
9249 );
9250 });
9251
9252 // Test 4: Selection spanning across words
9253 editor.update_in(cx, |editor, window, cx| {
9254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9255 s.select_display_ranges([
9256 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9257 ]);
9258 });
9259 });
9260 editor.update_in(cx, |editor, window, cx| {
9261 assert_text_with_selections(
9262 editor,
9263 indoc! {r#"
9264 use mod1::mod2::{mod3, mod4};
9265
9266 fn fn_1(param1: bool, param2: &str) {
9267 let var1 = "hel«lo woˇ»rld";
9268 }
9269 "#},
9270 cx,
9271 );
9272 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9273 assert_text_with_selections(
9274 editor,
9275 indoc! {r#"
9276 use mod1::mod2::{mod3, mod4};
9277
9278 fn fn_1(param1: bool, param2: &str) {
9279 let var1 = "«ˇhello world»";
9280 }
9281 "#},
9282 cx,
9283 );
9284 });
9285
9286 // Test 5: Expansion beyond string
9287 editor.update_in(cx, |editor, window, cx| {
9288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9290 assert_text_with_selections(
9291 editor,
9292 indoc! {r#"
9293 use mod1::mod2::{mod3, mod4};
9294
9295 fn fn_1(param1: bool, param2: &str) {
9296 «ˇlet var1 = "hello world";»
9297 }
9298 "#},
9299 cx,
9300 );
9301 });
9302}
9303
9304#[gpui::test]
9305async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9306 init_test(cx, |_| {});
9307
9308 let mut cx = EditorTestContext::new(cx).await;
9309
9310 let language = Arc::new(Language::new(
9311 LanguageConfig::default(),
9312 Some(tree_sitter_rust::LANGUAGE.into()),
9313 ));
9314
9315 cx.update_buffer(|buffer, cx| {
9316 buffer.set_language(Some(language), cx);
9317 });
9318
9319 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9320 cx.update_editor(|editor, window, cx| {
9321 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9322 });
9323
9324 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9325
9326 cx.set_state(indoc! { r#"fn a() {
9327 // what
9328 // a
9329 // ˇlong
9330 // method
9331 // I
9332 // sure
9333 // hope
9334 // it
9335 // works
9336 }"# });
9337
9338 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9339 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9340 cx.update(|_, cx| {
9341 multi_buffer.update(cx, |multi_buffer, cx| {
9342 multi_buffer.set_excerpts_for_path(
9343 PathKey::for_buffer(&buffer, cx),
9344 buffer,
9345 [Point::new(1, 0)..Point::new(1, 0)],
9346 3,
9347 cx,
9348 );
9349 });
9350 });
9351
9352 let editor2 = cx.new_window_entity(|window, cx| {
9353 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9354 });
9355
9356 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9357 cx.update_editor(|editor, window, cx| {
9358 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9359 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9360 })
9361 });
9362
9363 cx.assert_editor_state(indoc! { "
9364 fn a() {
9365 // what
9366 // a
9367 ˇ // long
9368 // method"});
9369
9370 cx.update_editor(|editor, window, cx| {
9371 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9372 });
9373
9374 // Although we could potentially make the action work when the syntax node
9375 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9376 // did. Maybe we could also expand the excerpt to contain the range?
9377 cx.assert_editor_state(indoc! { "
9378 fn a() {
9379 // what
9380 // a
9381 ˇ // long
9382 // method"});
9383}
9384
9385#[gpui::test]
9386async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9387 init_test(cx, |_| {});
9388
9389 let base_text = r#"
9390 impl A {
9391 // this is an uncommitted comment
9392
9393 fn b() {
9394 c();
9395 }
9396
9397 // this is another uncommitted comment
9398
9399 fn d() {
9400 // e
9401 // f
9402 }
9403 }
9404
9405 fn g() {
9406 // h
9407 }
9408 "#
9409 .unindent();
9410
9411 let text = r#"
9412 ˇimpl A {
9413
9414 fn b() {
9415 c();
9416 }
9417
9418 fn d() {
9419 // e
9420 // f
9421 }
9422 }
9423
9424 fn g() {
9425 // h
9426 }
9427 "#
9428 .unindent();
9429
9430 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9431 cx.set_state(&text);
9432 cx.set_head_text(&base_text);
9433 cx.update_editor(|editor, window, cx| {
9434 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9435 });
9436
9437 cx.assert_state_with_diff(
9438 "
9439 ˇimpl A {
9440 - // this is an uncommitted comment
9441
9442 fn b() {
9443 c();
9444 }
9445
9446 - // this is another uncommitted comment
9447 -
9448 fn d() {
9449 // e
9450 // f
9451 }
9452 }
9453
9454 fn g() {
9455 // h
9456 }
9457 "
9458 .unindent(),
9459 );
9460
9461 let expected_display_text = "
9462 impl A {
9463 // this is an uncommitted comment
9464
9465 fn b() {
9466 ⋯
9467 }
9468
9469 // this is another uncommitted comment
9470
9471 fn d() {
9472 ⋯
9473 }
9474 }
9475
9476 fn g() {
9477 ⋯
9478 }
9479 "
9480 .unindent();
9481
9482 cx.update_editor(|editor, window, cx| {
9483 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9484 assert_eq!(editor.display_text(cx), expected_display_text);
9485 });
9486}
9487
9488#[gpui::test]
9489async fn test_autoindent(cx: &mut TestAppContext) {
9490 init_test(cx, |_| {});
9491
9492 let language = Arc::new(
9493 Language::new(
9494 LanguageConfig {
9495 brackets: BracketPairConfig {
9496 pairs: vec![
9497 BracketPair {
9498 start: "{".to_string(),
9499 end: "}".to_string(),
9500 close: false,
9501 surround: false,
9502 newline: true,
9503 },
9504 BracketPair {
9505 start: "(".to_string(),
9506 end: ")".to_string(),
9507 close: false,
9508 surround: false,
9509 newline: true,
9510 },
9511 ],
9512 ..Default::default()
9513 },
9514 ..Default::default()
9515 },
9516 Some(tree_sitter_rust::LANGUAGE.into()),
9517 )
9518 .with_indents_query(
9519 r#"
9520 (_ "(" ")" @end) @indent
9521 (_ "{" "}" @end) @indent
9522 "#,
9523 )
9524 .unwrap(),
9525 );
9526
9527 let text = "fn a() {}";
9528
9529 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9530 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9531 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9532 editor
9533 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9534 .await;
9535
9536 editor.update_in(cx, |editor, window, cx| {
9537 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9538 s.select_ranges([5..5, 8..8, 9..9])
9539 });
9540 editor.newline(&Newline, window, cx);
9541 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9542 assert_eq!(
9543 editor.selections.ranges(cx),
9544 &[
9545 Point::new(1, 4)..Point::new(1, 4),
9546 Point::new(3, 4)..Point::new(3, 4),
9547 Point::new(5, 0)..Point::new(5, 0)
9548 ]
9549 );
9550 });
9551}
9552
9553#[gpui::test]
9554async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9555 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9556
9557 let language = Arc::new(
9558 Language::new(
9559 LanguageConfig {
9560 brackets: BracketPairConfig {
9561 pairs: vec![
9562 BracketPair {
9563 start: "{".to_string(),
9564 end: "}".to_string(),
9565 close: false,
9566 surround: false,
9567 newline: true,
9568 },
9569 BracketPair {
9570 start: "(".to_string(),
9571 end: ")".to_string(),
9572 close: false,
9573 surround: false,
9574 newline: true,
9575 },
9576 ],
9577 ..Default::default()
9578 },
9579 ..Default::default()
9580 },
9581 Some(tree_sitter_rust::LANGUAGE.into()),
9582 )
9583 .with_indents_query(
9584 r#"
9585 (_ "(" ")" @end) @indent
9586 (_ "{" "}" @end) @indent
9587 "#,
9588 )
9589 .unwrap(),
9590 );
9591
9592 let text = "fn a() {}";
9593
9594 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9595 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9596 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9597 editor
9598 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9599 .await;
9600
9601 editor.update_in(cx, |editor, window, cx| {
9602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9603 s.select_ranges([5..5, 8..8, 9..9])
9604 });
9605 editor.newline(&Newline, window, cx);
9606 assert_eq!(
9607 editor.text(cx),
9608 indoc!(
9609 "
9610 fn a(
9611
9612 ) {
9613
9614 }
9615 "
9616 )
9617 );
9618 assert_eq!(
9619 editor.selections.ranges(cx),
9620 &[
9621 Point::new(1, 0)..Point::new(1, 0),
9622 Point::new(3, 0)..Point::new(3, 0),
9623 Point::new(5, 0)..Point::new(5, 0)
9624 ]
9625 );
9626 });
9627}
9628
9629#[gpui::test]
9630async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9631 init_test(cx, |settings| {
9632 settings.defaults.auto_indent = Some(true);
9633 settings.languages.0.insert(
9634 "python".into(),
9635 LanguageSettingsContent {
9636 auto_indent: Some(false),
9637 ..Default::default()
9638 },
9639 );
9640 });
9641
9642 let mut cx = EditorTestContext::new(cx).await;
9643
9644 let injected_language = Arc::new(
9645 Language::new(
9646 LanguageConfig {
9647 brackets: BracketPairConfig {
9648 pairs: vec![
9649 BracketPair {
9650 start: "{".to_string(),
9651 end: "}".to_string(),
9652 close: false,
9653 surround: false,
9654 newline: true,
9655 },
9656 BracketPair {
9657 start: "(".to_string(),
9658 end: ")".to_string(),
9659 close: true,
9660 surround: false,
9661 newline: true,
9662 },
9663 ],
9664 ..Default::default()
9665 },
9666 name: "python".into(),
9667 ..Default::default()
9668 },
9669 Some(tree_sitter_python::LANGUAGE.into()),
9670 )
9671 .with_indents_query(
9672 r#"
9673 (_ "(" ")" @end) @indent
9674 (_ "{" "}" @end) @indent
9675 "#,
9676 )
9677 .unwrap(),
9678 );
9679
9680 let language = Arc::new(
9681 Language::new(
9682 LanguageConfig {
9683 brackets: BracketPairConfig {
9684 pairs: vec![
9685 BracketPair {
9686 start: "{".to_string(),
9687 end: "}".to_string(),
9688 close: false,
9689 surround: false,
9690 newline: true,
9691 },
9692 BracketPair {
9693 start: "(".to_string(),
9694 end: ")".to_string(),
9695 close: true,
9696 surround: false,
9697 newline: true,
9698 },
9699 ],
9700 ..Default::default()
9701 },
9702 name: LanguageName::new("rust"),
9703 ..Default::default()
9704 },
9705 Some(tree_sitter_rust::LANGUAGE.into()),
9706 )
9707 .with_indents_query(
9708 r#"
9709 (_ "(" ")" @end) @indent
9710 (_ "{" "}" @end) @indent
9711 "#,
9712 )
9713 .unwrap()
9714 .with_injection_query(
9715 r#"
9716 (macro_invocation
9717 macro: (identifier) @_macro_name
9718 (token_tree) @injection.content
9719 (#set! injection.language "python"))
9720 "#,
9721 )
9722 .unwrap(),
9723 );
9724
9725 cx.language_registry().add(injected_language);
9726 cx.language_registry().add(language.clone());
9727
9728 cx.update_buffer(|buffer, cx| {
9729 buffer.set_language(Some(language), cx);
9730 });
9731
9732 cx.set_state(r#"struct A {ˇ}"#);
9733
9734 cx.update_editor(|editor, window, cx| {
9735 editor.newline(&Default::default(), window, cx);
9736 });
9737
9738 cx.assert_editor_state(indoc!(
9739 "struct A {
9740 ˇ
9741 }"
9742 ));
9743
9744 cx.set_state(r#"select_biased!(ˇ)"#);
9745
9746 cx.update_editor(|editor, window, cx| {
9747 editor.newline(&Default::default(), window, cx);
9748 editor.handle_input("def ", window, cx);
9749 editor.handle_input("(", window, cx);
9750 editor.newline(&Default::default(), window, cx);
9751 editor.handle_input("a", window, cx);
9752 });
9753
9754 cx.assert_editor_state(indoc!(
9755 "select_biased!(
9756 def (
9757 aˇ
9758 )
9759 )"
9760 ));
9761}
9762
9763#[gpui::test]
9764async fn test_autoindent_selections(cx: &mut TestAppContext) {
9765 init_test(cx, |_| {});
9766
9767 {
9768 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9769 cx.set_state(indoc! {"
9770 impl A {
9771
9772 fn b() {}
9773
9774 «fn c() {
9775
9776 }ˇ»
9777 }
9778 "});
9779
9780 cx.update_editor(|editor, window, cx| {
9781 editor.autoindent(&Default::default(), window, cx);
9782 });
9783
9784 cx.assert_editor_state(indoc! {"
9785 impl A {
9786
9787 fn b() {}
9788
9789 «fn c() {
9790
9791 }ˇ»
9792 }
9793 "});
9794 }
9795
9796 {
9797 let mut cx = EditorTestContext::new_multibuffer(
9798 cx,
9799 [indoc! { "
9800 impl A {
9801 «
9802 // a
9803 fn b(){}
9804 »
9805 «
9806 }
9807 fn c(){}
9808 »
9809 "}],
9810 );
9811
9812 let buffer = cx.update_editor(|editor, _, cx| {
9813 let buffer = editor.buffer().update(cx, |buffer, _| {
9814 buffer.all_buffers().iter().next().unwrap().clone()
9815 });
9816 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9817 buffer
9818 });
9819
9820 cx.run_until_parked();
9821 cx.update_editor(|editor, window, cx| {
9822 editor.select_all(&Default::default(), window, cx);
9823 editor.autoindent(&Default::default(), window, cx)
9824 });
9825 cx.run_until_parked();
9826
9827 cx.update(|_, cx| {
9828 assert_eq!(
9829 buffer.read(cx).text(),
9830 indoc! { "
9831 impl A {
9832
9833 // a
9834 fn b(){}
9835
9836
9837 }
9838 fn c(){}
9839
9840 " }
9841 )
9842 });
9843 }
9844}
9845
9846#[gpui::test]
9847async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9848 init_test(cx, |_| {});
9849
9850 let mut cx = EditorTestContext::new(cx).await;
9851
9852 let language = Arc::new(Language::new(
9853 LanguageConfig {
9854 brackets: BracketPairConfig {
9855 pairs: vec![
9856 BracketPair {
9857 start: "{".to_string(),
9858 end: "}".to_string(),
9859 close: true,
9860 surround: true,
9861 newline: true,
9862 },
9863 BracketPair {
9864 start: "(".to_string(),
9865 end: ")".to_string(),
9866 close: true,
9867 surround: true,
9868 newline: true,
9869 },
9870 BracketPair {
9871 start: "/*".to_string(),
9872 end: " */".to_string(),
9873 close: true,
9874 surround: true,
9875 newline: true,
9876 },
9877 BracketPair {
9878 start: "[".to_string(),
9879 end: "]".to_string(),
9880 close: false,
9881 surround: false,
9882 newline: true,
9883 },
9884 BracketPair {
9885 start: "\"".to_string(),
9886 end: "\"".to_string(),
9887 close: true,
9888 surround: true,
9889 newline: false,
9890 },
9891 BracketPair {
9892 start: "<".to_string(),
9893 end: ">".to_string(),
9894 close: false,
9895 surround: true,
9896 newline: true,
9897 },
9898 ],
9899 ..Default::default()
9900 },
9901 autoclose_before: "})]".to_string(),
9902 ..Default::default()
9903 },
9904 Some(tree_sitter_rust::LANGUAGE.into()),
9905 ));
9906
9907 cx.language_registry().add(language.clone());
9908 cx.update_buffer(|buffer, cx| {
9909 buffer.set_language(Some(language), cx);
9910 });
9911
9912 cx.set_state(
9913 &r#"
9914 🏀ˇ
9915 εˇ
9916 ❤️ˇ
9917 "#
9918 .unindent(),
9919 );
9920
9921 // autoclose multiple nested brackets at multiple cursors
9922 cx.update_editor(|editor, window, cx| {
9923 editor.handle_input("{", window, cx);
9924 editor.handle_input("{", window, cx);
9925 editor.handle_input("{", window, cx);
9926 });
9927 cx.assert_editor_state(
9928 &"
9929 🏀{{{ˇ}}}
9930 ε{{{ˇ}}}
9931 ❤️{{{ˇ}}}
9932 "
9933 .unindent(),
9934 );
9935
9936 // insert a different closing bracket
9937 cx.update_editor(|editor, window, cx| {
9938 editor.handle_input(")", window, cx);
9939 });
9940 cx.assert_editor_state(
9941 &"
9942 🏀{{{)ˇ}}}
9943 ε{{{)ˇ}}}
9944 ❤️{{{)ˇ}}}
9945 "
9946 .unindent(),
9947 );
9948
9949 // skip over the auto-closed brackets when typing a closing bracket
9950 cx.update_editor(|editor, window, cx| {
9951 editor.move_right(&MoveRight, window, cx);
9952 editor.handle_input("}", window, cx);
9953 editor.handle_input("}", window, cx);
9954 editor.handle_input("}", window, cx);
9955 });
9956 cx.assert_editor_state(
9957 &"
9958 🏀{{{)}}}}ˇ
9959 ε{{{)}}}}ˇ
9960 ❤️{{{)}}}}ˇ
9961 "
9962 .unindent(),
9963 );
9964
9965 // autoclose multi-character pairs
9966 cx.set_state(
9967 &"
9968 ˇ
9969 ˇ
9970 "
9971 .unindent(),
9972 );
9973 cx.update_editor(|editor, window, cx| {
9974 editor.handle_input("/", window, cx);
9975 editor.handle_input("*", window, cx);
9976 });
9977 cx.assert_editor_state(
9978 &"
9979 /*ˇ */
9980 /*ˇ */
9981 "
9982 .unindent(),
9983 );
9984
9985 // one cursor autocloses a multi-character pair, one cursor
9986 // does not autoclose.
9987 cx.set_state(
9988 &"
9989 /ˇ
9990 ˇ
9991 "
9992 .unindent(),
9993 );
9994 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9995 cx.assert_editor_state(
9996 &"
9997 /*ˇ */
9998 *ˇ
9999 "
10000 .unindent(),
10001 );
10002
10003 // Don't autoclose if the next character isn't whitespace and isn't
10004 // listed in the language's "autoclose_before" section.
10005 cx.set_state("ˇa b");
10006 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10007 cx.assert_editor_state("{ˇa b");
10008
10009 // Don't autoclose if `close` is false for the bracket pair
10010 cx.set_state("ˇ");
10011 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10012 cx.assert_editor_state("[ˇ");
10013
10014 // Surround with brackets if text is selected
10015 cx.set_state("«aˇ» b");
10016 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10017 cx.assert_editor_state("{«aˇ»} b");
10018
10019 // Autoclose when not immediately after a word character
10020 cx.set_state("a ˇ");
10021 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10022 cx.assert_editor_state("a \"ˇ\"");
10023
10024 // Autoclose pair where the start and end characters are the same
10025 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10026 cx.assert_editor_state("a \"\"ˇ");
10027
10028 // Don't autoclose when immediately after a word character
10029 cx.set_state("aˇ");
10030 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10031 cx.assert_editor_state("a\"ˇ");
10032
10033 // Do autoclose when after a non-word character
10034 cx.set_state("{ˇ");
10035 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10036 cx.assert_editor_state("{\"ˇ\"");
10037
10038 // Non identical pairs autoclose regardless of preceding character
10039 cx.set_state("aˇ");
10040 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10041 cx.assert_editor_state("a{ˇ}");
10042
10043 // Don't autoclose pair if autoclose is disabled
10044 cx.set_state("ˇ");
10045 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10046 cx.assert_editor_state("<ˇ");
10047
10048 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10049 cx.set_state("«aˇ» b");
10050 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10051 cx.assert_editor_state("<«aˇ»> b");
10052}
10053
10054#[gpui::test]
10055async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10056 init_test(cx, |settings| {
10057 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10058 });
10059
10060 let mut cx = EditorTestContext::new(cx).await;
10061
10062 let language = Arc::new(Language::new(
10063 LanguageConfig {
10064 brackets: BracketPairConfig {
10065 pairs: vec![
10066 BracketPair {
10067 start: "{".to_string(),
10068 end: "}".to_string(),
10069 close: true,
10070 surround: true,
10071 newline: true,
10072 },
10073 BracketPair {
10074 start: "(".to_string(),
10075 end: ")".to_string(),
10076 close: true,
10077 surround: true,
10078 newline: true,
10079 },
10080 BracketPair {
10081 start: "[".to_string(),
10082 end: "]".to_string(),
10083 close: false,
10084 surround: false,
10085 newline: true,
10086 },
10087 ],
10088 ..Default::default()
10089 },
10090 autoclose_before: "})]".to_string(),
10091 ..Default::default()
10092 },
10093 Some(tree_sitter_rust::LANGUAGE.into()),
10094 ));
10095
10096 cx.language_registry().add(language.clone());
10097 cx.update_buffer(|buffer, cx| {
10098 buffer.set_language(Some(language), cx);
10099 });
10100
10101 cx.set_state(
10102 &"
10103 ˇ
10104 ˇ
10105 ˇ
10106 "
10107 .unindent(),
10108 );
10109
10110 // ensure only matching closing brackets are skipped over
10111 cx.update_editor(|editor, window, cx| {
10112 editor.handle_input("}", window, cx);
10113 editor.move_left(&MoveLeft, window, cx);
10114 editor.handle_input(")", window, cx);
10115 editor.move_left(&MoveLeft, window, cx);
10116 });
10117 cx.assert_editor_state(
10118 &"
10119 ˇ)}
10120 ˇ)}
10121 ˇ)}
10122 "
10123 .unindent(),
10124 );
10125
10126 // skip-over closing brackets at multiple cursors
10127 cx.update_editor(|editor, window, cx| {
10128 editor.handle_input(")", window, cx);
10129 editor.handle_input("}", window, cx);
10130 });
10131 cx.assert_editor_state(
10132 &"
10133 )}ˇ
10134 )}ˇ
10135 )}ˇ
10136 "
10137 .unindent(),
10138 );
10139
10140 // ignore non-close brackets
10141 cx.update_editor(|editor, window, cx| {
10142 editor.handle_input("]", window, cx);
10143 editor.move_left(&MoveLeft, window, cx);
10144 editor.handle_input("]", window, cx);
10145 });
10146 cx.assert_editor_state(
10147 &"
10148 )}]ˇ]
10149 )}]ˇ]
10150 )}]ˇ]
10151 "
10152 .unindent(),
10153 );
10154}
10155
10156#[gpui::test]
10157async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10158 init_test(cx, |_| {});
10159
10160 let mut cx = EditorTestContext::new(cx).await;
10161
10162 let html_language = Arc::new(
10163 Language::new(
10164 LanguageConfig {
10165 name: "HTML".into(),
10166 brackets: BracketPairConfig {
10167 pairs: vec![
10168 BracketPair {
10169 start: "<".into(),
10170 end: ">".into(),
10171 close: true,
10172 ..Default::default()
10173 },
10174 BracketPair {
10175 start: "{".into(),
10176 end: "}".into(),
10177 close: true,
10178 ..Default::default()
10179 },
10180 BracketPair {
10181 start: "(".into(),
10182 end: ")".into(),
10183 close: true,
10184 ..Default::default()
10185 },
10186 ],
10187 ..Default::default()
10188 },
10189 autoclose_before: "})]>".into(),
10190 ..Default::default()
10191 },
10192 Some(tree_sitter_html::LANGUAGE.into()),
10193 )
10194 .with_injection_query(
10195 r#"
10196 (script_element
10197 (raw_text) @injection.content
10198 (#set! injection.language "javascript"))
10199 "#,
10200 )
10201 .unwrap(),
10202 );
10203
10204 let javascript_language = Arc::new(Language::new(
10205 LanguageConfig {
10206 name: "JavaScript".into(),
10207 brackets: BracketPairConfig {
10208 pairs: vec![
10209 BracketPair {
10210 start: "/*".into(),
10211 end: " */".into(),
10212 close: true,
10213 ..Default::default()
10214 },
10215 BracketPair {
10216 start: "{".into(),
10217 end: "}".into(),
10218 close: true,
10219 ..Default::default()
10220 },
10221 BracketPair {
10222 start: "(".into(),
10223 end: ")".into(),
10224 close: true,
10225 ..Default::default()
10226 },
10227 ],
10228 ..Default::default()
10229 },
10230 autoclose_before: "})]>".into(),
10231 ..Default::default()
10232 },
10233 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10234 ));
10235
10236 cx.language_registry().add(html_language.clone());
10237 cx.language_registry().add(javascript_language);
10238 cx.executor().run_until_parked();
10239
10240 cx.update_buffer(|buffer, cx| {
10241 buffer.set_language(Some(html_language), cx);
10242 });
10243
10244 cx.set_state(
10245 &r#"
10246 <body>ˇ
10247 <script>
10248 var x = 1;ˇ
10249 </script>
10250 </body>ˇ
10251 "#
10252 .unindent(),
10253 );
10254
10255 // Precondition: different languages are active at different locations.
10256 cx.update_editor(|editor, window, cx| {
10257 let snapshot = editor.snapshot(window, cx);
10258 let cursors = editor.selections.ranges::<usize>(cx);
10259 let languages = cursors
10260 .iter()
10261 .map(|c| snapshot.language_at(c.start).unwrap().name())
10262 .collect::<Vec<_>>();
10263 assert_eq!(
10264 languages,
10265 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10266 );
10267 });
10268
10269 // Angle brackets autoclose in HTML, but not JavaScript.
10270 cx.update_editor(|editor, window, cx| {
10271 editor.handle_input("<", window, cx);
10272 editor.handle_input("a", window, cx);
10273 });
10274 cx.assert_editor_state(
10275 &r#"
10276 <body><aˇ>
10277 <script>
10278 var x = 1;<aˇ
10279 </script>
10280 </body><aˇ>
10281 "#
10282 .unindent(),
10283 );
10284
10285 // Curly braces and parens autoclose in both HTML and JavaScript.
10286 cx.update_editor(|editor, window, cx| {
10287 editor.handle_input(" b=", window, cx);
10288 editor.handle_input("{", window, cx);
10289 editor.handle_input("c", window, cx);
10290 editor.handle_input("(", window, cx);
10291 });
10292 cx.assert_editor_state(
10293 &r#"
10294 <body><a b={c(ˇ)}>
10295 <script>
10296 var x = 1;<a b={c(ˇ)}
10297 </script>
10298 </body><a b={c(ˇ)}>
10299 "#
10300 .unindent(),
10301 );
10302
10303 // Brackets that were already autoclosed are skipped.
10304 cx.update_editor(|editor, window, cx| {
10305 editor.handle_input(")", window, cx);
10306 editor.handle_input("d", window, cx);
10307 editor.handle_input("}", window, cx);
10308 });
10309 cx.assert_editor_state(
10310 &r#"
10311 <body><a b={c()d}ˇ>
10312 <script>
10313 var x = 1;<a b={c()d}ˇ
10314 </script>
10315 </body><a b={c()d}ˇ>
10316 "#
10317 .unindent(),
10318 );
10319 cx.update_editor(|editor, window, cx| {
10320 editor.handle_input(">", window, cx);
10321 });
10322 cx.assert_editor_state(
10323 &r#"
10324 <body><a b={c()d}>ˇ
10325 <script>
10326 var x = 1;<a b={c()d}>ˇ
10327 </script>
10328 </body><a b={c()d}>ˇ
10329 "#
10330 .unindent(),
10331 );
10332
10333 // Reset
10334 cx.set_state(
10335 &r#"
10336 <body>ˇ
10337 <script>
10338 var x = 1;ˇ
10339 </script>
10340 </body>ˇ
10341 "#
10342 .unindent(),
10343 );
10344
10345 cx.update_editor(|editor, window, cx| {
10346 editor.handle_input("<", window, cx);
10347 });
10348 cx.assert_editor_state(
10349 &r#"
10350 <body><ˇ>
10351 <script>
10352 var x = 1;<ˇ
10353 </script>
10354 </body><ˇ>
10355 "#
10356 .unindent(),
10357 );
10358
10359 // When backspacing, the closing angle brackets are removed.
10360 cx.update_editor(|editor, window, cx| {
10361 editor.backspace(&Backspace, window, cx);
10362 });
10363 cx.assert_editor_state(
10364 &r#"
10365 <body>ˇ
10366 <script>
10367 var x = 1;ˇ
10368 </script>
10369 </body>ˇ
10370 "#
10371 .unindent(),
10372 );
10373
10374 // Block comments autoclose in JavaScript, but not HTML.
10375 cx.update_editor(|editor, window, cx| {
10376 editor.handle_input("/", window, cx);
10377 editor.handle_input("*", window, cx);
10378 });
10379 cx.assert_editor_state(
10380 &r#"
10381 <body>/*ˇ
10382 <script>
10383 var x = 1;/*ˇ */
10384 </script>
10385 </body>/*ˇ
10386 "#
10387 .unindent(),
10388 );
10389}
10390
10391#[gpui::test]
10392async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10393 init_test(cx, |_| {});
10394
10395 let mut cx = EditorTestContext::new(cx).await;
10396
10397 let rust_language = Arc::new(
10398 Language::new(
10399 LanguageConfig {
10400 name: "Rust".into(),
10401 brackets: serde_json::from_value(json!([
10402 { "start": "{", "end": "}", "close": true, "newline": true },
10403 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10404 ]))
10405 .unwrap(),
10406 autoclose_before: "})]>".into(),
10407 ..Default::default()
10408 },
10409 Some(tree_sitter_rust::LANGUAGE.into()),
10410 )
10411 .with_override_query("(string_literal) @string")
10412 .unwrap(),
10413 );
10414
10415 cx.language_registry().add(rust_language.clone());
10416 cx.update_buffer(|buffer, cx| {
10417 buffer.set_language(Some(rust_language), cx);
10418 });
10419
10420 cx.set_state(
10421 &r#"
10422 let x = ˇ
10423 "#
10424 .unindent(),
10425 );
10426
10427 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10428 cx.update_editor(|editor, window, cx| {
10429 editor.handle_input("\"", window, cx);
10430 });
10431 cx.assert_editor_state(
10432 &r#"
10433 let x = "ˇ"
10434 "#
10435 .unindent(),
10436 );
10437
10438 // Inserting another quotation mark. The cursor moves across the existing
10439 // automatically-inserted quotation mark.
10440 cx.update_editor(|editor, window, cx| {
10441 editor.handle_input("\"", window, cx);
10442 });
10443 cx.assert_editor_state(
10444 &r#"
10445 let x = ""ˇ
10446 "#
10447 .unindent(),
10448 );
10449
10450 // Reset
10451 cx.set_state(
10452 &r#"
10453 let x = ˇ
10454 "#
10455 .unindent(),
10456 );
10457
10458 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10459 cx.update_editor(|editor, window, cx| {
10460 editor.handle_input("\"", window, cx);
10461 editor.handle_input(" ", window, cx);
10462 editor.move_left(&Default::default(), window, cx);
10463 editor.handle_input("\\", window, cx);
10464 editor.handle_input("\"", window, cx);
10465 });
10466 cx.assert_editor_state(
10467 &r#"
10468 let x = "\"ˇ "
10469 "#
10470 .unindent(),
10471 );
10472
10473 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10474 // mark. Nothing is inserted.
10475 cx.update_editor(|editor, window, cx| {
10476 editor.move_right(&Default::default(), window, cx);
10477 editor.handle_input("\"", window, cx);
10478 });
10479 cx.assert_editor_state(
10480 &r#"
10481 let x = "\" "ˇ
10482 "#
10483 .unindent(),
10484 );
10485}
10486
10487#[gpui::test]
10488async fn test_surround_with_pair(cx: &mut TestAppContext) {
10489 init_test(cx, |_| {});
10490
10491 let language = Arc::new(Language::new(
10492 LanguageConfig {
10493 brackets: BracketPairConfig {
10494 pairs: vec![
10495 BracketPair {
10496 start: "{".to_string(),
10497 end: "}".to_string(),
10498 close: true,
10499 surround: true,
10500 newline: true,
10501 },
10502 BracketPair {
10503 start: "/* ".to_string(),
10504 end: "*/".to_string(),
10505 close: true,
10506 surround: true,
10507 ..Default::default()
10508 },
10509 ],
10510 ..Default::default()
10511 },
10512 ..Default::default()
10513 },
10514 Some(tree_sitter_rust::LANGUAGE.into()),
10515 ));
10516
10517 let text = r#"
10518 a
10519 b
10520 c
10521 "#
10522 .unindent();
10523
10524 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10525 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10526 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10527 editor
10528 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10529 .await;
10530
10531 editor.update_in(cx, |editor, window, cx| {
10532 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10533 s.select_display_ranges([
10534 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10535 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10536 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10537 ])
10538 });
10539
10540 editor.handle_input("{", window, cx);
10541 editor.handle_input("{", window, cx);
10542 editor.handle_input("{", window, cx);
10543 assert_eq!(
10544 editor.text(cx),
10545 "
10546 {{{a}}}
10547 {{{b}}}
10548 {{{c}}}
10549 "
10550 .unindent()
10551 );
10552 assert_eq!(
10553 editor.selections.display_ranges(cx),
10554 [
10555 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10556 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10557 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10558 ]
10559 );
10560
10561 editor.undo(&Undo, window, cx);
10562 editor.undo(&Undo, window, cx);
10563 editor.undo(&Undo, window, cx);
10564 assert_eq!(
10565 editor.text(cx),
10566 "
10567 a
10568 b
10569 c
10570 "
10571 .unindent()
10572 );
10573 assert_eq!(
10574 editor.selections.display_ranges(cx),
10575 [
10576 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10577 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10578 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10579 ]
10580 );
10581
10582 // Ensure inserting the first character of a multi-byte bracket pair
10583 // doesn't surround the selections with the bracket.
10584 editor.handle_input("/", window, cx);
10585 assert_eq!(
10586 editor.text(cx),
10587 "
10588 /
10589 /
10590 /
10591 "
10592 .unindent()
10593 );
10594 assert_eq!(
10595 editor.selections.display_ranges(cx),
10596 [
10597 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10598 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10599 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10600 ]
10601 );
10602
10603 editor.undo(&Undo, window, cx);
10604 assert_eq!(
10605 editor.text(cx),
10606 "
10607 a
10608 b
10609 c
10610 "
10611 .unindent()
10612 );
10613 assert_eq!(
10614 editor.selections.display_ranges(cx),
10615 [
10616 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10617 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10618 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10619 ]
10620 );
10621
10622 // Ensure inserting the last character of a multi-byte bracket pair
10623 // doesn't surround the selections with the bracket.
10624 editor.handle_input("*", window, cx);
10625 assert_eq!(
10626 editor.text(cx),
10627 "
10628 *
10629 *
10630 *
10631 "
10632 .unindent()
10633 );
10634 assert_eq!(
10635 editor.selections.display_ranges(cx),
10636 [
10637 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10638 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10639 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10640 ]
10641 );
10642 });
10643}
10644
10645#[gpui::test]
10646async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10647 init_test(cx, |_| {});
10648
10649 let language = Arc::new(Language::new(
10650 LanguageConfig {
10651 brackets: BracketPairConfig {
10652 pairs: vec![BracketPair {
10653 start: "{".to_string(),
10654 end: "}".to_string(),
10655 close: true,
10656 surround: true,
10657 newline: true,
10658 }],
10659 ..Default::default()
10660 },
10661 autoclose_before: "}".to_string(),
10662 ..Default::default()
10663 },
10664 Some(tree_sitter_rust::LANGUAGE.into()),
10665 ));
10666
10667 let text = r#"
10668 a
10669 b
10670 c
10671 "#
10672 .unindent();
10673
10674 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10675 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10676 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10677 editor
10678 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10679 .await;
10680
10681 editor.update_in(cx, |editor, window, cx| {
10682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10683 s.select_ranges([
10684 Point::new(0, 1)..Point::new(0, 1),
10685 Point::new(1, 1)..Point::new(1, 1),
10686 Point::new(2, 1)..Point::new(2, 1),
10687 ])
10688 });
10689
10690 editor.handle_input("{", window, cx);
10691 editor.handle_input("{", window, cx);
10692 editor.handle_input("_", window, cx);
10693 assert_eq!(
10694 editor.text(cx),
10695 "
10696 a{{_}}
10697 b{{_}}
10698 c{{_}}
10699 "
10700 .unindent()
10701 );
10702 assert_eq!(
10703 editor.selections.ranges::<Point>(cx),
10704 [
10705 Point::new(0, 4)..Point::new(0, 4),
10706 Point::new(1, 4)..Point::new(1, 4),
10707 Point::new(2, 4)..Point::new(2, 4)
10708 ]
10709 );
10710
10711 editor.backspace(&Default::default(), window, cx);
10712 editor.backspace(&Default::default(), window, cx);
10713 assert_eq!(
10714 editor.text(cx),
10715 "
10716 a{}
10717 b{}
10718 c{}
10719 "
10720 .unindent()
10721 );
10722 assert_eq!(
10723 editor.selections.ranges::<Point>(cx),
10724 [
10725 Point::new(0, 2)..Point::new(0, 2),
10726 Point::new(1, 2)..Point::new(1, 2),
10727 Point::new(2, 2)..Point::new(2, 2)
10728 ]
10729 );
10730
10731 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10732 assert_eq!(
10733 editor.text(cx),
10734 "
10735 a
10736 b
10737 c
10738 "
10739 .unindent()
10740 );
10741 assert_eq!(
10742 editor.selections.ranges::<Point>(cx),
10743 [
10744 Point::new(0, 1)..Point::new(0, 1),
10745 Point::new(1, 1)..Point::new(1, 1),
10746 Point::new(2, 1)..Point::new(2, 1)
10747 ]
10748 );
10749 });
10750}
10751
10752#[gpui::test]
10753async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10754 init_test(cx, |settings| {
10755 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10756 });
10757
10758 let mut cx = EditorTestContext::new(cx).await;
10759
10760 let language = Arc::new(Language::new(
10761 LanguageConfig {
10762 brackets: BracketPairConfig {
10763 pairs: vec![
10764 BracketPair {
10765 start: "{".to_string(),
10766 end: "}".to_string(),
10767 close: true,
10768 surround: true,
10769 newline: true,
10770 },
10771 BracketPair {
10772 start: "(".to_string(),
10773 end: ")".to_string(),
10774 close: true,
10775 surround: true,
10776 newline: true,
10777 },
10778 BracketPair {
10779 start: "[".to_string(),
10780 end: "]".to_string(),
10781 close: false,
10782 surround: true,
10783 newline: true,
10784 },
10785 ],
10786 ..Default::default()
10787 },
10788 autoclose_before: "})]".to_string(),
10789 ..Default::default()
10790 },
10791 Some(tree_sitter_rust::LANGUAGE.into()),
10792 ));
10793
10794 cx.language_registry().add(language.clone());
10795 cx.update_buffer(|buffer, cx| {
10796 buffer.set_language(Some(language), cx);
10797 });
10798
10799 cx.set_state(
10800 &"
10801 {(ˇ)}
10802 [[ˇ]]
10803 {(ˇ)}
10804 "
10805 .unindent(),
10806 );
10807
10808 cx.update_editor(|editor, window, cx| {
10809 editor.backspace(&Default::default(), window, cx);
10810 editor.backspace(&Default::default(), window, cx);
10811 });
10812
10813 cx.assert_editor_state(
10814 &"
10815 ˇ
10816 ˇ]]
10817 ˇ
10818 "
10819 .unindent(),
10820 );
10821
10822 cx.update_editor(|editor, window, cx| {
10823 editor.handle_input("{", window, cx);
10824 editor.handle_input("{", window, cx);
10825 editor.move_right(&MoveRight, window, cx);
10826 editor.move_right(&MoveRight, window, cx);
10827 editor.move_left(&MoveLeft, window, cx);
10828 editor.move_left(&MoveLeft, window, cx);
10829 editor.backspace(&Default::default(), window, cx);
10830 });
10831
10832 cx.assert_editor_state(
10833 &"
10834 {ˇ}
10835 {ˇ}]]
10836 {ˇ}
10837 "
10838 .unindent(),
10839 );
10840
10841 cx.update_editor(|editor, window, cx| {
10842 editor.backspace(&Default::default(), window, cx);
10843 });
10844
10845 cx.assert_editor_state(
10846 &"
10847 ˇ
10848 ˇ]]
10849 ˇ
10850 "
10851 .unindent(),
10852 );
10853}
10854
10855#[gpui::test]
10856async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10857 init_test(cx, |_| {});
10858
10859 let language = Arc::new(Language::new(
10860 LanguageConfig::default(),
10861 Some(tree_sitter_rust::LANGUAGE.into()),
10862 ));
10863
10864 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10865 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10866 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10867 editor
10868 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10869 .await;
10870
10871 editor.update_in(cx, |editor, window, cx| {
10872 editor.set_auto_replace_emoji_shortcode(true);
10873
10874 editor.handle_input("Hello ", window, cx);
10875 editor.handle_input(":wave", window, cx);
10876 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10877
10878 editor.handle_input(":", window, cx);
10879 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10880
10881 editor.handle_input(" :smile", window, cx);
10882 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10883
10884 editor.handle_input(":", window, cx);
10885 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10886
10887 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10888 editor.handle_input(":wave", window, cx);
10889 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10890
10891 editor.handle_input(":", window, cx);
10892 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10893
10894 editor.handle_input(":1", window, cx);
10895 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10896
10897 editor.handle_input(":", window, cx);
10898 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10899
10900 // Ensure shortcode does not get replaced when it is part of a word
10901 editor.handle_input(" Test:wave", window, cx);
10902 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10903
10904 editor.handle_input(":", window, cx);
10905 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10906
10907 editor.set_auto_replace_emoji_shortcode(false);
10908
10909 // Ensure shortcode does not get replaced when auto replace is off
10910 editor.handle_input(" :wave", window, cx);
10911 assert_eq!(
10912 editor.text(cx),
10913 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10914 );
10915
10916 editor.handle_input(":", window, cx);
10917 assert_eq!(
10918 editor.text(cx),
10919 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10920 );
10921 });
10922}
10923
10924#[gpui::test]
10925async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10926 init_test(cx, |_| {});
10927
10928 let (text, insertion_ranges) = marked_text_ranges(
10929 indoc! {"
10930 ˇ
10931 "},
10932 false,
10933 );
10934
10935 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10936 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10937
10938 _ = editor.update_in(cx, |editor, window, cx| {
10939 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10940
10941 editor
10942 .insert_snippet(&insertion_ranges, snippet, window, cx)
10943 .unwrap();
10944
10945 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10946 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10947 assert_eq!(editor.text(cx), expected_text);
10948 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10949 }
10950
10951 assert(
10952 editor,
10953 cx,
10954 indoc! {"
10955 type «» =•
10956 "},
10957 );
10958
10959 assert!(editor.context_menu_visible(), "There should be a matches");
10960 });
10961}
10962
10963#[gpui::test]
10964async fn test_snippets(cx: &mut TestAppContext) {
10965 init_test(cx, |_| {});
10966
10967 let mut cx = EditorTestContext::new(cx).await;
10968
10969 cx.set_state(indoc! {"
10970 a.ˇ b
10971 a.ˇ b
10972 a.ˇ b
10973 "});
10974
10975 cx.update_editor(|editor, window, cx| {
10976 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10977 let insertion_ranges = editor
10978 .selections
10979 .all(cx)
10980 .iter()
10981 .map(|s| s.range())
10982 .collect::<Vec<_>>();
10983 editor
10984 .insert_snippet(&insertion_ranges, snippet, window, cx)
10985 .unwrap();
10986 });
10987
10988 cx.assert_editor_state(indoc! {"
10989 a.f(«oneˇ», two, «threeˇ») b
10990 a.f(«oneˇ», two, «threeˇ») b
10991 a.f(«oneˇ», two, «threeˇ») b
10992 "});
10993
10994 // Can't move earlier than the first tab stop
10995 cx.update_editor(|editor, window, cx| {
10996 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10997 });
10998 cx.assert_editor_state(indoc! {"
10999 a.f(«oneˇ», two, «threeˇ») b
11000 a.f(«oneˇ», two, «threeˇ») b
11001 a.f(«oneˇ», two, «threeˇ») b
11002 "});
11003
11004 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11005 cx.assert_editor_state(indoc! {"
11006 a.f(one, «twoˇ», three) b
11007 a.f(one, «twoˇ», three) b
11008 a.f(one, «twoˇ», three) b
11009 "});
11010
11011 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11012 cx.assert_editor_state(indoc! {"
11013 a.f(«oneˇ», two, «threeˇ») b
11014 a.f(«oneˇ», two, «threeˇ») b
11015 a.f(«oneˇ», two, «threeˇ») b
11016 "});
11017
11018 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11019 cx.assert_editor_state(indoc! {"
11020 a.f(one, «twoˇ», three) b
11021 a.f(one, «twoˇ», three) b
11022 a.f(one, «twoˇ», three) b
11023 "});
11024 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11025 cx.assert_editor_state(indoc! {"
11026 a.f(one, two, three)ˇ b
11027 a.f(one, two, three)ˇ b
11028 a.f(one, two, three)ˇ b
11029 "});
11030
11031 // As soon as the last tab stop is reached, snippet state is gone
11032 cx.update_editor(|editor, window, cx| {
11033 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11034 });
11035 cx.assert_editor_state(indoc! {"
11036 a.f(one, two, three)ˇ b
11037 a.f(one, two, three)ˇ b
11038 a.f(one, two, three)ˇ b
11039 "});
11040}
11041
11042#[gpui::test]
11043async fn test_snippet_indentation(cx: &mut TestAppContext) {
11044 init_test(cx, |_| {});
11045
11046 let mut cx = EditorTestContext::new(cx).await;
11047
11048 cx.update_editor(|editor, window, cx| {
11049 let snippet = Snippet::parse(indoc! {"
11050 /*
11051 * Multiline comment with leading indentation
11052 *
11053 * $1
11054 */
11055 $0"})
11056 .unwrap();
11057 let insertion_ranges = editor
11058 .selections
11059 .all(cx)
11060 .iter()
11061 .map(|s| s.range())
11062 .collect::<Vec<_>>();
11063 editor
11064 .insert_snippet(&insertion_ranges, snippet, window, cx)
11065 .unwrap();
11066 });
11067
11068 cx.assert_editor_state(indoc! {"
11069 /*
11070 * Multiline comment with leading indentation
11071 *
11072 * ˇ
11073 */
11074 "});
11075
11076 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11077 cx.assert_editor_state(indoc! {"
11078 /*
11079 * Multiline comment with leading indentation
11080 *
11081 *•
11082 */
11083 ˇ"});
11084}
11085
11086#[gpui::test]
11087async fn test_document_format_during_save(cx: &mut TestAppContext) {
11088 init_test(cx, |_| {});
11089
11090 let fs = FakeFs::new(cx.executor());
11091 fs.insert_file(path!("/file.rs"), Default::default()).await;
11092
11093 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11094
11095 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11096 language_registry.add(rust_lang());
11097 let mut fake_servers = language_registry.register_fake_lsp(
11098 "Rust",
11099 FakeLspAdapter {
11100 capabilities: lsp::ServerCapabilities {
11101 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11102 ..Default::default()
11103 },
11104 ..Default::default()
11105 },
11106 );
11107
11108 let buffer = project
11109 .update(cx, |project, cx| {
11110 project.open_local_buffer(path!("/file.rs"), cx)
11111 })
11112 .await
11113 .unwrap();
11114
11115 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11116 let (editor, cx) = cx.add_window_view(|window, cx| {
11117 build_editor_with_project(project.clone(), buffer, window, cx)
11118 });
11119 editor.update_in(cx, |editor, window, cx| {
11120 editor.set_text("one\ntwo\nthree\n", window, cx)
11121 });
11122 assert!(cx.read(|cx| editor.is_dirty(cx)));
11123
11124 cx.executor().start_waiting();
11125 let fake_server = fake_servers.next().await.unwrap();
11126
11127 {
11128 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11129 move |params, _| async move {
11130 assert_eq!(
11131 params.text_document.uri,
11132 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11133 );
11134 assert_eq!(params.options.tab_size, 4);
11135 Ok(Some(vec![lsp::TextEdit::new(
11136 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11137 ", ".to_string(),
11138 )]))
11139 },
11140 );
11141 let save = editor
11142 .update_in(cx, |editor, window, cx| {
11143 editor.save(
11144 SaveOptions {
11145 format: true,
11146 autosave: false,
11147 },
11148 project.clone(),
11149 window,
11150 cx,
11151 )
11152 })
11153 .unwrap();
11154 cx.executor().start_waiting();
11155 save.await;
11156
11157 assert_eq!(
11158 editor.update(cx, |editor, cx| editor.text(cx)),
11159 "one, two\nthree\n"
11160 );
11161 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11162 }
11163
11164 {
11165 editor.update_in(cx, |editor, window, cx| {
11166 editor.set_text("one\ntwo\nthree\n", window, cx)
11167 });
11168 assert!(cx.read(|cx| editor.is_dirty(cx)));
11169
11170 // Ensure we can still save even if formatting hangs.
11171 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11172 move |params, _| async move {
11173 assert_eq!(
11174 params.text_document.uri,
11175 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11176 );
11177 futures::future::pending::<()>().await;
11178 unreachable!()
11179 },
11180 );
11181 let save = editor
11182 .update_in(cx, |editor, window, cx| {
11183 editor.save(
11184 SaveOptions {
11185 format: true,
11186 autosave: false,
11187 },
11188 project.clone(),
11189 window,
11190 cx,
11191 )
11192 })
11193 .unwrap();
11194 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11195 cx.executor().start_waiting();
11196 save.await;
11197 assert_eq!(
11198 editor.update(cx, |editor, cx| editor.text(cx)),
11199 "one\ntwo\nthree\n"
11200 );
11201 }
11202
11203 // Set rust language override and assert overridden tabsize is sent to language server
11204 update_test_language_settings(cx, |settings| {
11205 settings.languages.0.insert(
11206 "Rust".into(),
11207 LanguageSettingsContent {
11208 tab_size: NonZeroU32::new(8),
11209 ..Default::default()
11210 },
11211 );
11212 });
11213
11214 {
11215 editor.update_in(cx, |editor, window, cx| {
11216 editor.set_text("somehting_new\n", window, cx)
11217 });
11218 assert!(cx.read(|cx| editor.is_dirty(cx)));
11219 let _formatting_request_signal = fake_server
11220 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11221 assert_eq!(
11222 params.text_document.uri,
11223 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11224 );
11225 assert_eq!(params.options.tab_size, 8);
11226 Ok(Some(vec![]))
11227 });
11228 let save = editor
11229 .update_in(cx, |editor, window, cx| {
11230 editor.save(
11231 SaveOptions {
11232 format: true,
11233 autosave: false,
11234 },
11235 project.clone(),
11236 window,
11237 cx,
11238 )
11239 })
11240 .unwrap();
11241 cx.executor().start_waiting();
11242 save.await;
11243 }
11244}
11245
11246#[gpui::test]
11247async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11248 init_test(cx, |settings| {
11249 settings.defaults.ensure_final_newline_on_save = Some(false);
11250 });
11251
11252 let fs = FakeFs::new(cx.executor());
11253 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11254
11255 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11256
11257 let buffer = project
11258 .update(cx, |project, cx| {
11259 project.open_local_buffer(path!("/file.txt"), cx)
11260 })
11261 .await
11262 .unwrap();
11263
11264 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11265 let (editor, cx) = cx.add_window_view(|window, cx| {
11266 build_editor_with_project(project.clone(), buffer, window, cx)
11267 });
11268 editor.update_in(cx, |editor, window, cx| {
11269 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11270 s.select_ranges([0..0])
11271 });
11272 });
11273 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11274
11275 editor.update_in(cx, |editor, window, cx| {
11276 editor.handle_input("\n", window, cx)
11277 });
11278 cx.run_until_parked();
11279 save(&editor, &project, cx).await;
11280 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11281
11282 editor.update_in(cx, |editor, window, cx| {
11283 editor.undo(&Default::default(), window, cx);
11284 });
11285 save(&editor, &project, cx).await;
11286 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11287
11288 editor.update_in(cx, |editor, window, cx| {
11289 editor.redo(&Default::default(), window, cx);
11290 });
11291 cx.run_until_parked();
11292 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11293
11294 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11295 let save = editor
11296 .update_in(cx, |editor, window, cx| {
11297 editor.save(
11298 SaveOptions {
11299 format: true,
11300 autosave: false,
11301 },
11302 project.clone(),
11303 window,
11304 cx,
11305 )
11306 })
11307 .unwrap();
11308 cx.executor().start_waiting();
11309 save.await;
11310 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11311 }
11312}
11313
11314#[gpui::test]
11315async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11316 init_test(cx, |_| {});
11317
11318 let cols = 4;
11319 let rows = 10;
11320 let sample_text_1 = sample_text(rows, cols, 'a');
11321 assert_eq!(
11322 sample_text_1,
11323 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11324 );
11325 let sample_text_2 = sample_text(rows, cols, 'l');
11326 assert_eq!(
11327 sample_text_2,
11328 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11329 );
11330 let sample_text_3 = sample_text(rows, cols, 'v');
11331 assert_eq!(
11332 sample_text_3,
11333 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11334 );
11335
11336 let fs = FakeFs::new(cx.executor());
11337 fs.insert_tree(
11338 path!("/a"),
11339 json!({
11340 "main.rs": sample_text_1,
11341 "other.rs": sample_text_2,
11342 "lib.rs": sample_text_3,
11343 }),
11344 )
11345 .await;
11346
11347 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11348 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11349 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11350
11351 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11352 language_registry.add(rust_lang());
11353 let mut fake_servers = language_registry.register_fake_lsp(
11354 "Rust",
11355 FakeLspAdapter {
11356 capabilities: lsp::ServerCapabilities {
11357 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11358 ..Default::default()
11359 },
11360 ..Default::default()
11361 },
11362 );
11363
11364 let worktree = project.update(cx, |project, cx| {
11365 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11366 assert_eq!(worktrees.len(), 1);
11367 worktrees.pop().unwrap()
11368 });
11369 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11370
11371 let buffer_1 = project
11372 .update(cx, |project, cx| {
11373 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11374 })
11375 .await
11376 .unwrap();
11377 let buffer_2 = project
11378 .update(cx, |project, cx| {
11379 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11380 })
11381 .await
11382 .unwrap();
11383 let buffer_3 = project
11384 .update(cx, |project, cx| {
11385 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11386 })
11387 .await
11388 .unwrap();
11389
11390 let multi_buffer = cx.new(|cx| {
11391 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11392 multi_buffer.push_excerpts(
11393 buffer_1.clone(),
11394 [
11395 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11396 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11397 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11398 ],
11399 cx,
11400 );
11401 multi_buffer.push_excerpts(
11402 buffer_2.clone(),
11403 [
11404 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11405 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11406 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11407 ],
11408 cx,
11409 );
11410 multi_buffer.push_excerpts(
11411 buffer_3.clone(),
11412 [
11413 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11414 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11415 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11416 ],
11417 cx,
11418 );
11419 multi_buffer
11420 });
11421 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11422 Editor::new(
11423 EditorMode::full(),
11424 multi_buffer,
11425 Some(project.clone()),
11426 window,
11427 cx,
11428 )
11429 });
11430
11431 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11432 editor.change_selections(
11433 SelectionEffects::scroll(Autoscroll::Next),
11434 window,
11435 cx,
11436 |s| s.select_ranges(Some(1..2)),
11437 );
11438 editor.insert("|one|two|three|", window, cx);
11439 });
11440 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11441 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11442 editor.change_selections(
11443 SelectionEffects::scroll(Autoscroll::Next),
11444 window,
11445 cx,
11446 |s| s.select_ranges(Some(60..70)),
11447 );
11448 editor.insert("|four|five|six|", window, cx);
11449 });
11450 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11451
11452 // First two buffers should be edited, but not the third one.
11453 assert_eq!(
11454 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11455 "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}",
11456 );
11457 buffer_1.update(cx, |buffer, _| {
11458 assert!(buffer.is_dirty());
11459 assert_eq!(
11460 buffer.text(),
11461 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11462 )
11463 });
11464 buffer_2.update(cx, |buffer, _| {
11465 assert!(buffer.is_dirty());
11466 assert_eq!(
11467 buffer.text(),
11468 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11469 )
11470 });
11471 buffer_3.update(cx, |buffer, _| {
11472 assert!(!buffer.is_dirty());
11473 assert_eq!(buffer.text(), sample_text_3,)
11474 });
11475 cx.executor().run_until_parked();
11476
11477 cx.executor().start_waiting();
11478 let save = multi_buffer_editor
11479 .update_in(cx, |editor, window, cx| {
11480 editor.save(
11481 SaveOptions {
11482 format: true,
11483 autosave: false,
11484 },
11485 project.clone(),
11486 window,
11487 cx,
11488 )
11489 })
11490 .unwrap();
11491
11492 let fake_server = fake_servers.next().await.unwrap();
11493 fake_server
11494 .server
11495 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11496 Ok(Some(vec![lsp::TextEdit::new(
11497 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11498 format!("[{} formatted]", params.text_document.uri),
11499 )]))
11500 })
11501 .detach();
11502 save.await;
11503
11504 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11505 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11506 assert_eq!(
11507 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11508 uri!(
11509 "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}"
11510 ),
11511 );
11512 buffer_1.update(cx, |buffer, _| {
11513 assert!(!buffer.is_dirty());
11514 assert_eq!(
11515 buffer.text(),
11516 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11517 )
11518 });
11519 buffer_2.update(cx, |buffer, _| {
11520 assert!(!buffer.is_dirty());
11521 assert_eq!(
11522 buffer.text(),
11523 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11524 )
11525 });
11526 buffer_3.update(cx, |buffer, _| {
11527 assert!(!buffer.is_dirty());
11528 assert_eq!(buffer.text(), sample_text_3,)
11529 });
11530}
11531
11532#[gpui::test]
11533async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11534 init_test(cx, |_| {});
11535
11536 let fs = FakeFs::new(cx.executor());
11537 fs.insert_tree(
11538 path!("/dir"),
11539 json!({
11540 "file1.rs": "fn main() { println!(\"hello\"); }",
11541 "file2.rs": "fn test() { println!(\"test\"); }",
11542 "file3.rs": "fn other() { println!(\"other\"); }\n",
11543 }),
11544 )
11545 .await;
11546
11547 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11548 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11549 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11550
11551 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11552 language_registry.add(rust_lang());
11553
11554 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11555 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11556
11557 // Open three buffers
11558 let buffer_1 = project
11559 .update(cx, |project, cx| {
11560 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11561 })
11562 .await
11563 .unwrap();
11564 let buffer_2 = project
11565 .update(cx, |project, cx| {
11566 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11567 })
11568 .await
11569 .unwrap();
11570 let buffer_3 = project
11571 .update(cx, |project, cx| {
11572 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11573 })
11574 .await
11575 .unwrap();
11576
11577 // Create a multi-buffer with all three buffers
11578 let multi_buffer = cx.new(|cx| {
11579 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11580 multi_buffer.push_excerpts(
11581 buffer_1.clone(),
11582 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11583 cx,
11584 );
11585 multi_buffer.push_excerpts(
11586 buffer_2.clone(),
11587 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11588 cx,
11589 );
11590 multi_buffer.push_excerpts(
11591 buffer_3.clone(),
11592 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11593 cx,
11594 );
11595 multi_buffer
11596 });
11597
11598 let editor = cx.new_window_entity(|window, cx| {
11599 Editor::new(
11600 EditorMode::full(),
11601 multi_buffer,
11602 Some(project.clone()),
11603 window,
11604 cx,
11605 )
11606 });
11607
11608 // Edit only the first buffer
11609 editor.update_in(cx, |editor, window, cx| {
11610 editor.change_selections(
11611 SelectionEffects::scroll(Autoscroll::Next),
11612 window,
11613 cx,
11614 |s| s.select_ranges(Some(10..10)),
11615 );
11616 editor.insert("// edited", window, cx);
11617 });
11618
11619 // Verify that only buffer 1 is dirty
11620 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11621 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11622 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11623
11624 // Get write counts after file creation (files were created with initial content)
11625 // We expect each file to have been written once during creation
11626 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11627 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11628 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11629
11630 // Perform autosave
11631 let save_task = editor.update_in(cx, |editor, window, cx| {
11632 editor.save(
11633 SaveOptions {
11634 format: true,
11635 autosave: true,
11636 },
11637 project.clone(),
11638 window,
11639 cx,
11640 )
11641 });
11642 save_task.await.unwrap();
11643
11644 // Only the dirty buffer should have been saved
11645 assert_eq!(
11646 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11647 1,
11648 "Buffer 1 was dirty, so it should have been written once during autosave"
11649 );
11650 assert_eq!(
11651 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11652 0,
11653 "Buffer 2 was clean, so it should not have been written during autosave"
11654 );
11655 assert_eq!(
11656 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11657 0,
11658 "Buffer 3 was clean, so it should not have been written during autosave"
11659 );
11660
11661 // Verify buffer states after autosave
11662 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11663 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11664 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11665
11666 // Now perform a manual save (format = true)
11667 let save_task = editor.update_in(cx, |editor, window, cx| {
11668 editor.save(
11669 SaveOptions {
11670 format: true,
11671 autosave: false,
11672 },
11673 project.clone(),
11674 window,
11675 cx,
11676 )
11677 });
11678 save_task.await.unwrap();
11679
11680 // During manual save, clean buffers don't get written to disk
11681 // They just get did_save called for language server notifications
11682 assert_eq!(
11683 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11684 1,
11685 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11686 );
11687 assert_eq!(
11688 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11689 0,
11690 "Buffer 2 should not have been written at all"
11691 );
11692 assert_eq!(
11693 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11694 0,
11695 "Buffer 3 should not have been written at all"
11696 );
11697}
11698
11699async fn setup_range_format_test(
11700 cx: &mut TestAppContext,
11701) -> (
11702 Entity<Project>,
11703 Entity<Editor>,
11704 &mut gpui::VisualTestContext,
11705 lsp::FakeLanguageServer,
11706) {
11707 init_test(cx, |_| {});
11708
11709 let fs = FakeFs::new(cx.executor());
11710 fs.insert_file(path!("/file.rs"), Default::default()).await;
11711
11712 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11713
11714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11715 language_registry.add(rust_lang());
11716 let mut fake_servers = language_registry.register_fake_lsp(
11717 "Rust",
11718 FakeLspAdapter {
11719 capabilities: lsp::ServerCapabilities {
11720 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11721 ..lsp::ServerCapabilities::default()
11722 },
11723 ..FakeLspAdapter::default()
11724 },
11725 );
11726
11727 let buffer = project
11728 .update(cx, |project, cx| {
11729 project.open_local_buffer(path!("/file.rs"), cx)
11730 })
11731 .await
11732 .unwrap();
11733
11734 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11735 let (editor, cx) = cx.add_window_view(|window, cx| {
11736 build_editor_with_project(project.clone(), buffer, window, cx)
11737 });
11738
11739 cx.executor().start_waiting();
11740 let fake_server = fake_servers.next().await.unwrap();
11741
11742 (project, editor, cx, fake_server)
11743}
11744
11745#[gpui::test]
11746async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11747 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11748
11749 editor.update_in(cx, |editor, window, cx| {
11750 editor.set_text("one\ntwo\nthree\n", window, cx)
11751 });
11752 assert!(cx.read(|cx| editor.is_dirty(cx)));
11753
11754 let save = editor
11755 .update_in(cx, |editor, window, cx| {
11756 editor.save(
11757 SaveOptions {
11758 format: true,
11759 autosave: false,
11760 },
11761 project.clone(),
11762 window,
11763 cx,
11764 )
11765 })
11766 .unwrap();
11767 fake_server
11768 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11769 assert_eq!(
11770 params.text_document.uri,
11771 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11772 );
11773 assert_eq!(params.options.tab_size, 4);
11774 Ok(Some(vec![lsp::TextEdit::new(
11775 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11776 ", ".to_string(),
11777 )]))
11778 })
11779 .next()
11780 .await;
11781 cx.executor().start_waiting();
11782 save.await;
11783 assert_eq!(
11784 editor.update(cx, |editor, cx| editor.text(cx)),
11785 "one, two\nthree\n"
11786 );
11787 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11788}
11789
11790#[gpui::test]
11791async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11792 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11793
11794 editor.update_in(cx, |editor, window, cx| {
11795 editor.set_text("one\ntwo\nthree\n", window, cx)
11796 });
11797 assert!(cx.read(|cx| editor.is_dirty(cx)));
11798
11799 // Test that save still works when formatting hangs
11800 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11801 move |params, _| async move {
11802 assert_eq!(
11803 params.text_document.uri,
11804 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11805 );
11806 futures::future::pending::<()>().await;
11807 unreachable!()
11808 },
11809 );
11810 let save = editor
11811 .update_in(cx, |editor, window, cx| {
11812 editor.save(
11813 SaveOptions {
11814 format: true,
11815 autosave: false,
11816 },
11817 project.clone(),
11818 window,
11819 cx,
11820 )
11821 })
11822 .unwrap();
11823 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11824 cx.executor().start_waiting();
11825 save.await;
11826 assert_eq!(
11827 editor.update(cx, |editor, cx| editor.text(cx)),
11828 "one\ntwo\nthree\n"
11829 );
11830 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11831}
11832
11833#[gpui::test]
11834async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11835 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11836
11837 // Buffer starts clean, no formatting should be requested
11838 let save = editor
11839 .update_in(cx, |editor, window, cx| {
11840 editor.save(
11841 SaveOptions {
11842 format: false,
11843 autosave: false,
11844 },
11845 project.clone(),
11846 window,
11847 cx,
11848 )
11849 })
11850 .unwrap();
11851 let _pending_format_request = fake_server
11852 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11853 panic!("Should not be invoked");
11854 })
11855 .next();
11856 cx.executor().start_waiting();
11857 save.await;
11858 cx.run_until_parked();
11859}
11860
11861#[gpui::test]
11862async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11863 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11864
11865 // Set Rust language override and assert overridden tabsize is sent to language server
11866 update_test_language_settings(cx, |settings| {
11867 settings.languages.0.insert(
11868 "Rust".into(),
11869 LanguageSettingsContent {
11870 tab_size: NonZeroU32::new(8),
11871 ..Default::default()
11872 },
11873 );
11874 });
11875
11876 editor.update_in(cx, |editor, window, cx| {
11877 editor.set_text("something_new\n", window, cx)
11878 });
11879 assert!(cx.read(|cx| editor.is_dirty(cx)));
11880 let save = editor
11881 .update_in(cx, |editor, window, cx| {
11882 editor.save(
11883 SaveOptions {
11884 format: true,
11885 autosave: false,
11886 },
11887 project.clone(),
11888 window,
11889 cx,
11890 )
11891 })
11892 .unwrap();
11893 fake_server
11894 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11895 assert_eq!(
11896 params.text_document.uri,
11897 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11898 );
11899 assert_eq!(params.options.tab_size, 8);
11900 Ok(Some(Vec::new()))
11901 })
11902 .next()
11903 .await;
11904 save.await;
11905}
11906
11907#[gpui::test]
11908async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11909 init_test(cx, |settings| {
11910 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
11911 settings::LanguageServerFormatterSpecifier::Current,
11912 )))
11913 });
11914
11915 let fs = FakeFs::new(cx.executor());
11916 fs.insert_file(path!("/file.rs"), Default::default()).await;
11917
11918 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11919
11920 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11921 language_registry.add(Arc::new(Language::new(
11922 LanguageConfig {
11923 name: "Rust".into(),
11924 matcher: LanguageMatcher {
11925 path_suffixes: vec!["rs".to_string()],
11926 ..Default::default()
11927 },
11928 ..LanguageConfig::default()
11929 },
11930 Some(tree_sitter_rust::LANGUAGE.into()),
11931 )));
11932 update_test_language_settings(cx, |settings| {
11933 // Enable Prettier formatting for the same buffer, and ensure
11934 // LSP is called instead of Prettier.
11935 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11936 });
11937 let mut fake_servers = language_registry.register_fake_lsp(
11938 "Rust",
11939 FakeLspAdapter {
11940 capabilities: lsp::ServerCapabilities {
11941 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11942 ..Default::default()
11943 },
11944 ..Default::default()
11945 },
11946 );
11947
11948 let buffer = project
11949 .update(cx, |project, cx| {
11950 project.open_local_buffer(path!("/file.rs"), cx)
11951 })
11952 .await
11953 .unwrap();
11954
11955 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11956 let (editor, cx) = cx.add_window_view(|window, cx| {
11957 build_editor_with_project(project.clone(), buffer, window, cx)
11958 });
11959 editor.update_in(cx, |editor, window, cx| {
11960 editor.set_text("one\ntwo\nthree\n", window, cx)
11961 });
11962
11963 cx.executor().start_waiting();
11964 let fake_server = fake_servers.next().await.unwrap();
11965
11966 let format = editor
11967 .update_in(cx, |editor, window, cx| {
11968 editor.perform_format(
11969 project.clone(),
11970 FormatTrigger::Manual,
11971 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11972 window,
11973 cx,
11974 )
11975 })
11976 .unwrap();
11977 fake_server
11978 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11979 assert_eq!(
11980 params.text_document.uri,
11981 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11982 );
11983 assert_eq!(params.options.tab_size, 4);
11984 Ok(Some(vec![lsp::TextEdit::new(
11985 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11986 ", ".to_string(),
11987 )]))
11988 })
11989 .next()
11990 .await;
11991 cx.executor().start_waiting();
11992 format.await;
11993 assert_eq!(
11994 editor.update(cx, |editor, cx| editor.text(cx)),
11995 "one, two\nthree\n"
11996 );
11997
11998 editor.update_in(cx, |editor, window, cx| {
11999 editor.set_text("one\ntwo\nthree\n", window, cx)
12000 });
12001 // Ensure we don't lock if formatting hangs.
12002 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12003 move |params, _| async move {
12004 assert_eq!(
12005 params.text_document.uri,
12006 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12007 );
12008 futures::future::pending::<()>().await;
12009 unreachable!()
12010 },
12011 );
12012 let format = editor
12013 .update_in(cx, |editor, window, cx| {
12014 editor.perform_format(
12015 project,
12016 FormatTrigger::Manual,
12017 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12018 window,
12019 cx,
12020 )
12021 })
12022 .unwrap();
12023 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12024 cx.executor().start_waiting();
12025 format.await;
12026 assert_eq!(
12027 editor.update(cx, |editor, cx| editor.text(cx)),
12028 "one\ntwo\nthree\n"
12029 );
12030}
12031
12032#[gpui::test]
12033async fn test_multiple_formatters(cx: &mut TestAppContext) {
12034 init_test(cx, |settings| {
12035 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12036 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12037 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12038 Formatter::CodeAction("code-action-1".into()),
12039 Formatter::CodeAction("code-action-2".into()),
12040 ]))
12041 });
12042
12043 let fs = FakeFs::new(cx.executor());
12044 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12045 .await;
12046
12047 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12048 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12049 language_registry.add(rust_lang());
12050
12051 let mut fake_servers = language_registry.register_fake_lsp(
12052 "Rust",
12053 FakeLspAdapter {
12054 capabilities: lsp::ServerCapabilities {
12055 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12056 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12057 commands: vec!["the-command-for-code-action-1".into()],
12058 ..Default::default()
12059 }),
12060 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12061 ..Default::default()
12062 },
12063 ..Default::default()
12064 },
12065 );
12066
12067 let buffer = project
12068 .update(cx, |project, cx| {
12069 project.open_local_buffer(path!("/file.rs"), cx)
12070 })
12071 .await
12072 .unwrap();
12073
12074 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12075 let (editor, cx) = cx.add_window_view(|window, cx| {
12076 build_editor_with_project(project.clone(), buffer, window, cx)
12077 });
12078
12079 cx.executor().start_waiting();
12080
12081 let fake_server = fake_servers.next().await.unwrap();
12082 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12083 move |_params, _| async move {
12084 Ok(Some(vec![lsp::TextEdit::new(
12085 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12086 "applied-formatting\n".to_string(),
12087 )]))
12088 },
12089 );
12090 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12091 move |params, _| async move {
12092 let requested_code_actions = params.context.only.expect("Expected code action request");
12093 assert_eq!(requested_code_actions.len(), 1);
12094
12095 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12096 let code_action = match requested_code_actions[0].as_str() {
12097 "code-action-1" => lsp::CodeAction {
12098 kind: Some("code-action-1".into()),
12099 edit: Some(lsp::WorkspaceEdit::new(
12100 [(
12101 uri,
12102 vec![lsp::TextEdit::new(
12103 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12104 "applied-code-action-1-edit\n".to_string(),
12105 )],
12106 )]
12107 .into_iter()
12108 .collect(),
12109 )),
12110 command: Some(lsp::Command {
12111 command: "the-command-for-code-action-1".into(),
12112 ..Default::default()
12113 }),
12114 ..Default::default()
12115 },
12116 "code-action-2" => lsp::CodeAction {
12117 kind: Some("code-action-2".into()),
12118 edit: Some(lsp::WorkspaceEdit::new(
12119 [(
12120 uri,
12121 vec![lsp::TextEdit::new(
12122 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12123 "applied-code-action-2-edit\n".to_string(),
12124 )],
12125 )]
12126 .into_iter()
12127 .collect(),
12128 )),
12129 ..Default::default()
12130 },
12131 req => panic!("Unexpected code action request: {:?}", req),
12132 };
12133 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12134 code_action,
12135 )]))
12136 },
12137 );
12138
12139 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12140 move |params, _| async move { Ok(params) }
12141 });
12142
12143 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12144 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12145 let fake = fake_server.clone();
12146 let lock = command_lock.clone();
12147 move |params, _| {
12148 assert_eq!(params.command, "the-command-for-code-action-1");
12149 let fake = fake.clone();
12150 let lock = lock.clone();
12151 async move {
12152 lock.lock().await;
12153 fake.server
12154 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12155 label: None,
12156 edit: lsp::WorkspaceEdit {
12157 changes: Some(
12158 [(
12159 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12160 vec![lsp::TextEdit {
12161 range: lsp::Range::new(
12162 lsp::Position::new(0, 0),
12163 lsp::Position::new(0, 0),
12164 ),
12165 new_text: "applied-code-action-1-command\n".into(),
12166 }],
12167 )]
12168 .into_iter()
12169 .collect(),
12170 ),
12171 ..Default::default()
12172 },
12173 })
12174 .await
12175 .into_response()
12176 .unwrap();
12177 Ok(Some(json!(null)))
12178 }
12179 }
12180 });
12181
12182 cx.executor().start_waiting();
12183 editor
12184 .update_in(cx, |editor, window, cx| {
12185 editor.perform_format(
12186 project.clone(),
12187 FormatTrigger::Manual,
12188 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12189 window,
12190 cx,
12191 )
12192 })
12193 .unwrap()
12194 .await;
12195 editor.update(cx, |editor, cx| {
12196 assert_eq!(
12197 editor.text(cx),
12198 r#"
12199 applied-code-action-2-edit
12200 applied-code-action-1-command
12201 applied-code-action-1-edit
12202 applied-formatting
12203 one
12204 two
12205 three
12206 "#
12207 .unindent()
12208 );
12209 });
12210
12211 editor.update_in(cx, |editor, window, cx| {
12212 editor.undo(&Default::default(), window, cx);
12213 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12214 });
12215
12216 // Perform a manual edit while waiting for an LSP command
12217 // that's being run as part of a formatting code action.
12218 let lock_guard = command_lock.lock().await;
12219 let format = editor
12220 .update_in(cx, |editor, window, cx| {
12221 editor.perform_format(
12222 project.clone(),
12223 FormatTrigger::Manual,
12224 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12225 window,
12226 cx,
12227 )
12228 })
12229 .unwrap();
12230 cx.run_until_parked();
12231 editor.update(cx, |editor, cx| {
12232 assert_eq!(
12233 editor.text(cx),
12234 r#"
12235 applied-code-action-1-edit
12236 applied-formatting
12237 one
12238 two
12239 three
12240 "#
12241 .unindent()
12242 );
12243
12244 editor.buffer.update(cx, |buffer, cx| {
12245 let ix = buffer.len(cx);
12246 buffer.edit([(ix..ix, "edited\n")], None, cx);
12247 });
12248 });
12249
12250 // Allow the LSP command to proceed. Because the buffer was edited,
12251 // the second code action will not be run.
12252 drop(lock_guard);
12253 format.await;
12254 editor.update_in(cx, |editor, window, cx| {
12255 assert_eq!(
12256 editor.text(cx),
12257 r#"
12258 applied-code-action-1-command
12259 applied-code-action-1-edit
12260 applied-formatting
12261 one
12262 two
12263 three
12264 edited
12265 "#
12266 .unindent()
12267 );
12268
12269 // The manual edit is undone first, because it is the last thing the user did
12270 // (even though the command completed afterwards).
12271 editor.undo(&Default::default(), window, cx);
12272 assert_eq!(
12273 editor.text(cx),
12274 r#"
12275 applied-code-action-1-command
12276 applied-code-action-1-edit
12277 applied-formatting
12278 one
12279 two
12280 three
12281 "#
12282 .unindent()
12283 );
12284
12285 // All the formatting (including the command, which completed after the manual edit)
12286 // is undone together.
12287 editor.undo(&Default::default(), window, cx);
12288 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12289 });
12290}
12291
12292#[gpui::test]
12293async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12294 init_test(cx, |settings| {
12295 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12296 settings::LanguageServerFormatterSpecifier::Current,
12297 )]))
12298 });
12299
12300 let fs = FakeFs::new(cx.executor());
12301 fs.insert_file(path!("/file.ts"), Default::default()).await;
12302
12303 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12304
12305 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12306 language_registry.add(Arc::new(Language::new(
12307 LanguageConfig {
12308 name: "TypeScript".into(),
12309 matcher: LanguageMatcher {
12310 path_suffixes: vec!["ts".to_string()],
12311 ..Default::default()
12312 },
12313 ..LanguageConfig::default()
12314 },
12315 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12316 )));
12317 update_test_language_settings(cx, |settings| {
12318 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12319 });
12320 let mut fake_servers = language_registry.register_fake_lsp(
12321 "TypeScript",
12322 FakeLspAdapter {
12323 capabilities: lsp::ServerCapabilities {
12324 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12325 ..Default::default()
12326 },
12327 ..Default::default()
12328 },
12329 );
12330
12331 let buffer = project
12332 .update(cx, |project, cx| {
12333 project.open_local_buffer(path!("/file.ts"), cx)
12334 })
12335 .await
12336 .unwrap();
12337
12338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12339 let (editor, cx) = cx.add_window_view(|window, cx| {
12340 build_editor_with_project(project.clone(), buffer, window, cx)
12341 });
12342 editor.update_in(cx, |editor, window, cx| {
12343 editor.set_text(
12344 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12345 window,
12346 cx,
12347 )
12348 });
12349
12350 cx.executor().start_waiting();
12351 let fake_server = fake_servers.next().await.unwrap();
12352
12353 let format = editor
12354 .update_in(cx, |editor, window, cx| {
12355 editor.perform_code_action_kind(
12356 project.clone(),
12357 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12358 window,
12359 cx,
12360 )
12361 })
12362 .unwrap();
12363 fake_server
12364 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12365 assert_eq!(
12366 params.text_document.uri,
12367 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12368 );
12369 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12370 lsp::CodeAction {
12371 title: "Organize Imports".to_string(),
12372 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12373 edit: Some(lsp::WorkspaceEdit {
12374 changes: Some(
12375 [(
12376 params.text_document.uri.clone(),
12377 vec![lsp::TextEdit::new(
12378 lsp::Range::new(
12379 lsp::Position::new(1, 0),
12380 lsp::Position::new(2, 0),
12381 ),
12382 "".to_string(),
12383 )],
12384 )]
12385 .into_iter()
12386 .collect(),
12387 ),
12388 ..Default::default()
12389 }),
12390 ..Default::default()
12391 },
12392 )]))
12393 })
12394 .next()
12395 .await;
12396 cx.executor().start_waiting();
12397 format.await;
12398 assert_eq!(
12399 editor.update(cx, |editor, cx| editor.text(cx)),
12400 "import { a } from 'module';\n\nconst x = a;\n"
12401 );
12402
12403 editor.update_in(cx, |editor, window, cx| {
12404 editor.set_text(
12405 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12406 window,
12407 cx,
12408 )
12409 });
12410 // Ensure we don't lock if code action hangs.
12411 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12412 move |params, _| async move {
12413 assert_eq!(
12414 params.text_document.uri,
12415 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12416 );
12417 futures::future::pending::<()>().await;
12418 unreachable!()
12419 },
12420 );
12421 let format = editor
12422 .update_in(cx, |editor, window, cx| {
12423 editor.perform_code_action_kind(
12424 project,
12425 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12426 window,
12427 cx,
12428 )
12429 })
12430 .unwrap();
12431 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12432 cx.executor().start_waiting();
12433 format.await;
12434 assert_eq!(
12435 editor.update(cx, |editor, cx| editor.text(cx)),
12436 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12437 );
12438}
12439
12440#[gpui::test]
12441async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12442 init_test(cx, |_| {});
12443
12444 let mut cx = EditorLspTestContext::new_rust(
12445 lsp::ServerCapabilities {
12446 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12447 ..Default::default()
12448 },
12449 cx,
12450 )
12451 .await;
12452
12453 cx.set_state(indoc! {"
12454 one.twoˇ
12455 "});
12456
12457 // The format request takes a long time. When it completes, it inserts
12458 // a newline and an indent before the `.`
12459 cx.lsp
12460 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12461 let executor = cx.background_executor().clone();
12462 async move {
12463 executor.timer(Duration::from_millis(100)).await;
12464 Ok(Some(vec![lsp::TextEdit {
12465 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12466 new_text: "\n ".into(),
12467 }]))
12468 }
12469 });
12470
12471 // Submit a format request.
12472 let format_1 = cx
12473 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12474 .unwrap();
12475 cx.executor().run_until_parked();
12476
12477 // Submit a second format request.
12478 let format_2 = cx
12479 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12480 .unwrap();
12481 cx.executor().run_until_parked();
12482
12483 // Wait for both format requests to complete
12484 cx.executor().advance_clock(Duration::from_millis(200));
12485 cx.executor().start_waiting();
12486 format_1.await.unwrap();
12487 cx.executor().start_waiting();
12488 format_2.await.unwrap();
12489
12490 // The formatting edits only happens once.
12491 cx.assert_editor_state(indoc! {"
12492 one
12493 .twoˇ
12494 "});
12495}
12496
12497#[gpui::test]
12498async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12499 init_test(cx, |settings| {
12500 settings.defaults.formatter = Some(FormatterList::default())
12501 });
12502
12503 let mut cx = EditorLspTestContext::new_rust(
12504 lsp::ServerCapabilities {
12505 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12506 ..Default::default()
12507 },
12508 cx,
12509 )
12510 .await;
12511
12512 // Set up a buffer white some trailing whitespace and no trailing newline.
12513 cx.set_state(
12514 &[
12515 "one ", //
12516 "twoˇ", //
12517 "three ", //
12518 "four", //
12519 ]
12520 .join("\n"),
12521 );
12522
12523 // Record which buffer changes have been sent to the language server
12524 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12525 cx.lsp
12526 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12527 let buffer_changes = buffer_changes.clone();
12528 move |params, _| {
12529 buffer_changes.lock().extend(
12530 params
12531 .content_changes
12532 .into_iter()
12533 .map(|e| (e.range.unwrap(), e.text)),
12534 );
12535 }
12536 });
12537
12538 // Handle formatting requests to the language server.
12539 cx.lsp
12540 .set_request_handler::<lsp::request::Formatting, _, _>({
12541 let buffer_changes = buffer_changes.clone();
12542 move |_, _| {
12543 let buffer_changes = buffer_changes.clone();
12544 // Insert blank lines between each line of the buffer.
12545 async move {
12546 // When formatting is requested, trailing whitespace has already been stripped,
12547 // and the trailing newline has already been added.
12548 assert_eq!(
12549 &buffer_changes.lock()[1..],
12550 &[
12551 (
12552 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12553 "".into()
12554 ),
12555 (
12556 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12557 "".into()
12558 ),
12559 (
12560 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12561 "\n".into()
12562 ),
12563 ]
12564 );
12565
12566 Ok(Some(vec![
12567 lsp::TextEdit {
12568 range: lsp::Range::new(
12569 lsp::Position::new(1, 0),
12570 lsp::Position::new(1, 0),
12571 ),
12572 new_text: "\n".into(),
12573 },
12574 lsp::TextEdit {
12575 range: lsp::Range::new(
12576 lsp::Position::new(2, 0),
12577 lsp::Position::new(2, 0),
12578 ),
12579 new_text: "\n".into(),
12580 },
12581 ]))
12582 }
12583 }
12584 });
12585
12586 // Submit a format request.
12587 let format = cx
12588 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12589 .unwrap();
12590
12591 cx.run_until_parked();
12592 // After formatting the buffer, the trailing whitespace is stripped,
12593 // a newline is appended, and the edits provided by the language server
12594 // have been applied.
12595 format.await.unwrap();
12596
12597 cx.assert_editor_state(
12598 &[
12599 "one", //
12600 "", //
12601 "twoˇ", //
12602 "", //
12603 "three", //
12604 "four", //
12605 "", //
12606 ]
12607 .join("\n"),
12608 );
12609
12610 // Undoing the formatting undoes the trailing whitespace removal, the
12611 // trailing newline, and the LSP edits.
12612 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12613 cx.assert_editor_state(
12614 &[
12615 "one ", //
12616 "twoˇ", //
12617 "three ", //
12618 "four", //
12619 ]
12620 .join("\n"),
12621 );
12622}
12623
12624#[gpui::test]
12625async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12626 cx: &mut TestAppContext,
12627) {
12628 init_test(cx, |_| {});
12629
12630 cx.update(|cx| {
12631 cx.update_global::<SettingsStore, _>(|settings, cx| {
12632 settings.update_user_settings(cx, |settings| {
12633 settings.editor.auto_signature_help = Some(true);
12634 });
12635 });
12636 });
12637
12638 let mut cx = EditorLspTestContext::new_rust(
12639 lsp::ServerCapabilities {
12640 signature_help_provider: Some(lsp::SignatureHelpOptions {
12641 ..Default::default()
12642 }),
12643 ..Default::default()
12644 },
12645 cx,
12646 )
12647 .await;
12648
12649 let language = Language::new(
12650 LanguageConfig {
12651 name: "Rust".into(),
12652 brackets: BracketPairConfig {
12653 pairs: vec![
12654 BracketPair {
12655 start: "{".to_string(),
12656 end: "}".to_string(),
12657 close: true,
12658 surround: true,
12659 newline: true,
12660 },
12661 BracketPair {
12662 start: "(".to_string(),
12663 end: ")".to_string(),
12664 close: true,
12665 surround: true,
12666 newline: true,
12667 },
12668 BracketPair {
12669 start: "/*".to_string(),
12670 end: " */".to_string(),
12671 close: true,
12672 surround: true,
12673 newline: true,
12674 },
12675 BracketPair {
12676 start: "[".to_string(),
12677 end: "]".to_string(),
12678 close: false,
12679 surround: false,
12680 newline: true,
12681 },
12682 BracketPair {
12683 start: "\"".to_string(),
12684 end: "\"".to_string(),
12685 close: true,
12686 surround: true,
12687 newline: false,
12688 },
12689 BracketPair {
12690 start: "<".to_string(),
12691 end: ">".to_string(),
12692 close: false,
12693 surround: true,
12694 newline: true,
12695 },
12696 ],
12697 ..Default::default()
12698 },
12699 autoclose_before: "})]".to_string(),
12700 ..Default::default()
12701 },
12702 Some(tree_sitter_rust::LANGUAGE.into()),
12703 );
12704 let language = Arc::new(language);
12705
12706 cx.language_registry().add(language.clone());
12707 cx.update_buffer(|buffer, cx| {
12708 buffer.set_language(Some(language), cx);
12709 });
12710
12711 cx.set_state(
12712 &r#"
12713 fn main() {
12714 sampleˇ
12715 }
12716 "#
12717 .unindent(),
12718 );
12719
12720 cx.update_editor(|editor, window, cx| {
12721 editor.handle_input("(", window, cx);
12722 });
12723 cx.assert_editor_state(
12724 &"
12725 fn main() {
12726 sample(ˇ)
12727 }
12728 "
12729 .unindent(),
12730 );
12731
12732 let mocked_response = lsp::SignatureHelp {
12733 signatures: vec![lsp::SignatureInformation {
12734 label: "fn sample(param1: u8, param2: u8)".to_string(),
12735 documentation: None,
12736 parameters: Some(vec![
12737 lsp::ParameterInformation {
12738 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12739 documentation: None,
12740 },
12741 lsp::ParameterInformation {
12742 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12743 documentation: None,
12744 },
12745 ]),
12746 active_parameter: None,
12747 }],
12748 active_signature: Some(0),
12749 active_parameter: Some(0),
12750 };
12751 handle_signature_help_request(&mut cx, mocked_response).await;
12752
12753 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12754 .await;
12755
12756 cx.editor(|editor, _, _| {
12757 let signature_help_state = editor.signature_help_state.popover().cloned();
12758 let signature = signature_help_state.unwrap();
12759 assert_eq!(
12760 signature.signatures[signature.current_signature].label,
12761 "fn sample(param1: u8, param2: u8)"
12762 );
12763 });
12764}
12765
12766#[gpui::test]
12767async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12768 init_test(cx, |_| {});
12769
12770 cx.update(|cx| {
12771 cx.update_global::<SettingsStore, _>(|settings, cx| {
12772 settings.update_user_settings(cx, |settings| {
12773 settings.editor.auto_signature_help = Some(false);
12774 settings.editor.show_signature_help_after_edits = Some(false);
12775 });
12776 });
12777 });
12778
12779 let mut cx = EditorLspTestContext::new_rust(
12780 lsp::ServerCapabilities {
12781 signature_help_provider: Some(lsp::SignatureHelpOptions {
12782 ..Default::default()
12783 }),
12784 ..Default::default()
12785 },
12786 cx,
12787 )
12788 .await;
12789
12790 let language = Language::new(
12791 LanguageConfig {
12792 name: "Rust".into(),
12793 brackets: BracketPairConfig {
12794 pairs: vec![
12795 BracketPair {
12796 start: "{".to_string(),
12797 end: "}".to_string(),
12798 close: true,
12799 surround: true,
12800 newline: true,
12801 },
12802 BracketPair {
12803 start: "(".to_string(),
12804 end: ")".to_string(),
12805 close: true,
12806 surround: true,
12807 newline: true,
12808 },
12809 BracketPair {
12810 start: "/*".to_string(),
12811 end: " */".to_string(),
12812 close: true,
12813 surround: true,
12814 newline: true,
12815 },
12816 BracketPair {
12817 start: "[".to_string(),
12818 end: "]".to_string(),
12819 close: false,
12820 surround: false,
12821 newline: true,
12822 },
12823 BracketPair {
12824 start: "\"".to_string(),
12825 end: "\"".to_string(),
12826 close: true,
12827 surround: true,
12828 newline: false,
12829 },
12830 BracketPair {
12831 start: "<".to_string(),
12832 end: ">".to_string(),
12833 close: false,
12834 surround: true,
12835 newline: true,
12836 },
12837 ],
12838 ..Default::default()
12839 },
12840 autoclose_before: "})]".to_string(),
12841 ..Default::default()
12842 },
12843 Some(tree_sitter_rust::LANGUAGE.into()),
12844 );
12845 let language = Arc::new(language);
12846
12847 cx.language_registry().add(language.clone());
12848 cx.update_buffer(|buffer, cx| {
12849 buffer.set_language(Some(language), cx);
12850 });
12851
12852 // Ensure that signature_help is not called when no signature help is enabled.
12853 cx.set_state(
12854 &r#"
12855 fn main() {
12856 sampleˇ
12857 }
12858 "#
12859 .unindent(),
12860 );
12861 cx.update_editor(|editor, window, cx| {
12862 editor.handle_input("(", window, cx);
12863 });
12864 cx.assert_editor_state(
12865 &"
12866 fn main() {
12867 sample(ˇ)
12868 }
12869 "
12870 .unindent(),
12871 );
12872 cx.editor(|editor, _, _| {
12873 assert!(editor.signature_help_state.task().is_none());
12874 });
12875
12876 let mocked_response = lsp::SignatureHelp {
12877 signatures: vec![lsp::SignatureInformation {
12878 label: "fn sample(param1: u8, param2: u8)".to_string(),
12879 documentation: None,
12880 parameters: Some(vec![
12881 lsp::ParameterInformation {
12882 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12883 documentation: None,
12884 },
12885 lsp::ParameterInformation {
12886 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12887 documentation: None,
12888 },
12889 ]),
12890 active_parameter: None,
12891 }],
12892 active_signature: Some(0),
12893 active_parameter: Some(0),
12894 };
12895
12896 // Ensure that signature_help is called when enabled afte edits
12897 cx.update(|_, cx| {
12898 cx.update_global::<SettingsStore, _>(|settings, cx| {
12899 settings.update_user_settings(cx, |settings| {
12900 settings.editor.auto_signature_help = Some(false);
12901 settings.editor.show_signature_help_after_edits = Some(true);
12902 });
12903 });
12904 });
12905 cx.set_state(
12906 &r#"
12907 fn main() {
12908 sampleˇ
12909 }
12910 "#
12911 .unindent(),
12912 );
12913 cx.update_editor(|editor, window, cx| {
12914 editor.handle_input("(", window, cx);
12915 });
12916 cx.assert_editor_state(
12917 &"
12918 fn main() {
12919 sample(ˇ)
12920 }
12921 "
12922 .unindent(),
12923 );
12924 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12925 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12926 .await;
12927 cx.update_editor(|editor, _, _| {
12928 let signature_help_state = editor.signature_help_state.popover().cloned();
12929 assert!(signature_help_state.is_some());
12930 let signature = signature_help_state.unwrap();
12931 assert_eq!(
12932 signature.signatures[signature.current_signature].label,
12933 "fn sample(param1: u8, param2: u8)"
12934 );
12935 editor.signature_help_state = SignatureHelpState::default();
12936 });
12937
12938 // Ensure that signature_help is called when auto signature help override is enabled
12939 cx.update(|_, cx| {
12940 cx.update_global::<SettingsStore, _>(|settings, cx| {
12941 settings.update_user_settings(cx, |settings| {
12942 settings.editor.auto_signature_help = Some(true);
12943 settings.editor.show_signature_help_after_edits = Some(false);
12944 });
12945 });
12946 });
12947 cx.set_state(
12948 &r#"
12949 fn main() {
12950 sampleˇ
12951 }
12952 "#
12953 .unindent(),
12954 );
12955 cx.update_editor(|editor, window, cx| {
12956 editor.handle_input("(", window, cx);
12957 });
12958 cx.assert_editor_state(
12959 &"
12960 fn main() {
12961 sample(ˇ)
12962 }
12963 "
12964 .unindent(),
12965 );
12966 handle_signature_help_request(&mut cx, mocked_response).await;
12967 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12968 .await;
12969 cx.editor(|editor, _, _| {
12970 let signature_help_state = editor.signature_help_state.popover().cloned();
12971 assert!(signature_help_state.is_some());
12972 let signature = signature_help_state.unwrap();
12973 assert_eq!(
12974 signature.signatures[signature.current_signature].label,
12975 "fn sample(param1: u8, param2: u8)"
12976 );
12977 });
12978}
12979
12980#[gpui::test]
12981async fn test_signature_help(cx: &mut TestAppContext) {
12982 init_test(cx, |_| {});
12983 cx.update(|cx| {
12984 cx.update_global::<SettingsStore, _>(|settings, cx| {
12985 settings.update_user_settings(cx, |settings| {
12986 settings.editor.auto_signature_help = Some(true);
12987 });
12988 });
12989 });
12990
12991 let mut cx = EditorLspTestContext::new_rust(
12992 lsp::ServerCapabilities {
12993 signature_help_provider: Some(lsp::SignatureHelpOptions {
12994 ..Default::default()
12995 }),
12996 ..Default::default()
12997 },
12998 cx,
12999 )
13000 .await;
13001
13002 // A test that directly calls `show_signature_help`
13003 cx.update_editor(|editor, window, cx| {
13004 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13005 });
13006
13007 let mocked_response = lsp::SignatureHelp {
13008 signatures: vec![lsp::SignatureInformation {
13009 label: "fn sample(param1: u8, param2: u8)".to_string(),
13010 documentation: None,
13011 parameters: Some(vec![
13012 lsp::ParameterInformation {
13013 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13014 documentation: None,
13015 },
13016 lsp::ParameterInformation {
13017 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13018 documentation: None,
13019 },
13020 ]),
13021 active_parameter: None,
13022 }],
13023 active_signature: Some(0),
13024 active_parameter: Some(0),
13025 };
13026 handle_signature_help_request(&mut cx, mocked_response).await;
13027
13028 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13029 .await;
13030
13031 cx.editor(|editor, _, _| {
13032 let signature_help_state = editor.signature_help_state.popover().cloned();
13033 assert!(signature_help_state.is_some());
13034 let signature = signature_help_state.unwrap();
13035 assert_eq!(
13036 signature.signatures[signature.current_signature].label,
13037 "fn sample(param1: u8, param2: u8)"
13038 );
13039 });
13040
13041 // When exiting outside from inside the brackets, `signature_help` is closed.
13042 cx.set_state(indoc! {"
13043 fn main() {
13044 sample(ˇ);
13045 }
13046
13047 fn sample(param1: u8, param2: u8) {}
13048 "});
13049
13050 cx.update_editor(|editor, window, cx| {
13051 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13052 s.select_ranges([0..0])
13053 });
13054 });
13055
13056 let mocked_response = lsp::SignatureHelp {
13057 signatures: Vec::new(),
13058 active_signature: None,
13059 active_parameter: None,
13060 };
13061 handle_signature_help_request(&mut cx, mocked_response).await;
13062
13063 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13064 .await;
13065
13066 cx.editor(|editor, _, _| {
13067 assert!(!editor.signature_help_state.is_shown());
13068 });
13069
13070 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13071 cx.set_state(indoc! {"
13072 fn main() {
13073 sample(ˇ);
13074 }
13075
13076 fn sample(param1: u8, param2: u8) {}
13077 "});
13078
13079 let mocked_response = lsp::SignatureHelp {
13080 signatures: vec![lsp::SignatureInformation {
13081 label: "fn sample(param1: u8, param2: u8)".to_string(),
13082 documentation: None,
13083 parameters: Some(vec![
13084 lsp::ParameterInformation {
13085 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13086 documentation: None,
13087 },
13088 lsp::ParameterInformation {
13089 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13090 documentation: None,
13091 },
13092 ]),
13093 active_parameter: None,
13094 }],
13095 active_signature: Some(0),
13096 active_parameter: Some(0),
13097 };
13098 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13099 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13100 .await;
13101 cx.editor(|editor, _, _| {
13102 assert!(editor.signature_help_state.is_shown());
13103 });
13104
13105 // Restore the popover with more parameter input
13106 cx.set_state(indoc! {"
13107 fn main() {
13108 sample(param1, param2ˇ);
13109 }
13110
13111 fn sample(param1: u8, param2: u8) {}
13112 "});
13113
13114 let mocked_response = lsp::SignatureHelp {
13115 signatures: vec![lsp::SignatureInformation {
13116 label: "fn sample(param1: u8, param2: u8)".to_string(),
13117 documentation: None,
13118 parameters: Some(vec![
13119 lsp::ParameterInformation {
13120 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13121 documentation: None,
13122 },
13123 lsp::ParameterInformation {
13124 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13125 documentation: None,
13126 },
13127 ]),
13128 active_parameter: None,
13129 }],
13130 active_signature: Some(0),
13131 active_parameter: Some(1),
13132 };
13133 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13134 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13135 .await;
13136
13137 // When selecting a range, the popover is gone.
13138 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13139 cx.update_editor(|editor, window, cx| {
13140 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13141 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13142 })
13143 });
13144 cx.assert_editor_state(indoc! {"
13145 fn main() {
13146 sample(param1, «ˇparam2»);
13147 }
13148
13149 fn sample(param1: u8, param2: u8) {}
13150 "});
13151 cx.editor(|editor, _, _| {
13152 assert!(!editor.signature_help_state.is_shown());
13153 });
13154
13155 // When unselecting again, the popover is back if within the brackets.
13156 cx.update_editor(|editor, window, cx| {
13157 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13158 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13159 })
13160 });
13161 cx.assert_editor_state(indoc! {"
13162 fn main() {
13163 sample(param1, ˇparam2);
13164 }
13165
13166 fn sample(param1: u8, param2: u8) {}
13167 "});
13168 handle_signature_help_request(&mut cx, mocked_response).await;
13169 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13170 .await;
13171 cx.editor(|editor, _, _| {
13172 assert!(editor.signature_help_state.is_shown());
13173 });
13174
13175 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13176 cx.update_editor(|editor, window, cx| {
13177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13178 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13179 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13180 })
13181 });
13182 cx.assert_editor_state(indoc! {"
13183 fn main() {
13184 sample(param1, ˇparam2);
13185 }
13186
13187 fn sample(param1: u8, param2: u8) {}
13188 "});
13189
13190 let mocked_response = lsp::SignatureHelp {
13191 signatures: vec![lsp::SignatureInformation {
13192 label: "fn sample(param1: u8, param2: u8)".to_string(),
13193 documentation: None,
13194 parameters: Some(vec![
13195 lsp::ParameterInformation {
13196 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13197 documentation: None,
13198 },
13199 lsp::ParameterInformation {
13200 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13201 documentation: None,
13202 },
13203 ]),
13204 active_parameter: None,
13205 }],
13206 active_signature: Some(0),
13207 active_parameter: Some(1),
13208 };
13209 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13210 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13211 .await;
13212 cx.update_editor(|editor, _, cx| {
13213 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13214 });
13215 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13216 .await;
13217 cx.update_editor(|editor, window, cx| {
13218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13219 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13220 })
13221 });
13222 cx.assert_editor_state(indoc! {"
13223 fn main() {
13224 sample(param1, «ˇparam2»);
13225 }
13226
13227 fn sample(param1: u8, param2: u8) {}
13228 "});
13229 cx.update_editor(|editor, window, cx| {
13230 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13231 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13232 })
13233 });
13234 cx.assert_editor_state(indoc! {"
13235 fn main() {
13236 sample(param1, ˇparam2);
13237 }
13238
13239 fn sample(param1: u8, param2: u8) {}
13240 "});
13241 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13242 .await;
13243}
13244
13245#[gpui::test]
13246async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13247 init_test(cx, |_| {});
13248
13249 let mut cx = EditorLspTestContext::new_rust(
13250 lsp::ServerCapabilities {
13251 signature_help_provider: Some(lsp::SignatureHelpOptions {
13252 ..Default::default()
13253 }),
13254 ..Default::default()
13255 },
13256 cx,
13257 )
13258 .await;
13259
13260 cx.set_state(indoc! {"
13261 fn main() {
13262 overloadedˇ
13263 }
13264 "});
13265
13266 cx.update_editor(|editor, window, cx| {
13267 editor.handle_input("(", window, cx);
13268 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13269 });
13270
13271 // Mock response with 3 signatures
13272 let mocked_response = lsp::SignatureHelp {
13273 signatures: vec![
13274 lsp::SignatureInformation {
13275 label: "fn overloaded(x: i32)".to_string(),
13276 documentation: None,
13277 parameters: Some(vec![lsp::ParameterInformation {
13278 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13279 documentation: None,
13280 }]),
13281 active_parameter: None,
13282 },
13283 lsp::SignatureInformation {
13284 label: "fn overloaded(x: i32, y: i32)".to_string(),
13285 documentation: None,
13286 parameters: Some(vec![
13287 lsp::ParameterInformation {
13288 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13289 documentation: None,
13290 },
13291 lsp::ParameterInformation {
13292 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13293 documentation: None,
13294 },
13295 ]),
13296 active_parameter: None,
13297 },
13298 lsp::SignatureInformation {
13299 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13300 documentation: None,
13301 parameters: Some(vec![
13302 lsp::ParameterInformation {
13303 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13304 documentation: None,
13305 },
13306 lsp::ParameterInformation {
13307 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13308 documentation: None,
13309 },
13310 lsp::ParameterInformation {
13311 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13312 documentation: None,
13313 },
13314 ]),
13315 active_parameter: None,
13316 },
13317 ],
13318 active_signature: Some(1),
13319 active_parameter: Some(0),
13320 };
13321 handle_signature_help_request(&mut cx, mocked_response).await;
13322
13323 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13324 .await;
13325
13326 // Verify we have multiple signatures and the right one is selected
13327 cx.editor(|editor, _, _| {
13328 let popover = editor.signature_help_state.popover().cloned().unwrap();
13329 assert_eq!(popover.signatures.len(), 3);
13330 // active_signature was 1, so that should be the current
13331 assert_eq!(popover.current_signature, 1);
13332 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13333 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13334 assert_eq!(
13335 popover.signatures[2].label,
13336 "fn overloaded(x: i32, y: i32, z: i32)"
13337 );
13338 });
13339
13340 // Test navigation functionality
13341 cx.update_editor(|editor, window, cx| {
13342 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13343 });
13344
13345 cx.editor(|editor, _, _| {
13346 let popover = editor.signature_help_state.popover().cloned().unwrap();
13347 assert_eq!(popover.current_signature, 2);
13348 });
13349
13350 // Test wrap around
13351 cx.update_editor(|editor, window, cx| {
13352 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13353 });
13354
13355 cx.editor(|editor, _, _| {
13356 let popover = editor.signature_help_state.popover().cloned().unwrap();
13357 assert_eq!(popover.current_signature, 0);
13358 });
13359
13360 // Test previous navigation
13361 cx.update_editor(|editor, window, cx| {
13362 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13363 });
13364
13365 cx.editor(|editor, _, _| {
13366 let popover = editor.signature_help_state.popover().cloned().unwrap();
13367 assert_eq!(popover.current_signature, 2);
13368 });
13369}
13370
13371#[gpui::test]
13372async fn test_completion_mode(cx: &mut TestAppContext) {
13373 init_test(cx, |_| {});
13374 let mut cx = EditorLspTestContext::new_rust(
13375 lsp::ServerCapabilities {
13376 completion_provider: Some(lsp::CompletionOptions {
13377 resolve_provider: Some(true),
13378 ..Default::default()
13379 }),
13380 ..Default::default()
13381 },
13382 cx,
13383 )
13384 .await;
13385
13386 struct Run {
13387 run_description: &'static str,
13388 initial_state: String,
13389 buffer_marked_text: String,
13390 completion_label: &'static str,
13391 completion_text: &'static str,
13392 expected_with_insert_mode: String,
13393 expected_with_replace_mode: String,
13394 expected_with_replace_subsequence_mode: String,
13395 expected_with_replace_suffix_mode: String,
13396 }
13397
13398 let runs = [
13399 Run {
13400 run_description: "Start of word matches completion text",
13401 initial_state: "before ediˇ after".into(),
13402 buffer_marked_text: "before <edi|> after".into(),
13403 completion_label: "editor",
13404 completion_text: "editor",
13405 expected_with_insert_mode: "before editorˇ after".into(),
13406 expected_with_replace_mode: "before editorˇ after".into(),
13407 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13408 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13409 },
13410 Run {
13411 run_description: "Accept same text at the middle of the word",
13412 initial_state: "before ediˇtor after".into(),
13413 buffer_marked_text: "before <edi|tor> after".into(),
13414 completion_label: "editor",
13415 completion_text: "editor",
13416 expected_with_insert_mode: "before editorˇtor after".into(),
13417 expected_with_replace_mode: "before editorˇ after".into(),
13418 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13419 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13420 },
13421 Run {
13422 run_description: "End of word matches completion text -- cursor at end",
13423 initial_state: "before torˇ after".into(),
13424 buffer_marked_text: "before <tor|> after".into(),
13425 completion_label: "editor",
13426 completion_text: "editor",
13427 expected_with_insert_mode: "before editorˇ after".into(),
13428 expected_with_replace_mode: "before editorˇ after".into(),
13429 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13430 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13431 },
13432 Run {
13433 run_description: "End of word matches completion text -- cursor at start",
13434 initial_state: "before ˇtor after".into(),
13435 buffer_marked_text: "before <|tor> after".into(),
13436 completion_label: "editor",
13437 completion_text: "editor",
13438 expected_with_insert_mode: "before editorˇtor after".into(),
13439 expected_with_replace_mode: "before editorˇ after".into(),
13440 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13441 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13442 },
13443 Run {
13444 run_description: "Prepend text containing whitespace",
13445 initial_state: "pˇfield: bool".into(),
13446 buffer_marked_text: "<p|field>: bool".into(),
13447 completion_label: "pub ",
13448 completion_text: "pub ",
13449 expected_with_insert_mode: "pub ˇfield: bool".into(),
13450 expected_with_replace_mode: "pub ˇ: bool".into(),
13451 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13452 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13453 },
13454 Run {
13455 run_description: "Add element to start of list",
13456 initial_state: "[element_ˇelement_2]".into(),
13457 buffer_marked_text: "[<element_|element_2>]".into(),
13458 completion_label: "element_1",
13459 completion_text: "element_1",
13460 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13461 expected_with_replace_mode: "[element_1ˇ]".into(),
13462 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13463 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13464 },
13465 Run {
13466 run_description: "Add element to start of list -- first and second elements are equal",
13467 initial_state: "[elˇelement]".into(),
13468 buffer_marked_text: "[<el|element>]".into(),
13469 completion_label: "element",
13470 completion_text: "element",
13471 expected_with_insert_mode: "[elementˇelement]".into(),
13472 expected_with_replace_mode: "[elementˇ]".into(),
13473 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13474 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13475 },
13476 Run {
13477 run_description: "Ends with matching suffix",
13478 initial_state: "SubˇError".into(),
13479 buffer_marked_text: "<Sub|Error>".into(),
13480 completion_label: "SubscriptionError",
13481 completion_text: "SubscriptionError",
13482 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13483 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13484 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13485 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13486 },
13487 Run {
13488 run_description: "Suffix is a subsequence -- contiguous",
13489 initial_state: "SubˇErr".into(),
13490 buffer_marked_text: "<Sub|Err>".into(),
13491 completion_label: "SubscriptionError",
13492 completion_text: "SubscriptionError",
13493 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13494 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13495 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13496 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13497 },
13498 Run {
13499 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13500 initial_state: "Suˇscrirr".into(),
13501 buffer_marked_text: "<Su|scrirr>".into(),
13502 completion_label: "SubscriptionError",
13503 completion_text: "SubscriptionError",
13504 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13505 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13506 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13507 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13508 },
13509 Run {
13510 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13511 initial_state: "foo(indˇix)".into(),
13512 buffer_marked_text: "foo(<ind|ix>)".into(),
13513 completion_label: "node_index",
13514 completion_text: "node_index",
13515 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13516 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13517 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13518 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13519 },
13520 Run {
13521 run_description: "Replace range ends before cursor - should extend to cursor",
13522 initial_state: "before editˇo after".into(),
13523 buffer_marked_text: "before <{ed}>it|o after".into(),
13524 completion_label: "editor",
13525 completion_text: "editor",
13526 expected_with_insert_mode: "before editorˇo after".into(),
13527 expected_with_replace_mode: "before editorˇo after".into(),
13528 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13529 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13530 },
13531 Run {
13532 run_description: "Uses label for suffix matching",
13533 initial_state: "before ediˇtor after".into(),
13534 buffer_marked_text: "before <edi|tor> after".into(),
13535 completion_label: "editor",
13536 completion_text: "editor()",
13537 expected_with_insert_mode: "before editor()ˇtor after".into(),
13538 expected_with_replace_mode: "before editor()ˇ after".into(),
13539 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13540 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13541 },
13542 Run {
13543 run_description: "Case insensitive subsequence and suffix matching",
13544 initial_state: "before EDiˇtoR after".into(),
13545 buffer_marked_text: "before <EDi|toR> after".into(),
13546 completion_label: "editor",
13547 completion_text: "editor",
13548 expected_with_insert_mode: "before editorˇtoR after".into(),
13549 expected_with_replace_mode: "before editorˇ after".into(),
13550 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13551 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13552 },
13553 ];
13554
13555 for run in runs {
13556 let run_variations = [
13557 (LspInsertMode::Insert, run.expected_with_insert_mode),
13558 (LspInsertMode::Replace, run.expected_with_replace_mode),
13559 (
13560 LspInsertMode::ReplaceSubsequence,
13561 run.expected_with_replace_subsequence_mode,
13562 ),
13563 (
13564 LspInsertMode::ReplaceSuffix,
13565 run.expected_with_replace_suffix_mode,
13566 ),
13567 ];
13568
13569 for (lsp_insert_mode, expected_text) in run_variations {
13570 eprintln!(
13571 "run = {:?}, mode = {lsp_insert_mode:.?}",
13572 run.run_description,
13573 );
13574
13575 update_test_language_settings(&mut cx, |settings| {
13576 settings.defaults.completions = Some(CompletionSettingsContent {
13577 lsp_insert_mode: Some(lsp_insert_mode),
13578 words: Some(WordsCompletionMode::Disabled),
13579 words_min_length: Some(0),
13580 ..Default::default()
13581 });
13582 });
13583
13584 cx.set_state(&run.initial_state);
13585 cx.update_editor(|editor, window, cx| {
13586 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13587 });
13588
13589 let counter = Arc::new(AtomicUsize::new(0));
13590 handle_completion_request_with_insert_and_replace(
13591 &mut cx,
13592 &run.buffer_marked_text,
13593 vec![(run.completion_label, run.completion_text)],
13594 counter.clone(),
13595 )
13596 .await;
13597 cx.condition(|editor, _| editor.context_menu_visible())
13598 .await;
13599 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13600
13601 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13602 editor
13603 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13604 .unwrap()
13605 });
13606 cx.assert_editor_state(&expected_text);
13607 handle_resolve_completion_request(&mut cx, None).await;
13608 apply_additional_edits.await.unwrap();
13609 }
13610 }
13611}
13612
13613#[gpui::test]
13614async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13615 init_test(cx, |_| {});
13616 let mut cx = EditorLspTestContext::new_rust(
13617 lsp::ServerCapabilities {
13618 completion_provider: Some(lsp::CompletionOptions {
13619 resolve_provider: Some(true),
13620 ..Default::default()
13621 }),
13622 ..Default::default()
13623 },
13624 cx,
13625 )
13626 .await;
13627
13628 let initial_state = "SubˇError";
13629 let buffer_marked_text = "<Sub|Error>";
13630 let completion_text = "SubscriptionError";
13631 let expected_with_insert_mode = "SubscriptionErrorˇError";
13632 let expected_with_replace_mode = "SubscriptionErrorˇ";
13633
13634 update_test_language_settings(&mut cx, |settings| {
13635 settings.defaults.completions = Some(CompletionSettingsContent {
13636 words: Some(WordsCompletionMode::Disabled),
13637 words_min_length: Some(0),
13638 // set the opposite here to ensure that the action is overriding the default behavior
13639 lsp_insert_mode: Some(LspInsertMode::Insert),
13640 ..Default::default()
13641 });
13642 });
13643
13644 cx.set_state(initial_state);
13645 cx.update_editor(|editor, window, cx| {
13646 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13647 });
13648
13649 let counter = Arc::new(AtomicUsize::new(0));
13650 handle_completion_request_with_insert_and_replace(
13651 &mut cx,
13652 buffer_marked_text,
13653 vec![(completion_text, completion_text)],
13654 counter.clone(),
13655 )
13656 .await;
13657 cx.condition(|editor, _| editor.context_menu_visible())
13658 .await;
13659 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13660
13661 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13662 editor
13663 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13664 .unwrap()
13665 });
13666 cx.assert_editor_state(expected_with_replace_mode);
13667 handle_resolve_completion_request(&mut cx, None).await;
13668 apply_additional_edits.await.unwrap();
13669
13670 update_test_language_settings(&mut cx, |settings| {
13671 settings.defaults.completions = Some(CompletionSettingsContent {
13672 words: Some(WordsCompletionMode::Disabled),
13673 words_min_length: Some(0),
13674 // set the opposite here to ensure that the action is overriding the default behavior
13675 lsp_insert_mode: Some(LspInsertMode::Replace),
13676 ..Default::default()
13677 });
13678 });
13679
13680 cx.set_state(initial_state);
13681 cx.update_editor(|editor, window, cx| {
13682 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13683 });
13684 handle_completion_request_with_insert_and_replace(
13685 &mut cx,
13686 buffer_marked_text,
13687 vec![(completion_text, completion_text)],
13688 counter.clone(),
13689 )
13690 .await;
13691 cx.condition(|editor, _| editor.context_menu_visible())
13692 .await;
13693 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13694
13695 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13696 editor
13697 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13698 .unwrap()
13699 });
13700 cx.assert_editor_state(expected_with_insert_mode);
13701 handle_resolve_completion_request(&mut cx, None).await;
13702 apply_additional_edits.await.unwrap();
13703}
13704
13705#[gpui::test]
13706async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13707 init_test(cx, |_| {});
13708 let mut cx = EditorLspTestContext::new_rust(
13709 lsp::ServerCapabilities {
13710 completion_provider: Some(lsp::CompletionOptions {
13711 resolve_provider: Some(true),
13712 ..Default::default()
13713 }),
13714 ..Default::default()
13715 },
13716 cx,
13717 )
13718 .await;
13719
13720 // scenario: surrounding text matches completion text
13721 let completion_text = "to_offset";
13722 let initial_state = indoc! {"
13723 1. buf.to_offˇsuffix
13724 2. buf.to_offˇsuf
13725 3. buf.to_offˇfix
13726 4. buf.to_offˇ
13727 5. into_offˇensive
13728 6. ˇsuffix
13729 7. let ˇ //
13730 8. aaˇzz
13731 9. buf.to_off«zzzzzˇ»suffix
13732 10. buf.«ˇzzzzz»suffix
13733 11. to_off«ˇzzzzz»
13734
13735 buf.to_offˇsuffix // newest cursor
13736 "};
13737 let completion_marked_buffer = indoc! {"
13738 1. buf.to_offsuffix
13739 2. buf.to_offsuf
13740 3. buf.to_offfix
13741 4. buf.to_off
13742 5. into_offensive
13743 6. suffix
13744 7. let //
13745 8. aazz
13746 9. buf.to_offzzzzzsuffix
13747 10. buf.zzzzzsuffix
13748 11. to_offzzzzz
13749
13750 buf.<to_off|suffix> // newest cursor
13751 "};
13752 let expected = indoc! {"
13753 1. buf.to_offsetˇ
13754 2. buf.to_offsetˇsuf
13755 3. buf.to_offsetˇfix
13756 4. buf.to_offsetˇ
13757 5. into_offsetˇensive
13758 6. to_offsetˇsuffix
13759 7. let to_offsetˇ //
13760 8. aato_offsetˇzz
13761 9. buf.to_offsetˇ
13762 10. buf.to_offsetˇsuffix
13763 11. to_offsetˇ
13764
13765 buf.to_offsetˇ // newest cursor
13766 "};
13767 cx.set_state(initial_state);
13768 cx.update_editor(|editor, window, cx| {
13769 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13770 });
13771 handle_completion_request_with_insert_and_replace(
13772 &mut cx,
13773 completion_marked_buffer,
13774 vec![(completion_text, completion_text)],
13775 Arc::new(AtomicUsize::new(0)),
13776 )
13777 .await;
13778 cx.condition(|editor, _| editor.context_menu_visible())
13779 .await;
13780 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13781 editor
13782 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13783 .unwrap()
13784 });
13785 cx.assert_editor_state(expected);
13786 handle_resolve_completion_request(&mut cx, None).await;
13787 apply_additional_edits.await.unwrap();
13788
13789 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13790 let completion_text = "foo_and_bar";
13791 let initial_state = indoc! {"
13792 1. ooanbˇ
13793 2. zooanbˇ
13794 3. ooanbˇz
13795 4. zooanbˇz
13796 5. ooanˇ
13797 6. oanbˇ
13798
13799 ooanbˇ
13800 "};
13801 let completion_marked_buffer = indoc! {"
13802 1. ooanb
13803 2. zooanb
13804 3. ooanbz
13805 4. zooanbz
13806 5. ooan
13807 6. oanb
13808
13809 <ooanb|>
13810 "};
13811 let expected = indoc! {"
13812 1. foo_and_barˇ
13813 2. zfoo_and_barˇ
13814 3. foo_and_barˇz
13815 4. zfoo_and_barˇz
13816 5. ooanfoo_and_barˇ
13817 6. oanbfoo_and_barˇ
13818
13819 foo_and_barˇ
13820 "};
13821 cx.set_state(initial_state);
13822 cx.update_editor(|editor, window, cx| {
13823 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13824 });
13825 handle_completion_request_with_insert_and_replace(
13826 &mut cx,
13827 completion_marked_buffer,
13828 vec![(completion_text, completion_text)],
13829 Arc::new(AtomicUsize::new(0)),
13830 )
13831 .await;
13832 cx.condition(|editor, _| editor.context_menu_visible())
13833 .await;
13834 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13835 editor
13836 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13837 .unwrap()
13838 });
13839 cx.assert_editor_state(expected);
13840 handle_resolve_completion_request(&mut cx, None).await;
13841 apply_additional_edits.await.unwrap();
13842
13843 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13844 // (expects the same as if it was inserted at the end)
13845 let completion_text = "foo_and_bar";
13846 let initial_state = indoc! {"
13847 1. ooˇanb
13848 2. zooˇanb
13849 3. ooˇanbz
13850 4. zooˇanbz
13851
13852 ooˇanb
13853 "};
13854 let completion_marked_buffer = indoc! {"
13855 1. ooanb
13856 2. zooanb
13857 3. ooanbz
13858 4. zooanbz
13859
13860 <oo|anb>
13861 "};
13862 let expected = indoc! {"
13863 1. foo_and_barˇ
13864 2. zfoo_and_barˇ
13865 3. foo_and_barˇz
13866 4. zfoo_and_barˇz
13867
13868 foo_and_barˇ
13869 "};
13870 cx.set_state(initial_state);
13871 cx.update_editor(|editor, window, cx| {
13872 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13873 });
13874 handle_completion_request_with_insert_and_replace(
13875 &mut cx,
13876 completion_marked_buffer,
13877 vec![(completion_text, completion_text)],
13878 Arc::new(AtomicUsize::new(0)),
13879 )
13880 .await;
13881 cx.condition(|editor, _| editor.context_menu_visible())
13882 .await;
13883 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13884 editor
13885 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13886 .unwrap()
13887 });
13888 cx.assert_editor_state(expected);
13889 handle_resolve_completion_request(&mut cx, None).await;
13890 apply_additional_edits.await.unwrap();
13891}
13892
13893// This used to crash
13894#[gpui::test]
13895async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13896 init_test(cx, |_| {});
13897
13898 let buffer_text = indoc! {"
13899 fn main() {
13900 10.satu;
13901
13902 //
13903 // separate cursors so they open in different excerpts (manually reproducible)
13904 //
13905
13906 10.satu20;
13907 }
13908 "};
13909 let multibuffer_text_with_selections = indoc! {"
13910 fn main() {
13911 10.satuˇ;
13912
13913 //
13914
13915 //
13916
13917 10.satuˇ20;
13918 }
13919 "};
13920 let expected_multibuffer = indoc! {"
13921 fn main() {
13922 10.saturating_sub()ˇ;
13923
13924 //
13925
13926 //
13927
13928 10.saturating_sub()ˇ;
13929 }
13930 "};
13931
13932 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13933 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13934
13935 let fs = FakeFs::new(cx.executor());
13936 fs.insert_tree(
13937 path!("/a"),
13938 json!({
13939 "main.rs": buffer_text,
13940 }),
13941 )
13942 .await;
13943
13944 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13945 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13946 language_registry.add(rust_lang());
13947 let mut fake_servers = language_registry.register_fake_lsp(
13948 "Rust",
13949 FakeLspAdapter {
13950 capabilities: lsp::ServerCapabilities {
13951 completion_provider: Some(lsp::CompletionOptions {
13952 resolve_provider: None,
13953 ..lsp::CompletionOptions::default()
13954 }),
13955 ..lsp::ServerCapabilities::default()
13956 },
13957 ..FakeLspAdapter::default()
13958 },
13959 );
13960 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13961 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13962 let buffer = project
13963 .update(cx, |project, cx| {
13964 project.open_local_buffer(path!("/a/main.rs"), cx)
13965 })
13966 .await
13967 .unwrap();
13968
13969 let multi_buffer = cx.new(|cx| {
13970 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13971 multi_buffer.push_excerpts(
13972 buffer.clone(),
13973 [ExcerptRange::new(0..first_excerpt_end)],
13974 cx,
13975 );
13976 multi_buffer.push_excerpts(
13977 buffer.clone(),
13978 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13979 cx,
13980 );
13981 multi_buffer
13982 });
13983
13984 let editor = workspace
13985 .update(cx, |_, window, cx| {
13986 cx.new(|cx| {
13987 Editor::new(
13988 EditorMode::Full {
13989 scale_ui_elements_with_buffer_font_size: false,
13990 show_active_line_background: false,
13991 sized_by_content: false,
13992 },
13993 multi_buffer.clone(),
13994 Some(project.clone()),
13995 window,
13996 cx,
13997 )
13998 })
13999 })
14000 .unwrap();
14001
14002 let pane = workspace
14003 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14004 .unwrap();
14005 pane.update_in(cx, |pane, window, cx| {
14006 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14007 });
14008
14009 let fake_server = fake_servers.next().await.unwrap();
14010
14011 editor.update_in(cx, |editor, window, cx| {
14012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14013 s.select_ranges([
14014 Point::new(1, 11)..Point::new(1, 11),
14015 Point::new(7, 11)..Point::new(7, 11),
14016 ])
14017 });
14018
14019 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14020 });
14021
14022 editor.update_in(cx, |editor, window, cx| {
14023 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14024 });
14025
14026 fake_server
14027 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14028 let completion_item = lsp::CompletionItem {
14029 label: "saturating_sub()".into(),
14030 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14031 lsp::InsertReplaceEdit {
14032 new_text: "saturating_sub()".to_owned(),
14033 insert: lsp::Range::new(
14034 lsp::Position::new(7, 7),
14035 lsp::Position::new(7, 11),
14036 ),
14037 replace: lsp::Range::new(
14038 lsp::Position::new(7, 7),
14039 lsp::Position::new(7, 13),
14040 ),
14041 },
14042 )),
14043 ..lsp::CompletionItem::default()
14044 };
14045
14046 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14047 })
14048 .next()
14049 .await
14050 .unwrap();
14051
14052 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14053 .await;
14054
14055 editor
14056 .update_in(cx, |editor, window, cx| {
14057 editor
14058 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14059 .unwrap()
14060 })
14061 .await
14062 .unwrap();
14063
14064 editor.update(cx, |editor, cx| {
14065 assert_text_with_selections(editor, expected_multibuffer, cx);
14066 })
14067}
14068
14069#[gpui::test]
14070async fn test_completion(cx: &mut TestAppContext) {
14071 init_test(cx, |_| {});
14072
14073 let mut cx = EditorLspTestContext::new_rust(
14074 lsp::ServerCapabilities {
14075 completion_provider: Some(lsp::CompletionOptions {
14076 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14077 resolve_provider: Some(true),
14078 ..Default::default()
14079 }),
14080 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14081 ..Default::default()
14082 },
14083 cx,
14084 )
14085 .await;
14086 let counter = Arc::new(AtomicUsize::new(0));
14087
14088 cx.set_state(indoc! {"
14089 oneˇ
14090 two
14091 three
14092 "});
14093 cx.simulate_keystroke(".");
14094 handle_completion_request(
14095 indoc! {"
14096 one.|<>
14097 two
14098 three
14099 "},
14100 vec!["first_completion", "second_completion"],
14101 true,
14102 counter.clone(),
14103 &mut cx,
14104 )
14105 .await;
14106 cx.condition(|editor, _| editor.context_menu_visible())
14107 .await;
14108 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14109
14110 let _handler = handle_signature_help_request(
14111 &mut cx,
14112 lsp::SignatureHelp {
14113 signatures: vec![lsp::SignatureInformation {
14114 label: "test signature".to_string(),
14115 documentation: None,
14116 parameters: Some(vec![lsp::ParameterInformation {
14117 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14118 documentation: None,
14119 }]),
14120 active_parameter: None,
14121 }],
14122 active_signature: None,
14123 active_parameter: None,
14124 },
14125 );
14126 cx.update_editor(|editor, window, cx| {
14127 assert!(
14128 !editor.signature_help_state.is_shown(),
14129 "No signature help was called for"
14130 );
14131 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14132 });
14133 cx.run_until_parked();
14134 cx.update_editor(|editor, _, _| {
14135 assert!(
14136 !editor.signature_help_state.is_shown(),
14137 "No signature help should be shown when completions menu is open"
14138 );
14139 });
14140
14141 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14142 editor.context_menu_next(&Default::default(), window, cx);
14143 editor
14144 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14145 .unwrap()
14146 });
14147 cx.assert_editor_state(indoc! {"
14148 one.second_completionˇ
14149 two
14150 three
14151 "});
14152
14153 handle_resolve_completion_request(
14154 &mut cx,
14155 Some(vec![
14156 (
14157 //This overlaps with the primary completion edit which is
14158 //misbehavior from the LSP spec, test that we filter it out
14159 indoc! {"
14160 one.second_ˇcompletion
14161 two
14162 threeˇ
14163 "},
14164 "overlapping additional edit",
14165 ),
14166 (
14167 indoc! {"
14168 one.second_completion
14169 two
14170 threeˇ
14171 "},
14172 "\nadditional edit",
14173 ),
14174 ]),
14175 )
14176 .await;
14177 apply_additional_edits.await.unwrap();
14178 cx.assert_editor_state(indoc! {"
14179 one.second_completionˇ
14180 two
14181 three
14182 additional edit
14183 "});
14184
14185 cx.set_state(indoc! {"
14186 one.second_completion
14187 twoˇ
14188 threeˇ
14189 additional edit
14190 "});
14191 cx.simulate_keystroke(" ");
14192 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14193 cx.simulate_keystroke("s");
14194 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14195
14196 cx.assert_editor_state(indoc! {"
14197 one.second_completion
14198 two sˇ
14199 three sˇ
14200 additional edit
14201 "});
14202 handle_completion_request(
14203 indoc! {"
14204 one.second_completion
14205 two s
14206 three <s|>
14207 additional edit
14208 "},
14209 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14210 true,
14211 counter.clone(),
14212 &mut cx,
14213 )
14214 .await;
14215 cx.condition(|editor, _| editor.context_menu_visible())
14216 .await;
14217 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14218
14219 cx.simulate_keystroke("i");
14220
14221 handle_completion_request(
14222 indoc! {"
14223 one.second_completion
14224 two si
14225 three <si|>
14226 additional edit
14227 "},
14228 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14229 true,
14230 counter.clone(),
14231 &mut cx,
14232 )
14233 .await;
14234 cx.condition(|editor, _| editor.context_menu_visible())
14235 .await;
14236 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14237
14238 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14239 editor
14240 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14241 .unwrap()
14242 });
14243 cx.assert_editor_state(indoc! {"
14244 one.second_completion
14245 two sixth_completionˇ
14246 three sixth_completionˇ
14247 additional edit
14248 "});
14249
14250 apply_additional_edits.await.unwrap();
14251
14252 update_test_language_settings(&mut cx, |settings| {
14253 settings.defaults.show_completions_on_input = Some(false);
14254 });
14255 cx.set_state("editorˇ");
14256 cx.simulate_keystroke(".");
14257 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14258 cx.simulate_keystrokes("c l o");
14259 cx.assert_editor_state("editor.cloˇ");
14260 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14261 cx.update_editor(|editor, window, cx| {
14262 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14263 });
14264 handle_completion_request(
14265 "editor.<clo|>",
14266 vec!["close", "clobber"],
14267 true,
14268 counter.clone(),
14269 &mut cx,
14270 )
14271 .await;
14272 cx.condition(|editor, _| editor.context_menu_visible())
14273 .await;
14274 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14275
14276 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14277 editor
14278 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14279 .unwrap()
14280 });
14281 cx.assert_editor_state("editor.clobberˇ");
14282 handle_resolve_completion_request(&mut cx, None).await;
14283 apply_additional_edits.await.unwrap();
14284}
14285
14286#[gpui::test]
14287async fn test_completion_reuse(cx: &mut TestAppContext) {
14288 init_test(cx, |_| {});
14289
14290 let mut cx = EditorLspTestContext::new_rust(
14291 lsp::ServerCapabilities {
14292 completion_provider: Some(lsp::CompletionOptions {
14293 trigger_characters: Some(vec![".".to_string()]),
14294 ..Default::default()
14295 }),
14296 ..Default::default()
14297 },
14298 cx,
14299 )
14300 .await;
14301
14302 let counter = Arc::new(AtomicUsize::new(0));
14303 cx.set_state("objˇ");
14304 cx.simulate_keystroke(".");
14305
14306 // Initial completion request returns complete results
14307 let is_incomplete = false;
14308 handle_completion_request(
14309 "obj.|<>",
14310 vec!["a", "ab", "abc"],
14311 is_incomplete,
14312 counter.clone(),
14313 &mut cx,
14314 )
14315 .await;
14316 cx.run_until_parked();
14317 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14318 cx.assert_editor_state("obj.ˇ");
14319 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14320
14321 // Type "a" - filters existing completions
14322 cx.simulate_keystroke("a");
14323 cx.run_until_parked();
14324 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14325 cx.assert_editor_state("obj.aˇ");
14326 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14327
14328 // Type "b" - filters existing completions
14329 cx.simulate_keystroke("b");
14330 cx.run_until_parked();
14331 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14332 cx.assert_editor_state("obj.abˇ");
14333 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14334
14335 // Type "c" - filters existing completions
14336 cx.simulate_keystroke("c");
14337 cx.run_until_parked();
14338 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14339 cx.assert_editor_state("obj.abcˇ");
14340 check_displayed_completions(vec!["abc"], &mut cx);
14341
14342 // Backspace to delete "c" - filters existing completions
14343 cx.update_editor(|editor, window, cx| {
14344 editor.backspace(&Backspace, window, cx);
14345 });
14346 cx.run_until_parked();
14347 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14348 cx.assert_editor_state("obj.abˇ");
14349 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14350
14351 // Moving cursor to the left dismisses menu.
14352 cx.update_editor(|editor, window, cx| {
14353 editor.move_left(&MoveLeft, window, cx);
14354 });
14355 cx.run_until_parked();
14356 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14357 cx.assert_editor_state("obj.aˇb");
14358 cx.update_editor(|editor, _, _| {
14359 assert_eq!(editor.context_menu_visible(), false);
14360 });
14361
14362 // Type "b" - new request
14363 cx.simulate_keystroke("b");
14364 let is_incomplete = false;
14365 handle_completion_request(
14366 "obj.<ab|>a",
14367 vec!["ab", "abc"],
14368 is_incomplete,
14369 counter.clone(),
14370 &mut cx,
14371 )
14372 .await;
14373 cx.run_until_parked();
14374 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14375 cx.assert_editor_state("obj.abˇb");
14376 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14377
14378 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14379 cx.update_editor(|editor, window, cx| {
14380 editor.backspace(&Backspace, window, cx);
14381 });
14382 let is_incomplete = false;
14383 handle_completion_request(
14384 "obj.<a|>b",
14385 vec!["a", "ab", "abc"],
14386 is_incomplete,
14387 counter.clone(),
14388 &mut cx,
14389 )
14390 .await;
14391 cx.run_until_parked();
14392 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14393 cx.assert_editor_state("obj.aˇb");
14394 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14395
14396 // Backspace to delete "a" - dismisses menu.
14397 cx.update_editor(|editor, window, cx| {
14398 editor.backspace(&Backspace, window, cx);
14399 });
14400 cx.run_until_parked();
14401 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14402 cx.assert_editor_state("obj.ˇb");
14403 cx.update_editor(|editor, _, _| {
14404 assert_eq!(editor.context_menu_visible(), false);
14405 });
14406}
14407
14408#[gpui::test]
14409async fn test_word_completion(cx: &mut TestAppContext) {
14410 let lsp_fetch_timeout_ms = 10;
14411 init_test(cx, |language_settings| {
14412 language_settings.defaults.completions = Some(CompletionSettingsContent {
14413 words_min_length: Some(0),
14414 lsp_fetch_timeout_ms: Some(10),
14415 lsp_insert_mode: Some(LspInsertMode::Insert),
14416 ..Default::default()
14417 });
14418 });
14419
14420 let mut cx = EditorLspTestContext::new_rust(
14421 lsp::ServerCapabilities {
14422 completion_provider: Some(lsp::CompletionOptions {
14423 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14424 ..lsp::CompletionOptions::default()
14425 }),
14426 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14427 ..lsp::ServerCapabilities::default()
14428 },
14429 cx,
14430 )
14431 .await;
14432
14433 let throttle_completions = Arc::new(AtomicBool::new(false));
14434
14435 let lsp_throttle_completions = throttle_completions.clone();
14436 let _completion_requests_handler =
14437 cx.lsp
14438 .server
14439 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14440 let lsp_throttle_completions = lsp_throttle_completions.clone();
14441 let cx = cx.clone();
14442 async move {
14443 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14444 cx.background_executor()
14445 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14446 .await;
14447 }
14448 Ok(Some(lsp::CompletionResponse::Array(vec![
14449 lsp::CompletionItem {
14450 label: "first".into(),
14451 ..lsp::CompletionItem::default()
14452 },
14453 lsp::CompletionItem {
14454 label: "last".into(),
14455 ..lsp::CompletionItem::default()
14456 },
14457 ])))
14458 }
14459 });
14460
14461 cx.set_state(indoc! {"
14462 oneˇ
14463 two
14464 three
14465 "});
14466 cx.simulate_keystroke(".");
14467 cx.executor().run_until_parked();
14468 cx.condition(|editor, _| editor.context_menu_visible())
14469 .await;
14470 cx.update_editor(|editor, window, cx| {
14471 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14472 {
14473 assert_eq!(
14474 completion_menu_entries(menu),
14475 &["first", "last"],
14476 "When LSP server is fast to reply, no fallback word completions are used"
14477 );
14478 } else {
14479 panic!("expected completion menu to be open");
14480 }
14481 editor.cancel(&Cancel, window, cx);
14482 });
14483 cx.executor().run_until_parked();
14484 cx.condition(|editor, _| !editor.context_menu_visible())
14485 .await;
14486
14487 throttle_completions.store(true, atomic::Ordering::Release);
14488 cx.simulate_keystroke(".");
14489 cx.executor()
14490 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14491 cx.executor().run_until_parked();
14492 cx.condition(|editor, _| editor.context_menu_visible())
14493 .await;
14494 cx.update_editor(|editor, _, _| {
14495 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14496 {
14497 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14498 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14499 } else {
14500 panic!("expected completion menu to be open");
14501 }
14502 });
14503}
14504
14505#[gpui::test]
14506async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14507 init_test(cx, |language_settings| {
14508 language_settings.defaults.completions = Some(CompletionSettingsContent {
14509 words: Some(WordsCompletionMode::Enabled),
14510 words_min_length: Some(0),
14511 lsp_insert_mode: Some(LspInsertMode::Insert),
14512 ..Default::default()
14513 });
14514 });
14515
14516 let mut cx = EditorLspTestContext::new_rust(
14517 lsp::ServerCapabilities {
14518 completion_provider: Some(lsp::CompletionOptions {
14519 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14520 ..lsp::CompletionOptions::default()
14521 }),
14522 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14523 ..lsp::ServerCapabilities::default()
14524 },
14525 cx,
14526 )
14527 .await;
14528
14529 let _completion_requests_handler =
14530 cx.lsp
14531 .server
14532 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14533 Ok(Some(lsp::CompletionResponse::Array(vec![
14534 lsp::CompletionItem {
14535 label: "first".into(),
14536 ..lsp::CompletionItem::default()
14537 },
14538 lsp::CompletionItem {
14539 label: "last".into(),
14540 ..lsp::CompletionItem::default()
14541 },
14542 ])))
14543 });
14544
14545 cx.set_state(indoc! {"ˇ
14546 first
14547 last
14548 second
14549 "});
14550 cx.simulate_keystroke(".");
14551 cx.executor().run_until_parked();
14552 cx.condition(|editor, _| editor.context_menu_visible())
14553 .await;
14554 cx.update_editor(|editor, _, _| {
14555 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14556 {
14557 assert_eq!(
14558 completion_menu_entries(menu),
14559 &["first", "last", "second"],
14560 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14561 );
14562 } else {
14563 panic!("expected completion menu to be open");
14564 }
14565 });
14566}
14567
14568#[gpui::test]
14569async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14570 init_test(cx, |language_settings| {
14571 language_settings.defaults.completions = Some(CompletionSettingsContent {
14572 words: Some(WordsCompletionMode::Disabled),
14573 words_min_length: Some(0),
14574 lsp_insert_mode: Some(LspInsertMode::Insert),
14575 ..Default::default()
14576 });
14577 });
14578
14579 let mut cx = EditorLspTestContext::new_rust(
14580 lsp::ServerCapabilities {
14581 completion_provider: Some(lsp::CompletionOptions {
14582 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14583 ..lsp::CompletionOptions::default()
14584 }),
14585 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14586 ..lsp::ServerCapabilities::default()
14587 },
14588 cx,
14589 )
14590 .await;
14591
14592 let _completion_requests_handler =
14593 cx.lsp
14594 .server
14595 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14596 panic!("LSP completions should not be queried when dealing with word completions")
14597 });
14598
14599 cx.set_state(indoc! {"ˇ
14600 first
14601 last
14602 second
14603 "});
14604 cx.update_editor(|editor, window, cx| {
14605 editor.show_word_completions(&ShowWordCompletions, window, cx);
14606 });
14607 cx.executor().run_until_parked();
14608 cx.condition(|editor, _| editor.context_menu_visible())
14609 .await;
14610 cx.update_editor(|editor, _, _| {
14611 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14612 {
14613 assert_eq!(
14614 completion_menu_entries(menu),
14615 &["first", "last", "second"],
14616 "`ShowWordCompletions` action should show word completions"
14617 );
14618 } else {
14619 panic!("expected completion menu to be open");
14620 }
14621 });
14622
14623 cx.simulate_keystroke("l");
14624 cx.executor().run_until_parked();
14625 cx.condition(|editor, _| editor.context_menu_visible())
14626 .await;
14627 cx.update_editor(|editor, _, _| {
14628 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14629 {
14630 assert_eq!(
14631 completion_menu_entries(menu),
14632 &["last"],
14633 "After showing word completions, further editing should filter them and not query the LSP"
14634 );
14635 } else {
14636 panic!("expected completion menu to be open");
14637 }
14638 });
14639}
14640
14641#[gpui::test]
14642async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14643 init_test(cx, |language_settings| {
14644 language_settings.defaults.completions = Some(CompletionSettingsContent {
14645 words_min_length: Some(0),
14646 lsp: Some(false),
14647 lsp_insert_mode: Some(LspInsertMode::Insert),
14648 ..Default::default()
14649 });
14650 });
14651
14652 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14653
14654 cx.set_state(indoc! {"ˇ
14655 0_usize
14656 let
14657 33
14658 4.5f32
14659 "});
14660 cx.update_editor(|editor, window, cx| {
14661 editor.show_completions(&ShowCompletions::default(), window, cx);
14662 });
14663 cx.executor().run_until_parked();
14664 cx.condition(|editor, _| editor.context_menu_visible())
14665 .await;
14666 cx.update_editor(|editor, window, cx| {
14667 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14668 {
14669 assert_eq!(
14670 completion_menu_entries(menu),
14671 &["let"],
14672 "With no digits in the completion query, no digits should be in the word completions"
14673 );
14674 } else {
14675 panic!("expected completion menu to be open");
14676 }
14677 editor.cancel(&Cancel, window, cx);
14678 });
14679
14680 cx.set_state(indoc! {"3ˇ
14681 0_usize
14682 let
14683 3
14684 33.35f32
14685 "});
14686 cx.update_editor(|editor, window, cx| {
14687 editor.show_completions(&ShowCompletions::default(), window, cx);
14688 });
14689 cx.executor().run_until_parked();
14690 cx.condition(|editor, _| editor.context_menu_visible())
14691 .await;
14692 cx.update_editor(|editor, _, _| {
14693 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14694 {
14695 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14696 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14697 } else {
14698 panic!("expected completion menu to be open");
14699 }
14700 });
14701}
14702
14703#[gpui::test]
14704async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14705 init_test(cx, |language_settings| {
14706 language_settings.defaults.completions = Some(CompletionSettingsContent {
14707 words: Some(WordsCompletionMode::Enabled),
14708 words_min_length: Some(3),
14709 lsp_insert_mode: Some(LspInsertMode::Insert),
14710 ..Default::default()
14711 });
14712 });
14713
14714 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14715 cx.set_state(indoc! {"ˇ
14716 wow
14717 wowen
14718 wowser
14719 "});
14720 cx.simulate_keystroke("w");
14721 cx.executor().run_until_parked();
14722 cx.update_editor(|editor, _, _| {
14723 if editor.context_menu.borrow_mut().is_some() {
14724 panic!(
14725 "expected completion menu to be hidden, as words completion threshold is not met"
14726 );
14727 }
14728 });
14729
14730 cx.update_editor(|editor, window, cx| {
14731 editor.show_word_completions(&ShowWordCompletions, window, cx);
14732 });
14733 cx.executor().run_until_parked();
14734 cx.update_editor(|editor, window, cx| {
14735 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14736 {
14737 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");
14738 } else {
14739 panic!("expected completion menu to be open after the word completions are called with an action");
14740 }
14741
14742 editor.cancel(&Cancel, window, cx);
14743 });
14744 cx.update_editor(|editor, _, _| {
14745 if editor.context_menu.borrow_mut().is_some() {
14746 panic!("expected completion menu to be hidden after canceling");
14747 }
14748 });
14749
14750 cx.simulate_keystroke("o");
14751 cx.executor().run_until_parked();
14752 cx.update_editor(|editor, _, _| {
14753 if editor.context_menu.borrow_mut().is_some() {
14754 panic!(
14755 "expected completion menu to be hidden, as words completion threshold is not met still"
14756 );
14757 }
14758 });
14759
14760 cx.simulate_keystroke("w");
14761 cx.executor().run_until_parked();
14762 cx.update_editor(|editor, _, _| {
14763 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14764 {
14765 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14766 } else {
14767 panic!("expected completion menu to be open after the word completions threshold is met");
14768 }
14769 });
14770}
14771
14772#[gpui::test]
14773async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14774 init_test(cx, |language_settings| {
14775 language_settings.defaults.completions = Some(CompletionSettingsContent {
14776 words: Some(WordsCompletionMode::Enabled),
14777 words_min_length: Some(0),
14778 lsp_insert_mode: Some(LspInsertMode::Insert),
14779 ..Default::default()
14780 });
14781 });
14782
14783 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14784 cx.update_editor(|editor, _, _| {
14785 editor.disable_word_completions();
14786 });
14787 cx.set_state(indoc! {"ˇ
14788 wow
14789 wowen
14790 wowser
14791 "});
14792 cx.simulate_keystroke("w");
14793 cx.executor().run_until_parked();
14794 cx.update_editor(|editor, _, _| {
14795 if editor.context_menu.borrow_mut().is_some() {
14796 panic!(
14797 "expected completion menu to be hidden, as words completion are disabled for this editor"
14798 );
14799 }
14800 });
14801
14802 cx.update_editor(|editor, window, cx| {
14803 editor.show_word_completions(&ShowWordCompletions, window, cx);
14804 });
14805 cx.executor().run_until_parked();
14806 cx.update_editor(|editor, _, _| {
14807 if editor.context_menu.borrow_mut().is_some() {
14808 panic!(
14809 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14810 );
14811 }
14812 });
14813}
14814
14815fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14816 let position = || lsp::Position {
14817 line: params.text_document_position.position.line,
14818 character: params.text_document_position.position.character,
14819 };
14820 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14821 range: lsp::Range {
14822 start: position(),
14823 end: position(),
14824 },
14825 new_text: text.to_string(),
14826 }))
14827}
14828
14829#[gpui::test]
14830async fn test_multiline_completion(cx: &mut TestAppContext) {
14831 init_test(cx, |_| {});
14832
14833 let fs = FakeFs::new(cx.executor());
14834 fs.insert_tree(
14835 path!("/a"),
14836 json!({
14837 "main.ts": "a",
14838 }),
14839 )
14840 .await;
14841
14842 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14844 let typescript_language = Arc::new(Language::new(
14845 LanguageConfig {
14846 name: "TypeScript".into(),
14847 matcher: LanguageMatcher {
14848 path_suffixes: vec!["ts".to_string()],
14849 ..LanguageMatcher::default()
14850 },
14851 line_comments: vec!["// ".into()],
14852 ..LanguageConfig::default()
14853 },
14854 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14855 ));
14856 language_registry.add(typescript_language.clone());
14857 let mut fake_servers = language_registry.register_fake_lsp(
14858 "TypeScript",
14859 FakeLspAdapter {
14860 capabilities: lsp::ServerCapabilities {
14861 completion_provider: Some(lsp::CompletionOptions {
14862 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14863 ..lsp::CompletionOptions::default()
14864 }),
14865 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14866 ..lsp::ServerCapabilities::default()
14867 },
14868 // Emulate vtsls label generation
14869 label_for_completion: Some(Box::new(|item, _| {
14870 let text = if let Some(description) = item
14871 .label_details
14872 .as_ref()
14873 .and_then(|label_details| label_details.description.as_ref())
14874 {
14875 format!("{} {}", item.label, description)
14876 } else if let Some(detail) = &item.detail {
14877 format!("{} {}", item.label, detail)
14878 } else {
14879 item.label.clone()
14880 };
14881 let len = text.len();
14882 Some(language::CodeLabel {
14883 text,
14884 runs: Vec::new(),
14885 filter_range: 0..len,
14886 })
14887 })),
14888 ..FakeLspAdapter::default()
14889 },
14890 );
14891 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14892 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14893 let worktree_id = workspace
14894 .update(cx, |workspace, _window, cx| {
14895 workspace.project().update(cx, |project, cx| {
14896 project.worktrees(cx).next().unwrap().read(cx).id()
14897 })
14898 })
14899 .unwrap();
14900 let _buffer = project
14901 .update(cx, |project, cx| {
14902 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14903 })
14904 .await
14905 .unwrap();
14906 let editor = workspace
14907 .update(cx, |workspace, window, cx| {
14908 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14909 })
14910 .unwrap()
14911 .await
14912 .unwrap()
14913 .downcast::<Editor>()
14914 .unwrap();
14915 let fake_server = fake_servers.next().await.unwrap();
14916
14917 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14918 let multiline_label_2 = "a\nb\nc\n";
14919 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14920 let multiline_description = "d\ne\nf\n";
14921 let multiline_detail_2 = "g\nh\ni\n";
14922
14923 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14924 move |params, _| async move {
14925 Ok(Some(lsp::CompletionResponse::Array(vec![
14926 lsp::CompletionItem {
14927 label: multiline_label.to_string(),
14928 text_edit: gen_text_edit(¶ms, "new_text_1"),
14929 ..lsp::CompletionItem::default()
14930 },
14931 lsp::CompletionItem {
14932 label: "single line label 1".to_string(),
14933 detail: Some(multiline_detail.to_string()),
14934 text_edit: gen_text_edit(¶ms, "new_text_2"),
14935 ..lsp::CompletionItem::default()
14936 },
14937 lsp::CompletionItem {
14938 label: "single line label 2".to_string(),
14939 label_details: Some(lsp::CompletionItemLabelDetails {
14940 description: Some(multiline_description.to_string()),
14941 detail: None,
14942 }),
14943 text_edit: gen_text_edit(¶ms, "new_text_2"),
14944 ..lsp::CompletionItem::default()
14945 },
14946 lsp::CompletionItem {
14947 label: multiline_label_2.to_string(),
14948 detail: Some(multiline_detail_2.to_string()),
14949 text_edit: gen_text_edit(¶ms, "new_text_3"),
14950 ..lsp::CompletionItem::default()
14951 },
14952 lsp::CompletionItem {
14953 label: "Label with many spaces and \t but without newlines".to_string(),
14954 detail: Some(
14955 "Details with many spaces and \t but without newlines".to_string(),
14956 ),
14957 text_edit: gen_text_edit(¶ms, "new_text_4"),
14958 ..lsp::CompletionItem::default()
14959 },
14960 ])))
14961 },
14962 );
14963
14964 editor.update_in(cx, |editor, window, cx| {
14965 cx.focus_self(window);
14966 editor.move_to_end(&MoveToEnd, window, cx);
14967 editor.handle_input(".", window, cx);
14968 });
14969 cx.run_until_parked();
14970 completion_handle.next().await.unwrap();
14971
14972 editor.update(cx, |editor, _| {
14973 assert!(editor.context_menu_visible());
14974 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14975 {
14976 let completion_labels = menu
14977 .completions
14978 .borrow()
14979 .iter()
14980 .map(|c| c.label.text.clone())
14981 .collect::<Vec<_>>();
14982 assert_eq!(
14983 completion_labels,
14984 &[
14985 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14986 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14987 "single line label 2 d e f ",
14988 "a b c g h i ",
14989 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14990 ],
14991 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14992 );
14993
14994 for completion in menu
14995 .completions
14996 .borrow()
14997 .iter() {
14998 assert_eq!(
14999 completion.label.filter_range,
15000 0..completion.label.text.len(),
15001 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15002 );
15003 }
15004 } else {
15005 panic!("expected completion menu to be open");
15006 }
15007 });
15008}
15009
15010#[gpui::test]
15011async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15012 init_test(cx, |_| {});
15013 let mut cx = EditorLspTestContext::new_rust(
15014 lsp::ServerCapabilities {
15015 completion_provider: Some(lsp::CompletionOptions {
15016 trigger_characters: Some(vec![".".to_string()]),
15017 ..Default::default()
15018 }),
15019 ..Default::default()
15020 },
15021 cx,
15022 )
15023 .await;
15024 cx.lsp
15025 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15026 Ok(Some(lsp::CompletionResponse::Array(vec![
15027 lsp::CompletionItem {
15028 label: "first".into(),
15029 ..Default::default()
15030 },
15031 lsp::CompletionItem {
15032 label: "last".into(),
15033 ..Default::default()
15034 },
15035 ])))
15036 });
15037 cx.set_state("variableˇ");
15038 cx.simulate_keystroke(".");
15039 cx.executor().run_until_parked();
15040
15041 cx.update_editor(|editor, _, _| {
15042 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15043 {
15044 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15045 } else {
15046 panic!("expected completion menu to be open");
15047 }
15048 });
15049
15050 cx.update_editor(|editor, window, cx| {
15051 editor.move_page_down(&MovePageDown::default(), window, cx);
15052 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15053 {
15054 assert!(
15055 menu.selected_item == 1,
15056 "expected PageDown to select the last item from the context menu"
15057 );
15058 } else {
15059 panic!("expected completion menu to stay open after PageDown");
15060 }
15061 });
15062
15063 cx.update_editor(|editor, window, cx| {
15064 editor.move_page_up(&MovePageUp::default(), window, cx);
15065 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15066 {
15067 assert!(
15068 menu.selected_item == 0,
15069 "expected PageUp to select the first item from the context menu"
15070 );
15071 } else {
15072 panic!("expected completion menu to stay open after PageUp");
15073 }
15074 });
15075}
15076
15077#[gpui::test]
15078async fn test_as_is_completions(cx: &mut TestAppContext) {
15079 init_test(cx, |_| {});
15080 let mut cx = EditorLspTestContext::new_rust(
15081 lsp::ServerCapabilities {
15082 completion_provider: Some(lsp::CompletionOptions {
15083 ..Default::default()
15084 }),
15085 ..Default::default()
15086 },
15087 cx,
15088 )
15089 .await;
15090 cx.lsp
15091 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15092 Ok(Some(lsp::CompletionResponse::Array(vec![
15093 lsp::CompletionItem {
15094 label: "unsafe".into(),
15095 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15096 range: lsp::Range {
15097 start: lsp::Position {
15098 line: 1,
15099 character: 2,
15100 },
15101 end: lsp::Position {
15102 line: 1,
15103 character: 3,
15104 },
15105 },
15106 new_text: "unsafe".to_string(),
15107 })),
15108 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15109 ..Default::default()
15110 },
15111 ])))
15112 });
15113 cx.set_state("fn a() {}\n nˇ");
15114 cx.executor().run_until_parked();
15115 cx.update_editor(|editor, window, cx| {
15116 editor.show_completions(
15117 &ShowCompletions {
15118 trigger: Some("\n".into()),
15119 },
15120 window,
15121 cx,
15122 );
15123 });
15124 cx.executor().run_until_parked();
15125
15126 cx.update_editor(|editor, window, cx| {
15127 editor.confirm_completion(&Default::default(), window, cx)
15128 });
15129 cx.executor().run_until_parked();
15130 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15131}
15132
15133#[gpui::test]
15134async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15135 init_test(cx, |_| {});
15136 let language =
15137 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15138 let mut cx = EditorLspTestContext::new(
15139 language,
15140 lsp::ServerCapabilities {
15141 completion_provider: Some(lsp::CompletionOptions {
15142 ..lsp::CompletionOptions::default()
15143 }),
15144 ..lsp::ServerCapabilities::default()
15145 },
15146 cx,
15147 )
15148 .await;
15149
15150 cx.set_state(
15151 "#ifndef BAR_H
15152#define BAR_H
15153
15154#include <stdbool.h>
15155
15156int fn_branch(bool do_branch1, bool do_branch2);
15157
15158#endif // BAR_H
15159ˇ",
15160 );
15161 cx.executor().run_until_parked();
15162 cx.update_editor(|editor, window, cx| {
15163 editor.handle_input("#", window, cx);
15164 });
15165 cx.executor().run_until_parked();
15166 cx.update_editor(|editor, window, cx| {
15167 editor.handle_input("i", window, cx);
15168 });
15169 cx.executor().run_until_parked();
15170 cx.update_editor(|editor, window, cx| {
15171 editor.handle_input("n", window, cx);
15172 });
15173 cx.executor().run_until_parked();
15174 cx.assert_editor_state(
15175 "#ifndef BAR_H
15176#define BAR_H
15177
15178#include <stdbool.h>
15179
15180int fn_branch(bool do_branch1, bool do_branch2);
15181
15182#endif // BAR_H
15183#inˇ",
15184 );
15185
15186 cx.lsp
15187 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15188 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15189 is_incomplete: false,
15190 item_defaults: None,
15191 items: vec![lsp::CompletionItem {
15192 kind: Some(lsp::CompletionItemKind::SNIPPET),
15193 label_details: Some(lsp::CompletionItemLabelDetails {
15194 detail: Some("header".to_string()),
15195 description: None,
15196 }),
15197 label: " include".to_string(),
15198 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15199 range: lsp::Range {
15200 start: lsp::Position {
15201 line: 8,
15202 character: 1,
15203 },
15204 end: lsp::Position {
15205 line: 8,
15206 character: 1,
15207 },
15208 },
15209 new_text: "include \"$0\"".to_string(),
15210 })),
15211 sort_text: Some("40b67681include".to_string()),
15212 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15213 filter_text: Some("include".to_string()),
15214 insert_text: Some("include \"$0\"".to_string()),
15215 ..lsp::CompletionItem::default()
15216 }],
15217 })))
15218 });
15219 cx.update_editor(|editor, window, cx| {
15220 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15221 });
15222 cx.executor().run_until_parked();
15223 cx.update_editor(|editor, window, cx| {
15224 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15225 });
15226 cx.executor().run_until_parked();
15227 cx.assert_editor_state(
15228 "#ifndef BAR_H
15229#define BAR_H
15230
15231#include <stdbool.h>
15232
15233int fn_branch(bool do_branch1, bool do_branch2);
15234
15235#endif // BAR_H
15236#include \"ˇ\"",
15237 );
15238
15239 cx.lsp
15240 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15241 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15242 is_incomplete: true,
15243 item_defaults: None,
15244 items: vec![lsp::CompletionItem {
15245 kind: Some(lsp::CompletionItemKind::FILE),
15246 label: "AGL/".to_string(),
15247 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15248 range: lsp::Range {
15249 start: lsp::Position {
15250 line: 8,
15251 character: 10,
15252 },
15253 end: lsp::Position {
15254 line: 8,
15255 character: 11,
15256 },
15257 },
15258 new_text: "AGL/".to_string(),
15259 })),
15260 sort_text: Some("40b67681AGL/".to_string()),
15261 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15262 filter_text: Some("AGL/".to_string()),
15263 insert_text: Some("AGL/".to_string()),
15264 ..lsp::CompletionItem::default()
15265 }],
15266 })))
15267 });
15268 cx.update_editor(|editor, window, cx| {
15269 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15270 });
15271 cx.executor().run_until_parked();
15272 cx.update_editor(|editor, window, cx| {
15273 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15274 });
15275 cx.executor().run_until_parked();
15276 cx.assert_editor_state(
15277 r##"#ifndef BAR_H
15278#define BAR_H
15279
15280#include <stdbool.h>
15281
15282int fn_branch(bool do_branch1, bool do_branch2);
15283
15284#endif // BAR_H
15285#include "AGL/ˇ"##,
15286 );
15287
15288 cx.update_editor(|editor, window, cx| {
15289 editor.handle_input("\"", window, cx);
15290 });
15291 cx.executor().run_until_parked();
15292 cx.assert_editor_state(
15293 r##"#ifndef BAR_H
15294#define BAR_H
15295
15296#include <stdbool.h>
15297
15298int fn_branch(bool do_branch1, bool do_branch2);
15299
15300#endif // BAR_H
15301#include "AGL/"ˇ"##,
15302 );
15303}
15304
15305#[gpui::test]
15306async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15307 init_test(cx, |_| {});
15308
15309 let mut cx = EditorLspTestContext::new_rust(
15310 lsp::ServerCapabilities {
15311 completion_provider: Some(lsp::CompletionOptions {
15312 trigger_characters: Some(vec![".".to_string()]),
15313 resolve_provider: Some(true),
15314 ..Default::default()
15315 }),
15316 ..Default::default()
15317 },
15318 cx,
15319 )
15320 .await;
15321
15322 cx.set_state("fn main() { let a = 2ˇ; }");
15323 cx.simulate_keystroke(".");
15324 let completion_item = lsp::CompletionItem {
15325 label: "Some".into(),
15326 kind: Some(lsp::CompletionItemKind::SNIPPET),
15327 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15328 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15329 kind: lsp::MarkupKind::Markdown,
15330 value: "```rust\nSome(2)\n```".to_string(),
15331 })),
15332 deprecated: Some(false),
15333 sort_text: Some("Some".to_string()),
15334 filter_text: Some("Some".to_string()),
15335 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15336 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15337 range: lsp::Range {
15338 start: lsp::Position {
15339 line: 0,
15340 character: 22,
15341 },
15342 end: lsp::Position {
15343 line: 0,
15344 character: 22,
15345 },
15346 },
15347 new_text: "Some(2)".to_string(),
15348 })),
15349 additional_text_edits: Some(vec![lsp::TextEdit {
15350 range: lsp::Range {
15351 start: lsp::Position {
15352 line: 0,
15353 character: 20,
15354 },
15355 end: lsp::Position {
15356 line: 0,
15357 character: 22,
15358 },
15359 },
15360 new_text: "".to_string(),
15361 }]),
15362 ..Default::default()
15363 };
15364
15365 let closure_completion_item = completion_item.clone();
15366 let counter = Arc::new(AtomicUsize::new(0));
15367 let counter_clone = counter.clone();
15368 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15369 let task_completion_item = closure_completion_item.clone();
15370 counter_clone.fetch_add(1, atomic::Ordering::Release);
15371 async move {
15372 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15373 is_incomplete: true,
15374 item_defaults: None,
15375 items: vec![task_completion_item],
15376 })))
15377 }
15378 });
15379
15380 cx.condition(|editor, _| editor.context_menu_visible())
15381 .await;
15382 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15383 assert!(request.next().await.is_some());
15384 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15385
15386 cx.simulate_keystrokes("S o m");
15387 cx.condition(|editor, _| editor.context_menu_visible())
15388 .await;
15389 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15390 assert!(request.next().await.is_some());
15391 assert!(request.next().await.is_some());
15392 assert!(request.next().await.is_some());
15393 request.close();
15394 assert!(request.next().await.is_none());
15395 assert_eq!(
15396 counter.load(atomic::Ordering::Acquire),
15397 4,
15398 "With the completions menu open, only one LSP request should happen per input"
15399 );
15400}
15401
15402#[gpui::test]
15403async fn test_toggle_comment(cx: &mut TestAppContext) {
15404 init_test(cx, |_| {});
15405 let mut cx = EditorTestContext::new(cx).await;
15406 let language = Arc::new(Language::new(
15407 LanguageConfig {
15408 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15409 ..Default::default()
15410 },
15411 Some(tree_sitter_rust::LANGUAGE.into()),
15412 ));
15413 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15414
15415 // If multiple selections intersect a line, the line is only toggled once.
15416 cx.set_state(indoc! {"
15417 fn a() {
15418 «//b();
15419 ˇ»// «c();
15420 //ˇ» d();
15421 }
15422 "});
15423
15424 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15425
15426 cx.assert_editor_state(indoc! {"
15427 fn a() {
15428 «b();
15429 c();
15430 ˇ» d();
15431 }
15432 "});
15433
15434 // The comment prefix is inserted at the same column for every line in a
15435 // selection.
15436 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15437
15438 cx.assert_editor_state(indoc! {"
15439 fn a() {
15440 // «b();
15441 // c();
15442 ˇ»// d();
15443 }
15444 "});
15445
15446 // If a selection ends at the beginning of a line, that line is not toggled.
15447 cx.set_selections_state(indoc! {"
15448 fn a() {
15449 // b();
15450 «// c();
15451 ˇ» // d();
15452 }
15453 "});
15454
15455 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15456
15457 cx.assert_editor_state(indoc! {"
15458 fn a() {
15459 // b();
15460 «c();
15461 ˇ» // d();
15462 }
15463 "});
15464
15465 // If a selection span a single line and is empty, the line is toggled.
15466 cx.set_state(indoc! {"
15467 fn a() {
15468 a();
15469 b();
15470 ˇ
15471 }
15472 "});
15473
15474 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15475
15476 cx.assert_editor_state(indoc! {"
15477 fn a() {
15478 a();
15479 b();
15480 //•ˇ
15481 }
15482 "});
15483
15484 // If a selection span multiple lines, empty lines are not toggled.
15485 cx.set_state(indoc! {"
15486 fn a() {
15487 «a();
15488
15489 c();ˇ»
15490 }
15491 "});
15492
15493 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15494
15495 cx.assert_editor_state(indoc! {"
15496 fn a() {
15497 // «a();
15498
15499 // c();ˇ»
15500 }
15501 "});
15502
15503 // If a selection includes multiple comment prefixes, all lines are uncommented.
15504 cx.set_state(indoc! {"
15505 fn a() {
15506 «// a();
15507 /// b();
15508 //! c();ˇ»
15509 }
15510 "});
15511
15512 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15513
15514 cx.assert_editor_state(indoc! {"
15515 fn a() {
15516 «a();
15517 b();
15518 c();ˇ»
15519 }
15520 "});
15521}
15522
15523#[gpui::test]
15524async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15525 init_test(cx, |_| {});
15526 let mut cx = EditorTestContext::new(cx).await;
15527 let language = Arc::new(Language::new(
15528 LanguageConfig {
15529 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15530 ..Default::default()
15531 },
15532 Some(tree_sitter_rust::LANGUAGE.into()),
15533 ));
15534 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15535
15536 let toggle_comments = &ToggleComments {
15537 advance_downwards: false,
15538 ignore_indent: true,
15539 };
15540
15541 // If multiple selections intersect a line, the line is only toggled once.
15542 cx.set_state(indoc! {"
15543 fn a() {
15544 // «b();
15545 // c();
15546 // ˇ» d();
15547 }
15548 "});
15549
15550 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15551
15552 cx.assert_editor_state(indoc! {"
15553 fn a() {
15554 «b();
15555 c();
15556 ˇ» d();
15557 }
15558 "});
15559
15560 // The comment prefix is inserted at the beginning of each line
15561 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15562
15563 cx.assert_editor_state(indoc! {"
15564 fn a() {
15565 // «b();
15566 // c();
15567 // ˇ» d();
15568 }
15569 "});
15570
15571 // If a selection ends at the beginning of a line, that line is not toggled.
15572 cx.set_selections_state(indoc! {"
15573 fn a() {
15574 // b();
15575 // «c();
15576 ˇ»// d();
15577 }
15578 "});
15579
15580 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15581
15582 cx.assert_editor_state(indoc! {"
15583 fn a() {
15584 // b();
15585 «c();
15586 ˇ»// d();
15587 }
15588 "});
15589
15590 // If a selection span a single line and is empty, the line is toggled.
15591 cx.set_state(indoc! {"
15592 fn a() {
15593 a();
15594 b();
15595 ˇ
15596 }
15597 "});
15598
15599 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15600
15601 cx.assert_editor_state(indoc! {"
15602 fn a() {
15603 a();
15604 b();
15605 //ˇ
15606 }
15607 "});
15608
15609 // If a selection span multiple lines, empty lines are not toggled.
15610 cx.set_state(indoc! {"
15611 fn a() {
15612 «a();
15613
15614 c();ˇ»
15615 }
15616 "});
15617
15618 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15619
15620 cx.assert_editor_state(indoc! {"
15621 fn a() {
15622 // «a();
15623
15624 // c();ˇ»
15625 }
15626 "});
15627
15628 // If a selection includes multiple comment prefixes, all lines are uncommented.
15629 cx.set_state(indoc! {"
15630 fn a() {
15631 // «a();
15632 /// b();
15633 //! c();ˇ»
15634 }
15635 "});
15636
15637 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15638
15639 cx.assert_editor_state(indoc! {"
15640 fn a() {
15641 «a();
15642 b();
15643 c();ˇ»
15644 }
15645 "});
15646}
15647
15648#[gpui::test]
15649async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15650 init_test(cx, |_| {});
15651
15652 let language = Arc::new(Language::new(
15653 LanguageConfig {
15654 line_comments: vec!["// ".into()],
15655 ..Default::default()
15656 },
15657 Some(tree_sitter_rust::LANGUAGE.into()),
15658 ));
15659
15660 let mut cx = EditorTestContext::new(cx).await;
15661
15662 cx.language_registry().add(language.clone());
15663 cx.update_buffer(|buffer, cx| {
15664 buffer.set_language(Some(language), cx);
15665 });
15666
15667 let toggle_comments = &ToggleComments {
15668 advance_downwards: true,
15669 ignore_indent: false,
15670 };
15671
15672 // Single cursor on one line -> advance
15673 // Cursor moves horizontally 3 characters as well on non-blank line
15674 cx.set_state(indoc!(
15675 "fn a() {
15676 ˇdog();
15677 cat();
15678 }"
15679 ));
15680 cx.update_editor(|editor, window, cx| {
15681 editor.toggle_comments(toggle_comments, window, cx);
15682 });
15683 cx.assert_editor_state(indoc!(
15684 "fn a() {
15685 // dog();
15686 catˇ();
15687 }"
15688 ));
15689
15690 // Single selection on one line -> don't advance
15691 cx.set_state(indoc!(
15692 "fn a() {
15693 «dog()ˇ»;
15694 cat();
15695 }"
15696 ));
15697 cx.update_editor(|editor, window, cx| {
15698 editor.toggle_comments(toggle_comments, window, cx);
15699 });
15700 cx.assert_editor_state(indoc!(
15701 "fn a() {
15702 // «dog()ˇ»;
15703 cat();
15704 }"
15705 ));
15706
15707 // Multiple cursors on one line -> advance
15708 cx.set_state(indoc!(
15709 "fn a() {
15710 ˇdˇog();
15711 cat();
15712 }"
15713 ));
15714 cx.update_editor(|editor, window, cx| {
15715 editor.toggle_comments(toggle_comments, window, cx);
15716 });
15717 cx.assert_editor_state(indoc!(
15718 "fn a() {
15719 // dog();
15720 catˇ(ˇ);
15721 }"
15722 ));
15723
15724 // Multiple cursors on one line, with selection -> don't advance
15725 cx.set_state(indoc!(
15726 "fn a() {
15727 ˇdˇog«()ˇ»;
15728 cat();
15729 }"
15730 ));
15731 cx.update_editor(|editor, window, cx| {
15732 editor.toggle_comments(toggle_comments, window, cx);
15733 });
15734 cx.assert_editor_state(indoc!(
15735 "fn a() {
15736 // ˇdˇog«()ˇ»;
15737 cat();
15738 }"
15739 ));
15740
15741 // Single cursor on one line -> advance
15742 // Cursor moves to column 0 on blank line
15743 cx.set_state(indoc!(
15744 "fn a() {
15745 ˇdog();
15746
15747 cat();
15748 }"
15749 ));
15750 cx.update_editor(|editor, window, cx| {
15751 editor.toggle_comments(toggle_comments, window, cx);
15752 });
15753 cx.assert_editor_state(indoc!(
15754 "fn a() {
15755 // dog();
15756 ˇ
15757 cat();
15758 }"
15759 ));
15760
15761 // Single cursor on one line -> advance
15762 // Cursor starts and ends at column 0
15763 cx.set_state(indoc!(
15764 "fn a() {
15765 ˇ dog();
15766 cat();
15767 }"
15768 ));
15769 cx.update_editor(|editor, window, cx| {
15770 editor.toggle_comments(toggle_comments, window, cx);
15771 });
15772 cx.assert_editor_state(indoc!(
15773 "fn a() {
15774 // dog();
15775 ˇ cat();
15776 }"
15777 ));
15778}
15779
15780#[gpui::test]
15781async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15782 init_test(cx, |_| {});
15783
15784 let mut cx = EditorTestContext::new(cx).await;
15785
15786 let html_language = Arc::new(
15787 Language::new(
15788 LanguageConfig {
15789 name: "HTML".into(),
15790 block_comment: Some(BlockCommentConfig {
15791 start: "<!-- ".into(),
15792 prefix: "".into(),
15793 end: " -->".into(),
15794 tab_size: 0,
15795 }),
15796 ..Default::default()
15797 },
15798 Some(tree_sitter_html::LANGUAGE.into()),
15799 )
15800 .with_injection_query(
15801 r#"
15802 (script_element
15803 (raw_text) @injection.content
15804 (#set! injection.language "javascript"))
15805 "#,
15806 )
15807 .unwrap(),
15808 );
15809
15810 let javascript_language = Arc::new(Language::new(
15811 LanguageConfig {
15812 name: "JavaScript".into(),
15813 line_comments: vec!["// ".into()],
15814 ..Default::default()
15815 },
15816 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15817 ));
15818
15819 cx.language_registry().add(html_language.clone());
15820 cx.language_registry().add(javascript_language);
15821 cx.update_buffer(|buffer, cx| {
15822 buffer.set_language(Some(html_language), cx);
15823 });
15824
15825 // Toggle comments for empty selections
15826 cx.set_state(
15827 &r#"
15828 <p>A</p>ˇ
15829 <p>B</p>ˇ
15830 <p>C</p>ˇ
15831 "#
15832 .unindent(),
15833 );
15834 cx.update_editor(|editor, window, cx| {
15835 editor.toggle_comments(&ToggleComments::default(), window, cx)
15836 });
15837 cx.assert_editor_state(
15838 &r#"
15839 <!-- <p>A</p>ˇ -->
15840 <!-- <p>B</p>ˇ -->
15841 <!-- <p>C</p>ˇ -->
15842 "#
15843 .unindent(),
15844 );
15845 cx.update_editor(|editor, window, cx| {
15846 editor.toggle_comments(&ToggleComments::default(), window, cx)
15847 });
15848 cx.assert_editor_state(
15849 &r#"
15850 <p>A</p>ˇ
15851 <p>B</p>ˇ
15852 <p>C</p>ˇ
15853 "#
15854 .unindent(),
15855 );
15856
15857 // Toggle comments for mixture of empty and non-empty selections, where
15858 // multiple selections occupy a given line.
15859 cx.set_state(
15860 &r#"
15861 <p>A«</p>
15862 <p>ˇ»B</p>ˇ
15863 <p>C«</p>
15864 <p>ˇ»D</p>ˇ
15865 "#
15866 .unindent(),
15867 );
15868
15869 cx.update_editor(|editor, window, cx| {
15870 editor.toggle_comments(&ToggleComments::default(), window, cx)
15871 });
15872 cx.assert_editor_state(
15873 &r#"
15874 <!-- <p>A«</p>
15875 <p>ˇ»B</p>ˇ -->
15876 <!-- <p>C«</p>
15877 <p>ˇ»D</p>ˇ -->
15878 "#
15879 .unindent(),
15880 );
15881 cx.update_editor(|editor, window, cx| {
15882 editor.toggle_comments(&ToggleComments::default(), window, cx)
15883 });
15884 cx.assert_editor_state(
15885 &r#"
15886 <p>A«</p>
15887 <p>ˇ»B</p>ˇ
15888 <p>C«</p>
15889 <p>ˇ»D</p>ˇ
15890 "#
15891 .unindent(),
15892 );
15893
15894 // Toggle comments when different languages are active for different
15895 // selections.
15896 cx.set_state(
15897 &r#"
15898 ˇ<script>
15899 ˇvar x = new Y();
15900 ˇ</script>
15901 "#
15902 .unindent(),
15903 );
15904 cx.executor().run_until_parked();
15905 cx.update_editor(|editor, window, cx| {
15906 editor.toggle_comments(&ToggleComments::default(), window, cx)
15907 });
15908 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15909 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15910 cx.assert_editor_state(
15911 &r#"
15912 <!-- ˇ<script> -->
15913 // ˇvar x = new Y();
15914 <!-- ˇ</script> -->
15915 "#
15916 .unindent(),
15917 );
15918}
15919
15920#[gpui::test]
15921fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15922 init_test(cx, |_| {});
15923
15924 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15925 let multibuffer = cx.new(|cx| {
15926 let mut multibuffer = MultiBuffer::new(ReadWrite);
15927 multibuffer.push_excerpts(
15928 buffer.clone(),
15929 [
15930 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15931 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15932 ],
15933 cx,
15934 );
15935 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15936 multibuffer
15937 });
15938
15939 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15940 editor.update_in(cx, |editor, window, cx| {
15941 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15942 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15943 s.select_ranges([
15944 Point::new(0, 0)..Point::new(0, 0),
15945 Point::new(1, 0)..Point::new(1, 0),
15946 ])
15947 });
15948
15949 editor.handle_input("X", window, cx);
15950 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15951 assert_eq!(
15952 editor.selections.ranges(cx),
15953 [
15954 Point::new(0, 1)..Point::new(0, 1),
15955 Point::new(1, 1)..Point::new(1, 1),
15956 ]
15957 );
15958
15959 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15960 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15961 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15962 });
15963 editor.backspace(&Default::default(), window, cx);
15964 assert_eq!(editor.text(cx), "Xa\nbbb");
15965 assert_eq!(
15966 editor.selections.ranges(cx),
15967 [Point::new(1, 0)..Point::new(1, 0)]
15968 );
15969
15970 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15971 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15972 });
15973 editor.backspace(&Default::default(), window, cx);
15974 assert_eq!(editor.text(cx), "X\nbb");
15975 assert_eq!(
15976 editor.selections.ranges(cx),
15977 [Point::new(0, 1)..Point::new(0, 1)]
15978 );
15979 });
15980}
15981
15982#[gpui::test]
15983fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15984 init_test(cx, |_| {});
15985
15986 let markers = vec![('[', ']').into(), ('(', ')').into()];
15987 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15988 indoc! {"
15989 [aaaa
15990 (bbbb]
15991 cccc)",
15992 },
15993 markers.clone(),
15994 );
15995 let excerpt_ranges = markers.into_iter().map(|marker| {
15996 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15997 ExcerptRange::new(context)
15998 });
15999 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16000 let multibuffer = cx.new(|cx| {
16001 let mut multibuffer = MultiBuffer::new(ReadWrite);
16002 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16003 multibuffer
16004 });
16005
16006 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16007 editor.update_in(cx, |editor, window, cx| {
16008 let (expected_text, selection_ranges) = marked_text_ranges(
16009 indoc! {"
16010 aaaa
16011 bˇbbb
16012 bˇbbˇb
16013 cccc"
16014 },
16015 true,
16016 );
16017 assert_eq!(editor.text(cx), expected_text);
16018 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16019 s.select_ranges(selection_ranges)
16020 });
16021
16022 editor.handle_input("X", window, cx);
16023
16024 let (expected_text, expected_selections) = marked_text_ranges(
16025 indoc! {"
16026 aaaa
16027 bXˇbbXb
16028 bXˇbbXˇb
16029 cccc"
16030 },
16031 false,
16032 );
16033 assert_eq!(editor.text(cx), expected_text);
16034 assert_eq!(editor.selections.ranges(cx), expected_selections);
16035
16036 editor.newline(&Newline, window, cx);
16037 let (expected_text, expected_selections) = marked_text_ranges(
16038 indoc! {"
16039 aaaa
16040 bX
16041 ˇbbX
16042 b
16043 bX
16044 ˇbbX
16045 ˇb
16046 cccc"
16047 },
16048 false,
16049 );
16050 assert_eq!(editor.text(cx), expected_text);
16051 assert_eq!(editor.selections.ranges(cx), expected_selections);
16052 });
16053}
16054
16055#[gpui::test]
16056fn test_refresh_selections(cx: &mut TestAppContext) {
16057 init_test(cx, |_| {});
16058
16059 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16060 let mut excerpt1_id = None;
16061 let multibuffer = cx.new(|cx| {
16062 let mut multibuffer = MultiBuffer::new(ReadWrite);
16063 excerpt1_id = multibuffer
16064 .push_excerpts(
16065 buffer.clone(),
16066 [
16067 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16068 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16069 ],
16070 cx,
16071 )
16072 .into_iter()
16073 .next();
16074 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16075 multibuffer
16076 });
16077
16078 let editor = cx.add_window(|window, cx| {
16079 let mut editor = build_editor(multibuffer.clone(), window, cx);
16080 let snapshot = editor.snapshot(window, cx);
16081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16082 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16083 });
16084 editor.begin_selection(
16085 Point::new(2, 1).to_display_point(&snapshot),
16086 true,
16087 1,
16088 window,
16089 cx,
16090 );
16091 assert_eq!(
16092 editor.selections.ranges(cx),
16093 [
16094 Point::new(1, 3)..Point::new(1, 3),
16095 Point::new(2, 1)..Point::new(2, 1),
16096 ]
16097 );
16098 editor
16099 });
16100
16101 // Refreshing selections is a no-op when excerpts haven't changed.
16102 _ = editor.update(cx, |editor, window, cx| {
16103 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16104 assert_eq!(
16105 editor.selections.ranges(cx),
16106 [
16107 Point::new(1, 3)..Point::new(1, 3),
16108 Point::new(2, 1)..Point::new(2, 1),
16109 ]
16110 );
16111 });
16112
16113 multibuffer.update(cx, |multibuffer, cx| {
16114 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16115 });
16116 _ = editor.update(cx, |editor, window, cx| {
16117 // Removing an excerpt causes the first selection to become degenerate.
16118 assert_eq!(
16119 editor.selections.ranges(cx),
16120 [
16121 Point::new(0, 0)..Point::new(0, 0),
16122 Point::new(0, 1)..Point::new(0, 1)
16123 ]
16124 );
16125
16126 // Refreshing selections will relocate the first selection to the original buffer
16127 // location.
16128 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16129 assert_eq!(
16130 editor.selections.ranges(cx),
16131 [
16132 Point::new(0, 1)..Point::new(0, 1),
16133 Point::new(0, 3)..Point::new(0, 3)
16134 ]
16135 );
16136 assert!(editor.selections.pending_anchor().is_some());
16137 });
16138}
16139
16140#[gpui::test]
16141fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16142 init_test(cx, |_| {});
16143
16144 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16145 let mut excerpt1_id = None;
16146 let multibuffer = cx.new(|cx| {
16147 let mut multibuffer = MultiBuffer::new(ReadWrite);
16148 excerpt1_id = multibuffer
16149 .push_excerpts(
16150 buffer.clone(),
16151 [
16152 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16153 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16154 ],
16155 cx,
16156 )
16157 .into_iter()
16158 .next();
16159 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16160 multibuffer
16161 });
16162
16163 let editor = cx.add_window(|window, cx| {
16164 let mut editor = build_editor(multibuffer.clone(), window, cx);
16165 let snapshot = editor.snapshot(window, cx);
16166 editor.begin_selection(
16167 Point::new(1, 3).to_display_point(&snapshot),
16168 false,
16169 1,
16170 window,
16171 cx,
16172 );
16173 assert_eq!(
16174 editor.selections.ranges(cx),
16175 [Point::new(1, 3)..Point::new(1, 3)]
16176 );
16177 editor
16178 });
16179
16180 multibuffer.update(cx, |multibuffer, cx| {
16181 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16182 });
16183 _ = editor.update(cx, |editor, window, cx| {
16184 assert_eq!(
16185 editor.selections.ranges(cx),
16186 [Point::new(0, 0)..Point::new(0, 0)]
16187 );
16188
16189 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16191 assert_eq!(
16192 editor.selections.ranges(cx),
16193 [Point::new(0, 3)..Point::new(0, 3)]
16194 );
16195 assert!(editor.selections.pending_anchor().is_some());
16196 });
16197}
16198
16199#[gpui::test]
16200async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16201 init_test(cx, |_| {});
16202
16203 let language = Arc::new(
16204 Language::new(
16205 LanguageConfig {
16206 brackets: BracketPairConfig {
16207 pairs: vec![
16208 BracketPair {
16209 start: "{".to_string(),
16210 end: "}".to_string(),
16211 close: true,
16212 surround: true,
16213 newline: true,
16214 },
16215 BracketPair {
16216 start: "/* ".to_string(),
16217 end: " */".to_string(),
16218 close: true,
16219 surround: true,
16220 newline: true,
16221 },
16222 ],
16223 ..Default::default()
16224 },
16225 ..Default::default()
16226 },
16227 Some(tree_sitter_rust::LANGUAGE.into()),
16228 )
16229 .with_indents_query("")
16230 .unwrap(),
16231 );
16232
16233 let text = concat!(
16234 "{ }\n", //
16235 " x\n", //
16236 " /* */\n", //
16237 "x\n", //
16238 "{{} }\n", //
16239 );
16240
16241 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16242 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16243 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16244 editor
16245 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16246 .await;
16247
16248 editor.update_in(cx, |editor, window, cx| {
16249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16250 s.select_display_ranges([
16251 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16252 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16253 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16254 ])
16255 });
16256 editor.newline(&Newline, window, cx);
16257
16258 assert_eq!(
16259 editor.buffer().read(cx).read(cx).text(),
16260 concat!(
16261 "{ \n", // Suppress rustfmt
16262 "\n", //
16263 "}\n", //
16264 " x\n", //
16265 " /* \n", //
16266 " \n", //
16267 " */\n", //
16268 "x\n", //
16269 "{{} \n", //
16270 "}\n", //
16271 )
16272 );
16273 });
16274}
16275
16276#[gpui::test]
16277fn test_highlighted_ranges(cx: &mut TestAppContext) {
16278 init_test(cx, |_| {});
16279
16280 let editor = cx.add_window(|window, cx| {
16281 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16282 build_editor(buffer, window, cx)
16283 });
16284
16285 _ = editor.update(cx, |editor, window, cx| {
16286 struct Type1;
16287 struct Type2;
16288
16289 let buffer = editor.buffer.read(cx).snapshot(cx);
16290
16291 let anchor_range =
16292 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16293
16294 editor.highlight_background::<Type1>(
16295 &[
16296 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16297 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16298 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16299 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16300 ],
16301 |_| Hsla::red(),
16302 cx,
16303 );
16304 editor.highlight_background::<Type2>(
16305 &[
16306 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16307 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16308 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16309 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16310 ],
16311 |_| Hsla::green(),
16312 cx,
16313 );
16314
16315 let snapshot = editor.snapshot(window, cx);
16316 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16317 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16318 &snapshot,
16319 cx.theme(),
16320 );
16321 assert_eq!(
16322 highlighted_ranges,
16323 &[
16324 (
16325 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16326 Hsla::green(),
16327 ),
16328 (
16329 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16330 Hsla::red(),
16331 ),
16332 (
16333 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16334 Hsla::green(),
16335 ),
16336 (
16337 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16338 Hsla::red(),
16339 ),
16340 ]
16341 );
16342 assert_eq!(
16343 editor.sorted_background_highlights_in_range(
16344 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16345 &snapshot,
16346 cx.theme(),
16347 ),
16348 &[(
16349 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16350 Hsla::red(),
16351 )]
16352 );
16353 });
16354}
16355
16356#[gpui::test]
16357async fn test_following(cx: &mut TestAppContext) {
16358 init_test(cx, |_| {});
16359
16360 let fs = FakeFs::new(cx.executor());
16361 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16362
16363 let buffer = project.update(cx, |project, cx| {
16364 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16365 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16366 });
16367 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16368 let follower = cx.update(|cx| {
16369 cx.open_window(
16370 WindowOptions {
16371 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16372 gpui::Point::new(px(0.), px(0.)),
16373 gpui::Point::new(px(10.), px(80.)),
16374 ))),
16375 ..Default::default()
16376 },
16377 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16378 )
16379 .unwrap()
16380 });
16381
16382 let is_still_following = Rc::new(RefCell::new(true));
16383 let follower_edit_event_count = Rc::new(RefCell::new(0));
16384 let pending_update = Rc::new(RefCell::new(None));
16385 let leader_entity = leader.root(cx).unwrap();
16386 let follower_entity = follower.root(cx).unwrap();
16387 _ = follower.update(cx, {
16388 let update = pending_update.clone();
16389 let is_still_following = is_still_following.clone();
16390 let follower_edit_event_count = follower_edit_event_count.clone();
16391 |_, window, cx| {
16392 cx.subscribe_in(
16393 &leader_entity,
16394 window,
16395 move |_, leader, event, window, cx| {
16396 leader.read(cx).add_event_to_update_proto(
16397 event,
16398 &mut update.borrow_mut(),
16399 window,
16400 cx,
16401 );
16402 },
16403 )
16404 .detach();
16405
16406 cx.subscribe_in(
16407 &follower_entity,
16408 window,
16409 move |_, _, event: &EditorEvent, _window, _cx| {
16410 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16411 *is_still_following.borrow_mut() = false;
16412 }
16413
16414 if let EditorEvent::BufferEdited = event {
16415 *follower_edit_event_count.borrow_mut() += 1;
16416 }
16417 },
16418 )
16419 .detach();
16420 }
16421 });
16422
16423 // Update the selections only
16424 _ = leader.update(cx, |leader, window, cx| {
16425 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16426 s.select_ranges([1..1])
16427 });
16428 });
16429 follower
16430 .update(cx, |follower, window, cx| {
16431 follower.apply_update_proto(
16432 &project,
16433 pending_update.borrow_mut().take().unwrap(),
16434 window,
16435 cx,
16436 )
16437 })
16438 .unwrap()
16439 .await
16440 .unwrap();
16441 _ = follower.update(cx, |follower, _, cx| {
16442 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16443 });
16444 assert!(*is_still_following.borrow());
16445 assert_eq!(*follower_edit_event_count.borrow(), 0);
16446
16447 // Update the scroll position only
16448 _ = leader.update(cx, |leader, window, cx| {
16449 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16450 });
16451 follower
16452 .update(cx, |follower, window, cx| {
16453 follower.apply_update_proto(
16454 &project,
16455 pending_update.borrow_mut().take().unwrap(),
16456 window,
16457 cx,
16458 )
16459 })
16460 .unwrap()
16461 .await
16462 .unwrap();
16463 assert_eq!(
16464 follower
16465 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16466 .unwrap(),
16467 gpui::Point::new(1.5, 3.5)
16468 );
16469 assert!(*is_still_following.borrow());
16470 assert_eq!(*follower_edit_event_count.borrow(), 0);
16471
16472 // Update the selections and scroll position. The follower's scroll position is updated
16473 // via autoscroll, not via the leader's exact scroll position.
16474 _ = leader.update(cx, |leader, window, cx| {
16475 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16476 s.select_ranges([0..0])
16477 });
16478 leader.request_autoscroll(Autoscroll::newest(), cx);
16479 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16480 });
16481 follower
16482 .update(cx, |follower, window, cx| {
16483 follower.apply_update_proto(
16484 &project,
16485 pending_update.borrow_mut().take().unwrap(),
16486 window,
16487 cx,
16488 )
16489 })
16490 .unwrap()
16491 .await
16492 .unwrap();
16493 _ = follower.update(cx, |follower, _, cx| {
16494 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16495 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16496 });
16497 assert!(*is_still_following.borrow());
16498
16499 // Creating a pending selection that precedes another selection
16500 _ = leader.update(cx, |leader, window, cx| {
16501 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16502 s.select_ranges([1..1])
16503 });
16504 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16505 });
16506 follower
16507 .update(cx, |follower, window, cx| {
16508 follower.apply_update_proto(
16509 &project,
16510 pending_update.borrow_mut().take().unwrap(),
16511 window,
16512 cx,
16513 )
16514 })
16515 .unwrap()
16516 .await
16517 .unwrap();
16518 _ = follower.update(cx, |follower, _, cx| {
16519 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16520 });
16521 assert!(*is_still_following.borrow());
16522
16523 // Extend the pending selection so that it surrounds another selection
16524 _ = leader.update(cx, |leader, window, cx| {
16525 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16526 });
16527 follower
16528 .update(cx, |follower, window, cx| {
16529 follower.apply_update_proto(
16530 &project,
16531 pending_update.borrow_mut().take().unwrap(),
16532 window,
16533 cx,
16534 )
16535 })
16536 .unwrap()
16537 .await
16538 .unwrap();
16539 _ = follower.update(cx, |follower, _, cx| {
16540 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16541 });
16542
16543 // Scrolling locally breaks the follow
16544 _ = follower.update(cx, |follower, window, cx| {
16545 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16546 follower.set_scroll_anchor(
16547 ScrollAnchor {
16548 anchor: top_anchor,
16549 offset: gpui::Point::new(0.0, 0.5),
16550 },
16551 window,
16552 cx,
16553 );
16554 });
16555 assert!(!(*is_still_following.borrow()));
16556}
16557
16558#[gpui::test]
16559async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16560 init_test(cx, |_| {});
16561
16562 let fs = FakeFs::new(cx.executor());
16563 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16564 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16565 let pane = workspace
16566 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16567 .unwrap();
16568
16569 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16570
16571 let leader = pane.update_in(cx, |_, window, cx| {
16572 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16573 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16574 });
16575
16576 // Start following the editor when it has no excerpts.
16577 let mut state_message =
16578 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16579 let workspace_entity = workspace.root(cx).unwrap();
16580 let follower_1 = cx
16581 .update_window(*workspace.deref(), |_, window, cx| {
16582 Editor::from_state_proto(
16583 workspace_entity,
16584 ViewId {
16585 creator: CollaboratorId::PeerId(PeerId::default()),
16586 id: 0,
16587 },
16588 &mut state_message,
16589 window,
16590 cx,
16591 )
16592 })
16593 .unwrap()
16594 .unwrap()
16595 .await
16596 .unwrap();
16597
16598 let update_message = Rc::new(RefCell::new(None));
16599 follower_1.update_in(cx, {
16600 let update = update_message.clone();
16601 |_, window, cx| {
16602 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16603 leader.read(cx).add_event_to_update_proto(
16604 event,
16605 &mut update.borrow_mut(),
16606 window,
16607 cx,
16608 );
16609 })
16610 .detach();
16611 }
16612 });
16613
16614 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16615 (
16616 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16617 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16618 )
16619 });
16620
16621 // Insert some excerpts.
16622 leader.update(cx, |leader, cx| {
16623 leader.buffer.update(cx, |multibuffer, cx| {
16624 multibuffer.set_excerpts_for_path(
16625 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16626 buffer_1.clone(),
16627 vec![
16628 Point::row_range(0..3),
16629 Point::row_range(1..6),
16630 Point::row_range(12..15),
16631 ],
16632 0,
16633 cx,
16634 );
16635 multibuffer.set_excerpts_for_path(
16636 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16637 buffer_2.clone(),
16638 vec![Point::row_range(0..6), Point::row_range(8..12)],
16639 0,
16640 cx,
16641 );
16642 });
16643 });
16644
16645 // Apply the update of adding the excerpts.
16646 follower_1
16647 .update_in(cx, |follower, window, cx| {
16648 follower.apply_update_proto(
16649 &project,
16650 update_message.borrow().clone().unwrap(),
16651 window,
16652 cx,
16653 )
16654 })
16655 .await
16656 .unwrap();
16657 assert_eq!(
16658 follower_1.update(cx, |editor, cx| editor.text(cx)),
16659 leader.update(cx, |editor, cx| editor.text(cx))
16660 );
16661 update_message.borrow_mut().take();
16662
16663 // Start following separately after it already has excerpts.
16664 let mut state_message =
16665 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16666 let workspace_entity = workspace.root(cx).unwrap();
16667 let follower_2 = cx
16668 .update_window(*workspace.deref(), |_, window, cx| {
16669 Editor::from_state_proto(
16670 workspace_entity,
16671 ViewId {
16672 creator: CollaboratorId::PeerId(PeerId::default()),
16673 id: 0,
16674 },
16675 &mut state_message,
16676 window,
16677 cx,
16678 )
16679 })
16680 .unwrap()
16681 .unwrap()
16682 .await
16683 .unwrap();
16684 assert_eq!(
16685 follower_2.update(cx, |editor, cx| editor.text(cx)),
16686 leader.update(cx, |editor, cx| editor.text(cx))
16687 );
16688
16689 // Remove some excerpts.
16690 leader.update(cx, |leader, cx| {
16691 leader.buffer.update(cx, |multibuffer, cx| {
16692 let excerpt_ids = multibuffer.excerpt_ids();
16693 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16694 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16695 });
16696 });
16697
16698 // Apply the update of removing the excerpts.
16699 follower_1
16700 .update_in(cx, |follower, window, cx| {
16701 follower.apply_update_proto(
16702 &project,
16703 update_message.borrow().clone().unwrap(),
16704 window,
16705 cx,
16706 )
16707 })
16708 .await
16709 .unwrap();
16710 follower_2
16711 .update_in(cx, |follower, window, cx| {
16712 follower.apply_update_proto(
16713 &project,
16714 update_message.borrow().clone().unwrap(),
16715 window,
16716 cx,
16717 )
16718 })
16719 .await
16720 .unwrap();
16721 update_message.borrow_mut().take();
16722 assert_eq!(
16723 follower_1.update(cx, |editor, cx| editor.text(cx)),
16724 leader.update(cx, |editor, cx| editor.text(cx))
16725 );
16726}
16727
16728#[gpui::test]
16729async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16730 init_test(cx, |_| {});
16731
16732 let mut cx = EditorTestContext::new(cx).await;
16733 let lsp_store =
16734 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16735
16736 cx.set_state(indoc! {"
16737 ˇfn func(abc def: i32) -> u32 {
16738 }
16739 "});
16740
16741 cx.update(|_, cx| {
16742 lsp_store.update(cx, |lsp_store, cx| {
16743 lsp_store
16744 .update_diagnostics(
16745 LanguageServerId(0),
16746 lsp::PublishDiagnosticsParams {
16747 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16748 version: None,
16749 diagnostics: vec![
16750 lsp::Diagnostic {
16751 range: lsp::Range::new(
16752 lsp::Position::new(0, 11),
16753 lsp::Position::new(0, 12),
16754 ),
16755 severity: Some(lsp::DiagnosticSeverity::ERROR),
16756 ..Default::default()
16757 },
16758 lsp::Diagnostic {
16759 range: lsp::Range::new(
16760 lsp::Position::new(0, 12),
16761 lsp::Position::new(0, 15),
16762 ),
16763 severity: Some(lsp::DiagnosticSeverity::ERROR),
16764 ..Default::default()
16765 },
16766 lsp::Diagnostic {
16767 range: lsp::Range::new(
16768 lsp::Position::new(0, 25),
16769 lsp::Position::new(0, 28),
16770 ),
16771 severity: Some(lsp::DiagnosticSeverity::ERROR),
16772 ..Default::default()
16773 },
16774 ],
16775 },
16776 None,
16777 DiagnosticSourceKind::Pushed,
16778 &[],
16779 cx,
16780 )
16781 .unwrap()
16782 });
16783 });
16784
16785 executor.run_until_parked();
16786
16787 cx.update_editor(|editor, window, cx| {
16788 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16789 });
16790
16791 cx.assert_editor_state(indoc! {"
16792 fn func(abc def: i32) -> ˇu32 {
16793 }
16794 "});
16795
16796 cx.update_editor(|editor, window, cx| {
16797 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16798 });
16799
16800 cx.assert_editor_state(indoc! {"
16801 fn func(abc ˇdef: i32) -> u32 {
16802 }
16803 "});
16804
16805 cx.update_editor(|editor, window, cx| {
16806 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16807 });
16808
16809 cx.assert_editor_state(indoc! {"
16810 fn func(abcˇ def: i32) -> u32 {
16811 }
16812 "});
16813
16814 cx.update_editor(|editor, window, cx| {
16815 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16816 });
16817
16818 cx.assert_editor_state(indoc! {"
16819 fn func(abc def: i32) -> ˇu32 {
16820 }
16821 "});
16822}
16823
16824#[gpui::test]
16825async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16826 init_test(cx, |_| {});
16827
16828 let mut cx = EditorTestContext::new(cx).await;
16829
16830 let diff_base = r#"
16831 use some::mod;
16832
16833 const A: u32 = 42;
16834
16835 fn main() {
16836 println!("hello");
16837
16838 println!("world");
16839 }
16840 "#
16841 .unindent();
16842
16843 // Edits are modified, removed, modified, added
16844 cx.set_state(
16845 &r#"
16846 use some::modified;
16847
16848 ˇ
16849 fn main() {
16850 println!("hello there");
16851
16852 println!("around the");
16853 println!("world");
16854 }
16855 "#
16856 .unindent(),
16857 );
16858
16859 cx.set_head_text(&diff_base);
16860 executor.run_until_parked();
16861
16862 cx.update_editor(|editor, window, cx| {
16863 //Wrap around the bottom of the buffer
16864 for _ in 0..3 {
16865 editor.go_to_next_hunk(&GoToHunk, window, cx);
16866 }
16867 });
16868
16869 cx.assert_editor_state(
16870 &r#"
16871 ˇuse some::modified;
16872
16873
16874 fn main() {
16875 println!("hello there");
16876
16877 println!("around the");
16878 println!("world");
16879 }
16880 "#
16881 .unindent(),
16882 );
16883
16884 cx.update_editor(|editor, window, cx| {
16885 //Wrap around the top of the buffer
16886 for _ in 0..2 {
16887 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16888 }
16889 });
16890
16891 cx.assert_editor_state(
16892 &r#"
16893 use some::modified;
16894
16895
16896 fn main() {
16897 ˇ println!("hello there");
16898
16899 println!("around the");
16900 println!("world");
16901 }
16902 "#
16903 .unindent(),
16904 );
16905
16906 cx.update_editor(|editor, window, cx| {
16907 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16908 });
16909
16910 cx.assert_editor_state(
16911 &r#"
16912 use some::modified;
16913
16914 ˇ
16915 fn main() {
16916 println!("hello there");
16917
16918 println!("around the");
16919 println!("world");
16920 }
16921 "#
16922 .unindent(),
16923 );
16924
16925 cx.update_editor(|editor, window, cx| {
16926 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16927 });
16928
16929 cx.assert_editor_state(
16930 &r#"
16931 ˇuse some::modified;
16932
16933
16934 fn main() {
16935 println!("hello there");
16936
16937 println!("around the");
16938 println!("world");
16939 }
16940 "#
16941 .unindent(),
16942 );
16943
16944 cx.update_editor(|editor, window, cx| {
16945 for _ in 0..2 {
16946 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16947 }
16948 });
16949
16950 cx.assert_editor_state(
16951 &r#"
16952 use some::modified;
16953
16954
16955 fn main() {
16956 ˇ println!("hello there");
16957
16958 println!("around the");
16959 println!("world");
16960 }
16961 "#
16962 .unindent(),
16963 );
16964
16965 cx.update_editor(|editor, window, cx| {
16966 editor.fold(&Fold, window, cx);
16967 });
16968
16969 cx.update_editor(|editor, window, cx| {
16970 editor.go_to_next_hunk(&GoToHunk, window, cx);
16971 });
16972
16973 cx.assert_editor_state(
16974 &r#"
16975 ˇuse some::modified;
16976
16977
16978 fn main() {
16979 println!("hello there");
16980
16981 println!("around the");
16982 println!("world");
16983 }
16984 "#
16985 .unindent(),
16986 );
16987}
16988
16989#[test]
16990fn test_split_words() {
16991 fn split(text: &str) -> Vec<&str> {
16992 split_words(text).collect()
16993 }
16994
16995 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16996 assert_eq!(split("hello_world"), &["hello_", "world"]);
16997 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16998 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16999 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17000 assert_eq!(split("helloworld"), &["helloworld"]);
17001
17002 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17003}
17004
17005#[gpui::test]
17006async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17007 init_test(cx, |_| {});
17008
17009 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17010 let mut assert = |before, after| {
17011 let _state_context = cx.set_state(before);
17012 cx.run_until_parked();
17013 cx.update_editor(|editor, window, cx| {
17014 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17015 });
17016 cx.run_until_parked();
17017 cx.assert_editor_state(after);
17018 };
17019
17020 // Outside bracket jumps to outside of matching bracket
17021 assert("console.logˇ(var);", "console.log(var)ˇ;");
17022 assert("console.log(var)ˇ;", "console.logˇ(var);");
17023
17024 // Inside bracket jumps to inside of matching bracket
17025 assert("console.log(ˇvar);", "console.log(varˇ);");
17026 assert("console.log(varˇ);", "console.log(ˇvar);");
17027
17028 // When outside a bracket and inside, favor jumping to the inside bracket
17029 assert(
17030 "console.log('foo', [1, 2, 3]ˇ);",
17031 "console.log(ˇ'foo', [1, 2, 3]);",
17032 );
17033 assert(
17034 "console.log(ˇ'foo', [1, 2, 3]);",
17035 "console.log('foo', [1, 2, 3]ˇ);",
17036 );
17037
17038 // Bias forward if two options are equally likely
17039 assert(
17040 "let result = curried_fun()ˇ();",
17041 "let result = curried_fun()()ˇ;",
17042 );
17043
17044 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17045 assert(
17046 indoc! {"
17047 function test() {
17048 console.log('test')ˇ
17049 }"},
17050 indoc! {"
17051 function test() {
17052 console.logˇ('test')
17053 }"},
17054 );
17055}
17056
17057#[gpui::test]
17058async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17059 init_test(cx, |_| {});
17060
17061 let fs = FakeFs::new(cx.executor());
17062 fs.insert_tree(
17063 path!("/a"),
17064 json!({
17065 "main.rs": "fn main() { let a = 5; }",
17066 "other.rs": "// Test file",
17067 }),
17068 )
17069 .await;
17070 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17071
17072 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17073 language_registry.add(Arc::new(Language::new(
17074 LanguageConfig {
17075 name: "Rust".into(),
17076 matcher: LanguageMatcher {
17077 path_suffixes: vec!["rs".to_string()],
17078 ..Default::default()
17079 },
17080 brackets: BracketPairConfig {
17081 pairs: vec![BracketPair {
17082 start: "{".to_string(),
17083 end: "}".to_string(),
17084 close: true,
17085 surround: true,
17086 newline: true,
17087 }],
17088 disabled_scopes_by_bracket_ix: Vec::new(),
17089 },
17090 ..Default::default()
17091 },
17092 Some(tree_sitter_rust::LANGUAGE.into()),
17093 )));
17094 let mut fake_servers = language_registry.register_fake_lsp(
17095 "Rust",
17096 FakeLspAdapter {
17097 capabilities: lsp::ServerCapabilities {
17098 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17099 first_trigger_character: "{".to_string(),
17100 more_trigger_character: None,
17101 }),
17102 ..Default::default()
17103 },
17104 ..Default::default()
17105 },
17106 );
17107
17108 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17109
17110 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17111
17112 let worktree_id = workspace
17113 .update(cx, |workspace, _, cx| {
17114 workspace.project().update(cx, |project, cx| {
17115 project.worktrees(cx).next().unwrap().read(cx).id()
17116 })
17117 })
17118 .unwrap();
17119
17120 let buffer = project
17121 .update(cx, |project, cx| {
17122 project.open_local_buffer(path!("/a/main.rs"), cx)
17123 })
17124 .await
17125 .unwrap();
17126 let editor_handle = workspace
17127 .update(cx, |workspace, window, cx| {
17128 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17129 })
17130 .unwrap()
17131 .await
17132 .unwrap()
17133 .downcast::<Editor>()
17134 .unwrap();
17135
17136 cx.executor().start_waiting();
17137 let fake_server = fake_servers.next().await.unwrap();
17138
17139 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17140 |params, _| async move {
17141 assert_eq!(
17142 params.text_document_position.text_document.uri,
17143 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17144 );
17145 assert_eq!(
17146 params.text_document_position.position,
17147 lsp::Position::new(0, 21),
17148 );
17149
17150 Ok(Some(vec![lsp::TextEdit {
17151 new_text: "]".to_string(),
17152 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17153 }]))
17154 },
17155 );
17156
17157 editor_handle.update_in(cx, |editor, window, cx| {
17158 window.focus(&editor.focus_handle(cx));
17159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17160 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17161 });
17162 editor.handle_input("{", window, cx);
17163 });
17164
17165 cx.executor().run_until_parked();
17166
17167 buffer.update(cx, |buffer, _| {
17168 assert_eq!(
17169 buffer.text(),
17170 "fn main() { let a = {5}; }",
17171 "No extra braces from on type formatting should appear in the buffer"
17172 )
17173 });
17174}
17175
17176#[gpui::test(iterations = 20, seeds(31))]
17177async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17178 init_test(cx, |_| {});
17179
17180 let mut cx = EditorLspTestContext::new_rust(
17181 lsp::ServerCapabilities {
17182 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17183 first_trigger_character: ".".to_string(),
17184 more_trigger_character: None,
17185 }),
17186 ..Default::default()
17187 },
17188 cx,
17189 )
17190 .await;
17191
17192 cx.update_buffer(|buffer, _| {
17193 // This causes autoindent to be async.
17194 buffer.set_sync_parse_timeout(Duration::ZERO)
17195 });
17196
17197 cx.set_state("fn c() {\n d()ˇ\n}\n");
17198 cx.simulate_keystroke("\n");
17199 cx.run_until_parked();
17200
17201 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17202 let mut request =
17203 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17204 let buffer_cloned = buffer_cloned.clone();
17205 async move {
17206 buffer_cloned.update(&mut cx, |buffer, _| {
17207 assert_eq!(
17208 buffer.text(),
17209 "fn c() {\n d()\n .\n}\n",
17210 "OnTypeFormatting should triggered after autoindent applied"
17211 )
17212 })?;
17213
17214 Ok(Some(vec![]))
17215 }
17216 });
17217
17218 cx.simulate_keystroke(".");
17219 cx.run_until_parked();
17220
17221 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17222 assert!(request.next().await.is_some());
17223 request.close();
17224 assert!(request.next().await.is_none());
17225}
17226
17227#[gpui::test]
17228async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17229 init_test(cx, |_| {});
17230
17231 let fs = FakeFs::new(cx.executor());
17232 fs.insert_tree(
17233 path!("/a"),
17234 json!({
17235 "main.rs": "fn main() { let a = 5; }",
17236 "other.rs": "// Test file",
17237 }),
17238 )
17239 .await;
17240
17241 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17242
17243 let server_restarts = Arc::new(AtomicUsize::new(0));
17244 let closure_restarts = Arc::clone(&server_restarts);
17245 let language_server_name = "test language server";
17246 let language_name: LanguageName = "Rust".into();
17247
17248 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17249 language_registry.add(Arc::new(Language::new(
17250 LanguageConfig {
17251 name: language_name.clone(),
17252 matcher: LanguageMatcher {
17253 path_suffixes: vec!["rs".to_string()],
17254 ..Default::default()
17255 },
17256 ..Default::default()
17257 },
17258 Some(tree_sitter_rust::LANGUAGE.into()),
17259 )));
17260 let mut fake_servers = language_registry.register_fake_lsp(
17261 "Rust",
17262 FakeLspAdapter {
17263 name: language_server_name,
17264 initialization_options: Some(json!({
17265 "testOptionValue": true
17266 })),
17267 initializer: Some(Box::new(move |fake_server| {
17268 let task_restarts = Arc::clone(&closure_restarts);
17269 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17270 task_restarts.fetch_add(1, atomic::Ordering::Release);
17271 futures::future::ready(Ok(()))
17272 });
17273 })),
17274 ..Default::default()
17275 },
17276 );
17277
17278 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17279 let _buffer = project
17280 .update(cx, |project, cx| {
17281 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17282 })
17283 .await
17284 .unwrap();
17285 let _fake_server = fake_servers.next().await.unwrap();
17286 update_test_language_settings(cx, |language_settings| {
17287 language_settings.languages.0.insert(
17288 language_name.clone().0,
17289 LanguageSettingsContent {
17290 tab_size: NonZeroU32::new(8),
17291 ..Default::default()
17292 },
17293 );
17294 });
17295 cx.executor().run_until_parked();
17296 assert_eq!(
17297 server_restarts.load(atomic::Ordering::Acquire),
17298 0,
17299 "Should not restart LSP server on an unrelated change"
17300 );
17301
17302 update_test_project_settings(cx, |project_settings| {
17303 project_settings.lsp.insert(
17304 "Some other server name".into(),
17305 LspSettings {
17306 binary: None,
17307 settings: None,
17308 initialization_options: Some(json!({
17309 "some other init value": false
17310 })),
17311 enable_lsp_tasks: false,
17312 fetch: None,
17313 },
17314 );
17315 });
17316 cx.executor().run_until_parked();
17317 assert_eq!(
17318 server_restarts.load(atomic::Ordering::Acquire),
17319 0,
17320 "Should not restart LSP server on an unrelated LSP settings change"
17321 );
17322
17323 update_test_project_settings(cx, |project_settings| {
17324 project_settings.lsp.insert(
17325 language_server_name.into(),
17326 LspSettings {
17327 binary: None,
17328 settings: None,
17329 initialization_options: Some(json!({
17330 "anotherInitValue": false
17331 })),
17332 enable_lsp_tasks: false,
17333 fetch: None,
17334 },
17335 );
17336 });
17337 cx.executor().run_until_parked();
17338 assert_eq!(
17339 server_restarts.load(atomic::Ordering::Acquire),
17340 1,
17341 "Should restart LSP server on a related LSP settings change"
17342 );
17343
17344 update_test_project_settings(cx, |project_settings| {
17345 project_settings.lsp.insert(
17346 language_server_name.into(),
17347 LspSettings {
17348 binary: None,
17349 settings: None,
17350 initialization_options: Some(json!({
17351 "anotherInitValue": false
17352 })),
17353 enable_lsp_tasks: false,
17354 fetch: None,
17355 },
17356 );
17357 });
17358 cx.executor().run_until_parked();
17359 assert_eq!(
17360 server_restarts.load(atomic::Ordering::Acquire),
17361 1,
17362 "Should not restart LSP server on a related LSP settings change that is the same"
17363 );
17364
17365 update_test_project_settings(cx, |project_settings| {
17366 project_settings.lsp.insert(
17367 language_server_name.into(),
17368 LspSettings {
17369 binary: None,
17370 settings: None,
17371 initialization_options: None,
17372 enable_lsp_tasks: false,
17373 fetch: None,
17374 },
17375 );
17376 });
17377 cx.executor().run_until_parked();
17378 assert_eq!(
17379 server_restarts.load(atomic::Ordering::Acquire),
17380 2,
17381 "Should restart LSP server on another related LSP settings change"
17382 );
17383}
17384
17385#[gpui::test]
17386async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17387 init_test(cx, |_| {});
17388
17389 let mut cx = EditorLspTestContext::new_rust(
17390 lsp::ServerCapabilities {
17391 completion_provider: Some(lsp::CompletionOptions {
17392 trigger_characters: Some(vec![".".to_string()]),
17393 resolve_provider: Some(true),
17394 ..Default::default()
17395 }),
17396 ..Default::default()
17397 },
17398 cx,
17399 )
17400 .await;
17401
17402 cx.set_state("fn main() { let a = 2ˇ; }");
17403 cx.simulate_keystroke(".");
17404 let completion_item = lsp::CompletionItem {
17405 label: "some".into(),
17406 kind: Some(lsp::CompletionItemKind::SNIPPET),
17407 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17408 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17409 kind: lsp::MarkupKind::Markdown,
17410 value: "```rust\nSome(2)\n```".to_string(),
17411 })),
17412 deprecated: Some(false),
17413 sort_text: Some("fffffff2".to_string()),
17414 filter_text: Some("some".to_string()),
17415 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17416 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17417 range: lsp::Range {
17418 start: lsp::Position {
17419 line: 0,
17420 character: 22,
17421 },
17422 end: lsp::Position {
17423 line: 0,
17424 character: 22,
17425 },
17426 },
17427 new_text: "Some(2)".to_string(),
17428 })),
17429 additional_text_edits: Some(vec![lsp::TextEdit {
17430 range: lsp::Range {
17431 start: lsp::Position {
17432 line: 0,
17433 character: 20,
17434 },
17435 end: lsp::Position {
17436 line: 0,
17437 character: 22,
17438 },
17439 },
17440 new_text: "".to_string(),
17441 }]),
17442 ..Default::default()
17443 };
17444
17445 let closure_completion_item = completion_item.clone();
17446 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17447 let task_completion_item = closure_completion_item.clone();
17448 async move {
17449 Ok(Some(lsp::CompletionResponse::Array(vec![
17450 task_completion_item,
17451 ])))
17452 }
17453 });
17454
17455 request.next().await;
17456
17457 cx.condition(|editor, _| editor.context_menu_visible())
17458 .await;
17459 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17460 editor
17461 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17462 .unwrap()
17463 });
17464 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17465
17466 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17467 let task_completion_item = completion_item.clone();
17468 async move { Ok(task_completion_item) }
17469 })
17470 .next()
17471 .await
17472 .unwrap();
17473 apply_additional_edits.await.unwrap();
17474 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17475}
17476
17477#[gpui::test]
17478async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17479 init_test(cx, |_| {});
17480
17481 let mut cx = EditorLspTestContext::new_rust(
17482 lsp::ServerCapabilities {
17483 completion_provider: Some(lsp::CompletionOptions {
17484 trigger_characters: Some(vec![".".to_string()]),
17485 resolve_provider: Some(true),
17486 ..Default::default()
17487 }),
17488 ..Default::default()
17489 },
17490 cx,
17491 )
17492 .await;
17493
17494 cx.set_state("fn main() { let a = 2ˇ; }");
17495 cx.simulate_keystroke(".");
17496
17497 let item1 = lsp::CompletionItem {
17498 label: "method id()".to_string(),
17499 filter_text: Some("id".to_string()),
17500 detail: None,
17501 documentation: None,
17502 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17503 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17504 new_text: ".id".to_string(),
17505 })),
17506 ..lsp::CompletionItem::default()
17507 };
17508
17509 let item2 = lsp::CompletionItem {
17510 label: "other".to_string(),
17511 filter_text: Some("other".to_string()),
17512 detail: None,
17513 documentation: None,
17514 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17515 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17516 new_text: ".other".to_string(),
17517 })),
17518 ..lsp::CompletionItem::default()
17519 };
17520
17521 let item1 = item1.clone();
17522 cx.set_request_handler::<lsp::request::Completion, _, _>({
17523 let item1 = item1.clone();
17524 move |_, _, _| {
17525 let item1 = item1.clone();
17526 let item2 = item2.clone();
17527 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17528 }
17529 })
17530 .next()
17531 .await;
17532
17533 cx.condition(|editor, _| editor.context_menu_visible())
17534 .await;
17535 cx.update_editor(|editor, _, _| {
17536 let context_menu = editor.context_menu.borrow_mut();
17537 let context_menu = context_menu
17538 .as_ref()
17539 .expect("Should have the context menu deployed");
17540 match context_menu {
17541 CodeContextMenu::Completions(completions_menu) => {
17542 let completions = completions_menu.completions.borrow_mut();
17543 assert_eq!(
17544 completions
17545 .iter()
17546 .map(|completion| &completion.label.text)
17547 .collect::<Vec<_>>(),
17548 vec!["method id()", "other"]
17549 )
17550 }
17551 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17552 }
17553 });
17554
17555 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17556 let item1 = item1.clone();
17557 move |_, item_to_resolve, _| {
17558 let item1 = item1.clone();
17559 async move {
17560 if item1 == item_to_resolve {
17561 Ok(lsp::CompletionItem {
17562 label: "method id()".to_string(),
17563 filter_text: Some("id".to_string()),
17564 detail: Some("Now resolved!".to_string()),
17565 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17566 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17567 range: lsp::Range::new(
17568 lsp::Position::new(0, 22),
17569 lsp::Position::new(0, 22),
17570 ),
17571 new_text: ".id".to_string(),
17572 })),
17573 ..lsp::CompletionItem::default()
17574 })
17575 } else {
17576 Ok(item_to_resolve)
17577 }
17578 }
17579 }
17580 })
17581 .next()
17582 .await
17583 .unwrap();
17584 cx.run_until_parked();
17585
17586 cx.update_editor(|editor, window, cx| {
17587 editor.context_menu_next(&Default::default(), window, cx);
17588 });
17589
17590 cx.update_editor(|editor, _, _| {
17591 let context_menu = editor.context_menu.borrow_mut();
17592 let context_menu = context_menu
17593 .as_ref()
17594 .expect("Should have the context menu deployed");
17595 match context_menu {
17596 CodeContextMenu::Completions(completions_menu) => {
17597 let completions = completions_menu.completions.borrow_mut();
17598 assert_eq!(
17599 completions
17600 .iter()
17601 .map(|completion| &completion.label.text)
17602 .collect::<Vec<_>>(),
17603 vec!["method id() Now resolved!", "other"],
17604 "Should update first completion label, but not second as the filter text did not match."
17605 );
17606 }
17607 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17608 }
17609 });
17610}
17611
17612#[gpui::test]
17613async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17614 init_test(cx, |_| {});
17615 let mut cx = EditorLspTestContext::new_rust(
17616 lsp::ServerCapabilities {
17617 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17618 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17619 completion_provider: Some(lsp::CompletionOptions {
17620 resolve_provider: Some(true),
17621 ..Default::default()
17622 }),
17623 ..Default::default()
17624 },
17625 cx,
17626 )
17627 .await;
17628 cx.set_state(indoc! {"
17629 struct TestStruct {
17630 field: i32
17631 }
17632
17633 fn mainˇ() {
17634 let unused_var = 42;
17635 let test_struct = TestStruct { field: 42 };
17636 }
17637 "});
17638 let symbol_range = cx.lsp_range(indoc! {"
17639 struct TestStruct {
17640 field: i32
17641 }
17642
17643 «fn main»() {
17644 let unused_var = 42;
17645 let test_struct = TestStruct { field: 42 };
17646 }
17647 "});
17648 let mut hover_requests =
17649 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17650 Ok(Some(lsp::Hover {
17651 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17652 kind: lsp::MarkupKind::Markdown,
17653 value: "Function documentation".to_string(),
17654 }),
17655 range: Some(symbol_range),
17656 }))
17657 });
17658
17659 // Case 1: Test that code action menu hide hover popover
17660 cx.dispatch_action(Hover);
17661 hover_requests.next().await;
17662 cx.condition(|editor, _| editor.hover_state.visible()).await;
17663 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17664 move |_, _, _| async move {
17665 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17666 lsp::CodeAction {
17667 title: "Remove unused variable".to_string(),
17668 kind: Some(CodeActionKind::QUICKFIX),
17669 edit: Some(lsp::WorkspaceEdit {
17670 changes: Some(
17671 [(
17672 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17673 vec![lsp::TextEdit {
17674 range: lsp::Range::new(
17675 lsp::Position::new(5, 4),
17676 lsp::Position::new(5, 27),
17677 ),
17678 new_text: "".to_string(),
17679 }],
17680 )]
17681 .into_iter()
17682 .collect(),
17683 ),
17684 ..Default::default()
17685 }),
17686 ..Default::default()
17687 },
17688 )]))
17689 },
17690 );
17691 cx.update_editor(|editor, window, cx| {
17692 editor.toggle_code_actions(
17693 &ToggleCodeActions {
17694 deployed_from: None,
17695 quick_launch: false,
17696 },
17697 window,
17698 cx,
17699 );
17700 });
17701 code_action_requests.next().await;
17702 cx.run_until_parked();
17703 cx.condition(|editor, _| editor.context_menu_visible())
17704 .await;
17705 cx.update_editor(|editor, _, _| {
17706 assert!(
17707 !editor.hover_state.visible(),
17708 "Hover popover should be hidden when code action menu is shown"
17709 );
17710 // Hide code actions
17711 editor.context_menu.take();
17712 });
17713
17714 // Case 2: Test that code completions hide hover popover
17715 cx.dispatch_action(Hover);
17716 hover_requests.next().await;
17717 cx.condition(|editor, _| editor.hover_state.visible()).await;
17718 let counter = Arc::new(AtomicUsize::new(0));
17719 let mut completion_requests =
17720 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17721 let counter = counter.clone();
17722 async move {
17723 counter.fetch_add(1, atomic::Ordering::Release);
17724 Ok(Some(lsp::CompletionResponse::Array(vec![
17725 lsp::CompletionItem {
17726 label: "main".into(),
17727 kind: Some(lsp::CompletionItemKind::FUNCTION),
17728 detail: Some("() -> ()".to_string()),
17729 ..Default::default()
17730 },
17731 lsp::CompletionItem {
17732 label: "TestStruct".into(),
17733 kind: Some(lsp::CompletionItemKind::STRUCT),
17734 detail: Some("struct TestStruct".to_string()),
17735 ..Default::default()
17736 },
17737 ])))
17738 }
17739 });
17740 cx.update_editor(|editor, window, cx| {
17741 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17742 });
17743 completion_requests.next().await;
17744 cx.condition(|editor, _| editor.context_menu_visible())
17745 .await;
17746 cx.update_editor(|editor, _, _| {
17747 assert!(
17748 !editor.hover_state.visible(),
17749 "Hover popover should be hidden when completion menu is shown"
17750 );
17751 });
17752}
17753
17754#[gpui::test]
17755async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17756 init_test(cx, |_| {});
17757
17758 let mut cx = EditorLspTestContext::new_rust(
17759 lsp::ServerCapabilities {
17760 completion_provider: Some(lsp::CompletionOptions {
17761 trigger_characters: Some(vec![".".to_string()]),
17762 resolve_provider: Some(true),
17763 ..Default::default()
17764 }),
17765 ..Default::default()
17766 },
17767 cx,
17768 )
17769 .await;
17770
17771 cx.set_state("fn main() { let a = 2ˇ; }");
17772 cx.simulate_keystroke(".");
17773
17774 let unresolved_item_1 = lsp::CompletionItem {
17775 label: "id".to_string(),
17776 filter_text: Some("id".to_string()),
17777 detail: None,
17778 documentation: None,
17779 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17780 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17781 new_text: ".id".to_string(),
17782 })),
17783 ..lsp::CompletionItem::default()
17784 };
17785 let resolved_item_1 = lsp::CompletionItem {
17786 additional_text_edits: Some(vec![lsp::TextEdit {
17787 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17788 new_text: "!!".to_string(),
17789 }]),
17790 ..unresolved_item_1.clone()
17791 };
17792 let unresolved_item_2 = lsp::CompletionItem {
17793 label: "other".to_string(),
17794 filter_text: Some("other".to_string()),
17795 detail: None,
17796 documentation: None,
17797 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17798 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17799 new_text: ".other".to_string(),
17800 })),
17801 ..lsp::CompletionItem::default()
17802 };
17803 let resolved_item_2 = lsp::CompletionItem {
17804 additional_text_edits: Some(vec![lsp::TextEdit {
17805 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17806 new_text: "??".to_string(),
17807 }]),
17808 ..unresolved_item_2.clone()
17809 };
17810
17811 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17812 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17813 cx.lsp
17814 .server
17815 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17816 let unresolved_item_1 = unresolved_item_1.clone();
17817 let resolved_item_1 = resolved_item_1.clone();
17818 let unresolved_item_2 = unresolved_item_2.clone();
17819 let resolved_item_2 = resolved_item_2.clone();
17820 let resolve_requests_1 = resolve_requests_1.clone();
17821 let resolve_requests_2 = resolve_requests_2.clone();
17822 move |unresolved_request, _| {
17823 let unresolved_item_1 = unresolved_item_1.clone();
17824 let resolved_item_1 = resolved_item_1.clone();
17825 let unresolved_item_2 = unresolved_item_2.clone();
17826 let resolved_item_2 = resolved_item_2.clone();
17827 let resolve_requests_1 = resolve_requests_1.clone();
17828 let resolve_requests_2 = resolve_requests_2.clone();
17829 async move {
17830 if unresolved_request == unresolved_item_1 {
17831 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17832 Ok(resolved_item_1.clone())
17833 } else if unresolved_request == unresolved_item_2 {
17834 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17835 Ok(resolved_item_2.clone())
17836 } else {
17837 panic!("Unexpected completion item {unresolved_request:?}")
17838 }
17839 }
17840 }
17841 })
17842 .detach();
17843
17844 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17845 let unresolved_item_1 = unresolved_item_1.clone();
17846 let unresolved_item_2 = unresolved_item_2.clone();
17847 async move {
17848 Ok(Some(lsp::CompletionResponse::Array(vec![
17849 unresolved_item_1,
17850 unresolved_item_2,
17851 ])))
17852 }
17853 })
17854 .next()
17855 .await;
17856
17857 cx.condition(|editor, _| editor.context_menu_visible())
17858 .await;
17859 cx.update_editor(|editor, _, _| {
17860 let context_menu = editor.context_menu.borrow_mut();
17861 let context_menu = context_menu
17862 .as_ref()
17863 .expect("Should have the context menu deployed");
17864 match context_menu {
17865 CodeContextMenu::Completions(completions_menu) => {
17866 let completions = completions_menu.completions.borrow_mut();
17867 assert_eq!(
17868 completions
17869 .iter()
17870 .map(|completion| &completion.label.text)
17871 .collect::<Vec<_>>(),
17872 vec!["id", "other"]
17873 )
17874 }
17875 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17876 }
17877 });
17878 cx.run_until_parked();
17879
17880 cx.update_editor(|editor, window, cx| {
17881 editor.context_menu_next(&ContextMenuNext, window, cx);
17882 });
17883 cx.run_until_parked();
17884 cx.update_editor(|editor, window, cx| {
17885 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17886 });
17887 cx.run_until_parked();
17888 cx.update_editor(|editor, window, cx| {
17889 editor.context_menu_next(&ContextMenuNext, window, cx);
17890 });
17891 cx.run_until_parked();
17892 cx.update_editor(|editor, window, cx| {
17893 editor
17894 .compose_completion(&ComposeCompletion::default(), window, cx)
17895 .expect("No task returned")
17896 })
17897 .await
17898 .expect("Completion failed");
17899 cx.run_until_parked();
17900
17901 cx.update_editor(|editor, _, cx| {
17902 assert_eq!(
17903 resolve_requests_1.load(atomic::Ordering::Acquire),
17904 1,
17905 "Should always resolve once despite multiple selections"
17906 );
17907 assert_eq!(
17908 resolve_requests_2.load(atomic::Ordering::Acquire),
17909 1,
17910 "Should always resolve once after multiple selections and applying the completion"
17911 );
17912 assert_eq!(
17913 editor.text(cx),
17914 "fn main() { let a = ??.other; }",
17915 "Should use resolved data when applying the completion"
17916 );
17917 });
17918}
17919
17920#[gpui::test]
17921async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17922 init_test(cx, |_| {});
17923
17924 let item_0 = lsp::CompletionItem {
17925 label: "abs".into(),
17926 insert_text: Some("abs".into()),
17927 data: Some(json!({ "very": "special"})),
17928 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17929 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17930 lsp::InsertReplaceEdit {
17931 new_text: "abs".to_string(),
17932 insert: lsp::Range::default(),
17933 replace: lsp::Range::default(),
17934 },
17935 )),
17936 ..lsp::CompletionItem::default()
17937 };
17938 let items = iter::once(item_0.clone())
17939 .chain((11..51).map(|i| lsp::CompletionItem {
17940 label: format!("item_{}", i),
17941 insert_text: Some(format!("item_{}", i)),
17942 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17943 ..lsp::CompletionItem::default()
17944 }))
17945 .collect::<Vec<_>>();
17946
17947 let default_commit_characters = vec!["?".to_string()];
17948 let default_data = json!({ "default": "data"});
17949 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17950 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17951 let default_edit_range = lsp::Range {
17952 start: lsp::Position {
17953 line: 0,
17954 character: 5,
17955 },
17956 end: lsp::Position {
17957 line: 0,
17958 character: 5,
17959 },
17960 };
17961
17962 let mut cx = EditorLspTestContext::new_rust(
17963 lsp::ServerCapabilities {
17964 completion_provider: Some(lsp::CompletionOptions {
17965 trigger_characters: Some(vec![".".to_string()]),
17966 resolve_provider: Some(true),
17967 ..Default::default()
17968 }),
17969 ..Default::default()
17970 },
17971 cx,
17972 )
17973 .await;
17974
17975 cx.set_state("fn main() { let a = 2ˇ; }");
17976 cx.simulate_keystroke(".");
17977
17978 let completion_data = default_data.clone();
17979 let completion_characters = default_commit_characters.clone();
17980 let completion_items = items.clone();
17981 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17982 let default_data = completion_data.clone();
17983 let default_commit_characters = completion_characters.clone();
17984 let items = completion_items.clone();
17985 async move {
17986 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17987 items,
17988 item_defaults: Some(lsp::CompletionListItemDefaults {
17989 data: Some(default_data.clone()),
17990 commit_characters: Some(default_commit_characters.clone()),
17991 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17992 default_edit_range,
17993 )),
17994 insert_text_format: Some(default_insert_text_format),
17995 insert_text_mode: Some(default_insert_text_mode),
17996 }),
17997 ..lsp::CompletionList::default()
17998 })))
17999 }
18000 })
18001 .next()
18002 .await;
18003
18004 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18005 cx.lsp
18006 .server
18007 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18008 let closure_resolved_items = resolved_items.clone();
18009 move |item_to_resolve, _| {
18010 let closure_resolved_items = closure_resolved_items.clone();
18011 async move {
18012 closure_resolved_items.lock().push(item_to_resolve.clone());
18013 Ok(item_to_resolve)
18014 }
18015 }
18016 })
18017 .detach();
18018
18019 cx.condition(|editor, _| editor.context_menu_visible())
18020 .await;
18021 cx.run_until_parked();
18022 cx.update_editor(|editor, _, _| {
18023 let menu = editor.context_menu.borrow_mut();
18024 match menu.as_ref().expect("should have the completions menu") {
18025 CodeContextMenu::Completions(completions_menu) => {
18026 assert_eq!(
18027 completions_menu
18028 .entries
18029 .borrow()
18030 .iter()
18031 .map(|mat| mat.string.clone())
18032 .collect::<Vec<String>>(),
18033 items
18034 .iter()
18035 .map(|completion| completion.label.clone())
18036 .collect::<Vec<String>>()
18037 );
18038 }
18039 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18040 }
18041 });
18042 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18043 // with 4 from the end.
18044 assert_eq!(
18045 *resolved_items.lock(),
18046 [&items[0..16], &items[items.len() - 4..items.len()]]
18047 .concat()
18048 .iter()
18049 .cloned()
18050 .map(|mut item| {
18051 if item.data.is_none() {
18052 item.data = Some(default_data.clone());
18053 }
18054 item
18055 })
18056 .collect::<Vec<lsp::CompletionItem>>(),
18057 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18058 );
18059 resolved_items.lock().clear();
18060
18061 cx.update_editor(|editor, window, cx| {
18062 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18063 });
18064 cx.run_until_parked();
18065 // Completions that have already been resolved are skipped.
18066 assert_eq!(
18067 *resolved_items.lock(),
18068 items[items.len() - 17..items.len() - 4]
18069 .iter()
18070 .cloned()
18071 .map(|mut item| {
18072 if item.data.is_none() {
18073 item.data = Some(default_data.clone());
18074 }
18075 item
18076 })
18077 .collect::<Vec<lsp::CompletionItem>>()
18078 );
18079 resolved_items.lock().clear();
18080}
18081
18082#[gpui::test]
18083async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18084 init_test(cx, |_| {});
18085
18086 let mut cx = EditorLspTestContext::new(
18087 Language::new(
18088 LanguageConfig {
18089 matcher: LanguageMatcher {
18090 path_suffixes: vec!["jsx".into()],
18091 ..Default::default()
18092 },
18093 overrides: [(
18094 "element".into(),
18095 LanguageConfigOverride {
18096 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18097 ..Default::default()
18098 },
18099 )]
18100 .into_iter()
18101 .collect(),
18102 ..Default::default()
18103 },
18104 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18105 )
18106 .with_override_query("(jsx_self_closing_element) @element")
18107 .unwrap(),
18108 lsp::ServerCapabilities {
18109 completion_provider: Some(lsp::CompletionOptions {
18110 trigger_characters: Some(vec![":".to_string()]),
18111 ..Default::default()
18112 }),
18113 ..Default::default()
18114 },
18115 cx,
18116 )
18117 .await;
18118
18119 cx.lsp
18120 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18121 Ok(Some(lsp::CompletionResponse::Array(vec![
18122 lsp::CompletionItem {
18123 label: "bg-blue".into(),
18124 ..Default::default()
18125 },
18126 lsp::CompletionItem {
18127 label: "bg-red".into(),
18128 ..Default::default()
18129 },
18130 lsp::CompletionItem {
18131 label: "bg-yellow".into(),
18132 ..Default::default()
18133 },
18134 ])))
18135 });
18136
18137 cx.set_state(r#"<p class="bgˇ" />"#);
18138
18139 // Trigger completion when typing a dash, because the dash is an extra
18140 // word character in the 'element' scope, which contains the cursor.
18141 cx.simulate_keystroke("-");
18142 cx.executor().run_until_parked();
18143 cx.update_editor(|editor, _, _| {
18144 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18145 {
18146 assert_eq!(
18147 completion_menu_entries(menu),
18148 &["bg-blue", "bg-red", "bg-yellow"]
18149 );
18150 } else {
18151 panic!("expected completion menu to be open");
18152 }
18153 });
18154
18155 cx.simulate_keystroke("l");
18156 cx.executor().run_until_parked();
18157 cx.update_editor(|editor, _, _| {
18158 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18159 {
18160 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18161 } else {
18162 panic!("expected completion menu to be open");
18163 }
18164 });
18165
18166 // When filtering completions, consider the character after the '-' to
18167 // be the start of a subword.
18168 cx.set_state(r#"<p class="yelˇ" />"#);
18169 cx.simulate_keystroke("l");
18170 cx.executor().run_until_parked();
18171 cx.update_editor(|editor, _, _| {
18172 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18173 {
18174 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18175 } else {
18176 panic!("expected completion menu to be open");
18177 }
18178 });
18179}
18180
18181fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18182 let entries = menu.entries.borrow();
18183 entries.iter().map(|mat| mat.string.clone()).collect()
18184}
18185
18186#[gpui::test]
18187async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18188 init_test(cx, |settings| {
18189 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18190 });
18191
18192 let fs = FakeFs::new(cx.executor());
18193 fs.insert_file(path!("/file.ts"), Default::default()).await;
18194
18195 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18196 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18197
18198 language_registry.add(Arc::new(Language::new(
18199 LanguageConfig {
18200 name: "TypeScript".into(),
18201 matcher: LanguageMatcher {
18202 path_suffixes: vec!["ts".to_string()],
18203 ..Default::default()
18204 },
18205 ..Default::default()
18206 },
18207 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18208 )));
18209 update_test_language_settings(cx, |settings| {
18210 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18211 });
18212
18213 let test_plugin = "test_plugin";
18214 let _ = language_registry.register_fake_lsp(
18215 "TypeScript",
18216 FakeLspAdapter {
18217 prettier_plugins: vec![test_plugin],
18218 ..Default::default()
18219 },
18220 );
18221
18222 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18223 let buffer = project
18224 .update(cx, |project, cx| {
18225 project.open_local_buffer(path!("/file.ts"), cx)
18226 })
18227 .await
18228 .unwrap();
18229
18230 let buffer_text = "one\ntwo\nthree\n";
18231 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18232 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18233 editor.update_in(cx, |editor, window, cx| {
18234 editor.set_text(buffer_text, window, cx)
18235 });
18236
18237 editor
18238 .update_in(cx, |editor, window, cx| {
18239 editor.perform_format(
18240 project.clone(),
18241 FormatTrigger::Manual,
18242 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18243 window,
18244 cx,
18245 )
18246 })
18247 .unwrap()
18248 .await;
18249 assert_eq!(
18250 editor.update(cx, |editor, cx| editor.text(cx)),
18251 buffer_text.to_string() + prettier_format_suffix,
18252 "Test prettier formatting was not applied to the original buffer text",
18253 );
18254
18255 update_test_language_settings(cx, |settings| {
18256 settings.defaults.formatter = Some(FormatterList::default())
18257 });
18258 let format = editor.update_in(cx, |editor, window, cx| {
18259 editor.perform_format(
18260 project.clone(),
18261 FormatTrigger::Manual,
18262 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18263 window,
18264 cx,
18265 )
18266 });
18267 format.await.unwrap();
18268 assert_eq!(
18269 editor.update(cx, |editor, cx| editor.text(cx)),
18270 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18271 "Autoformatting (via test prettier) was not applied to the original buffer text",
18272 );
18273}
18274
18275#[gpui::test]
18276async fn test_addition_reverts(cx: &mut TestAppContext) {
18277 init_test(cx, |_| {});
18278 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18279 let base_text = indoc! {r#"
18280 struct Row;
18281 struct Row1;
18282 struct Row2;
18283
18284 struct Row4;
18285 struct Row5;
18286 struct Row6;
18287
18288 struct Row8;
18289 struct Row9;
18290 struct Row10;"#};
18291
18292 // When addition hunks are not adjacent to carets, no hunk revert is performed
18293 assert_hunk_revert(
18294 indoc! {r#"struct Row;
18295 struct Row1;
18296 struct Row1.1;
18297 struct Row1.2;
18298 struct Row2;ˇ
18299
18300 struct Row4;
18301 struct Row5;
18302 struct Row6;
18303
18304 struct Row8;
18305 ˇstruct Row9;
18306 struct Row9.1;
18307 struct Row9.2;
18308 struct Row9.3;
18309 struct Row10;"#},
18310 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18311 indoc! {r#"struct Row;
18312 struct Row1;
18313 struct Row1.1;
18314 struct Row1.2;
18315 struct Row2;ˇ
18316
18317 struct Row4;
18318 struct Row5;
18319 struct Row6;
18320
18321 struct Row8;
18322 ˇstruct Row9;
18323 struct Row9.1;
18324 struct Row9.2;
18325 struct Row9.3;
18326 struct Row10;"#},
18327 base_text,
18328 &mut cx,
18329 );
18330 // Same for selections
18331 assert_hunk_revert(
18332 indoc! {r#"struct Row;
18333 struct Row1;
18334 struct Row2;
18335 struct Row2.1;
18336 struct Row2.2;
18337 «ˇ
18338 struct Row4;
18339 struct» Row5;
18340 «struct Row6;
18341 ˇ»
18342 struct Row9.1;
18343 struct Row9.2;
18344 struct Row9.3;
18345 struct Row8;
18346 struct Row9;
18347 struct Row10;"#},
18348 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18349 indoc! {r#"struct Row;
18350 struct Row1;
18351 struct Row2;
18352 struct Row2.1;
18353 struct Row2.2;
18354 «ˇ
18355 struct Row4;
18356 struct» Row5;
18357 «struct Row6;
18358 ˇ»
18359 struct Row9.1;
18360 struct Row9.2;
18361 struct Row9.3;
18362 struct Row8;
18363 struct Row9;
18364 struct Row10;"#},
18365 base_text,
18366 &mut cx,
18367 );
18368
18369 // When carets and selections intersect the addition hunks, those are reverted.
18370 // Adjacent carets got merged.
18371 assert_hunk_revert(
18372 indoc! {r#"struct Row;
18373 ˇ// something on the top
18374 struct Row1;
18375 struct Row2;
18376 struct Roˇw3.1;
18377 struct Row2.2;
18378 struct Row2.3;ˇ
18379
18380 struct Row4;
18381 struct ˇRow5.1;
18382 struct Row5.2;
18383 struct «Rowˇ»5.3;
18384 struct Row5;
18385 struct Row6;
18386 ˇ
18387 struct Row9.1;
18388 struct «Rowˇ»9.2;
18389 struct «ˇRow»9.3;
18390 struct Row8;
18391 struct Row9;
18392 «ˇ// something on bottom»
18393 struct Row10;"#},
18394 vec![
18395 DiffHunkStatusKind::Added,
18396 DiffHunkStatusKind::Added,
18397 DiffHunkStatusKind::Added,
18398 DiffHunkStatusKind::Added,
18399 DiffHunkStatusKind::Added,
18400 ],
18401 indoc! {r#"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 base_text,
18413 &mut cx,
18414 );
18415}
18416
18417#[gpui::test]
18418async fn test_modification_reverts(cx: &mut TestAppContext) {
18419 init_test(cx, |_| {});
18420 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18421 let base_text = indoc! {r#"
18422 struct Row;
18423 struct Row1;
18424 struct Row2;
18425
18426 struct Row4;
18427 struct Row5;
18428 struct Row6;
18429
18430 struct Row8;
18431 struct Row9;
18432 struct Row10;"#};
18433
18434 // Modification hunks behave the same as the addition ones.
18435 assert_hunk_revert(
18436 indoc! {r#"struct Row;
18437 struct Row1;
18438 struct Row33;
18439 ˇ
18440 struct Row4;
18441 struct Row5;
18442 struct Row6;
18443 ˇ
18444 struct Row99;
18445 struct Row9;
18446 struct Row10;"#},
18447 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18448 indoc! {r#"struct Row;
18449 struct Row1;
18450 struct Row33;
18451 ˇ
18452 struct Row4;
18453 struct Row5;
18454 struct Row6;
18455 ˇ
18456 struct Row99;
18457 struct Row9;
18458 struct Row10;"#},
18459 base_text,
18460 &mut cx,
18461 );
18462 assert_hunk_revert(
18463 indoc! {r#"struct Row;
18464 struct Row1;
18465 struct Row33;
18466 «ˇ
18467 struct Row4;
18468 struct» Row5;
18469 «struct Row6;
18470 ˇ»
18471 struct Row99;
18472 struct Row9;
18473 struct Row10;"#},
18474 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18475 indoc! {r#"struct Row;
18476 struct Row1;
18477 struct Row33;
18478 «ˇ
18479 struct Row4;
18480 struct» Row5;
18481 «struct Row6;
18482 ˇ»
18483 struct Row99;
18484 struct Row9;
18485 struct Row10;"#},
18486 base_text,
18487 &mut cx,
18488 );
18489
18490 assert_hunk_revert(
18491 indoc! {r#"ˇstruct Row1.1;
18492 struct Row1;
18493 «ˇstr»uct Row22;
18494
18495 struct ˇRow44;
18496 struct Row5;
18497 struct «Rˇ»ow66;ˇ
18498
18499 «struˇ»ct Row88;
18500 struct Row9;
18501 struct Row1011;ˇ"#},
18502 vec![
18503 DiffHunkStatusKind::Modified,
18504 DiffHunkStatusKind::Modified,
18505 DiffHunkStatusKind::Modified,
18506 DiffHunkStatusKind::Modified,
18507 DiffHunkStatusKind::Modified,
18508 DiffHunkStatusKind::Modified,
18509 ],
18510 indoc! {r#"struct Row;
18511 ˇstruct Row1;
18512 struct Row2;
18513 ˇ
18514 struct Row4;
18515 ˇstruct Row5;
18516 struct Row6;
18517 ˇ
18518 struct Row8;
18519 ˇstruct Row9;
18520 struct Row10;ˇ"#},
18521 base_text,
18522 &mut cx,
18523 );
18524}
18525
18526#[gpui::test]
18527async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18528 init_test(cx, |_| {});
18529 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18530 let base_text = indoc! {r#"
18531 one
18532
18533 two
18534 three
18535 "#};
18536
18537 cx.set_head_text(base_text);
18538 cx.set_state("\nˇ\n");
18539 cx.executor().run_until_parked();
18540 cx.update_editor(|editor, _window, cx| {
18541 editor.expand_selected_diff_hunks(cx);
18542 });
18543 cx.executor().run_until_parked();
18544 cx.update_editor(|editor, window, cx| {
18545 editor.backspace(&Default::default(), window, cx);
18546 });
18547 cx.run_until_parked();
18548 cx.assert_state_with_diff(
18549 indoc! {r#"
18550
18551 - two
18552 - threeˇ
18553 +
18554 "#}
18555 .to_string(),
18556 );
18557}
18558
18559#[gpui::test]
18560async fn test_deletion_reverts(cx: &mut TestAppContext) {
18561 init_test(cx, |_| {});
18562 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18563 let base_text = indoc! {r#"struct Row;
18564struct Row1;
18565struct Row2;
18566
18567struct Row4;
18568struct Row5;
18569struct Row6;
18570
18571struct Row8;
18572struct Row9;
18573struct Row10;"#};
18574
18575 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18576 assert_hunk_revert(
18577 indoc! {r#"struct Row;
18578 struct Row2;
18579
18580 ˇstruct Row4;
18581 struct Row5;
18582 struct Row6;
18583 ˇ
18584 struct Row8;
18585 struct Row10;"#},
18586 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18587 indoc! {r#"struct Row;
18588 struct Row2;
18589
18590 ˇstruct Row4;
18591 struct Row5;
18592 struct Row6;
18593 ˇ
18594 struct Row8;
18595 struct Row10;"#},
18596 base_text,
18597 &mut cx,
18598 );
18599 assert_hunk_revert(
18600 indoc! {r#"struct Row;
18601 struct Row2;
18602
18603 «ˇstruct Row4;
18604 struct» Row5;
18605 «struct Row6;
18606 ˇ»
18607 struct Row8;
18608 struct Row10;"#},
18609 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18610 indoc! {r#"struct Row;
18611 struct Row2;
18612
18613 «ˇstruct Row4;
18614 struct» Row5;
18615 «struct Row6;
18616 ˇ»
18617 struct Row8;
18618 struct Row10;"#},
18619 base_text,
18620 &mut cx,
18621 );
18622
18623 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18624 assert_hunk_revert(
18625 indoc! {r#"struct Row;
18626 ˇstruct Row2;
18627
18628 struct Row4;
18629 struct Row5;
18630 struct Row6;
18631
18632 struct Row8;ˇ
18633 struct Row10;"#},
18634 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18635 indoc! {r#"struct Row;
18636 struct Row1;
18637 ˇstruct Row2;
18638
18639 struct Row4;
18640 struct Row5;
18641 struct Row6;
18642
18643 struct Row8;ˇ
18644 struct Row9;
18645 struct Row10;"#},
18646 base_text,
18647 &mut cx,
18648 );
18649 assert_hunk_revert(
18650 indoc! {r#"struct Row;
18651 struct Row2«ˇ;
18652 struct Row4;
18653 struct» Row5;
18654 «struct Row6;
18655
18656 struct Row8;ˇ»
18657 struct Row10;"#},
18658 vec![
18659 DiffHunkStatusKind::Deleted,
18660 DiffHunkStatusKind::Deleted,
18661 DiffHunkStatusKind::Deleted,
18662 ],
18663 indoc! {r#"struct Row;
18664 struct Row1;
18665 struct Row2«ˇ;
18666
18667 struct Row4;
18668 struct» Row5;
18669 «struct Row6;
18670
18671 struct Row8;ˇ»
18672 struct Row9;
18673 struct Row10;"#},
18674 base_text,
18675 &mut cx,
18676 );
18677}
18678
18679#[gpui::test]
18680async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18681 init_test(cx, |_| {});
18682
18683 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18684 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18685 let base_text_3 =
18686 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18687
18688 let text_1 = edit_first_char_of_every_line(base_text_1);
18689 let text_2 = edit_first_char_of_every_line(base_text_2);
18690 let text_3 = edit_first_char_of_every_line(base_text_3);
18691
18692 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18693 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18694 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18695
18696 let multibuffer = cx.new(|cx| {
18697 let mut multibuffer = MultiBuffer::new(ReadWrite);
18698 multibuffer.push_excerpts(
18699 buffer_1.clone(),
18700 [
18701 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18702 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18703 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18704 ],
18705 cx,
18706 );
18707 multibuffer.push_excerpts(
18708 buffer_2.clone(),
18709 [
18710 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18711 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18712 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18713 ],
18714 cx,
18715 );
18716 multibuffer.push_excerpts(
18717 buffer_3.clone(),
18718 [
18719 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18720 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18721 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18722 ],
18723 cx,
18724 );
18725 multibuffer
18726 });
18727
18728 let fs = FakeFs::new(cx.executor());
18729 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18730 let (editor, cx) = cx
18731 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18732 editor.update_in(cx, |editor, _window, cx| {
18733 for (buffer, diff_base) in [
18734 (buffer_1.clone(), base_text_1),
18735 (buffer_2.clone(), base_text_2),
18736 (buffer_3.clone(), base_text_3),
18737 ] {
18738 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18739 editor
18740 .buffer
18741 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18742 }
18743 });
18744 cx.executor().run_until_parked();
18745
18746 editor.update_in(cx, |editor, window, cx| {
18747 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}");
18748 editor.select_all(&SelectAll, window, cx);
18749 editor.git_restore(&Default::default(), window, cx);
18750 });
18751 cx.executor().run_until_parked();
18752
18753 // When all ranges are selected, all buffer hunks are reverted.
18754 editor.update(cx, |editor, cx| {
18755 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");
18756 });
18757 buffer_1.update(cx, |buffer, _| {
18758 assert_eq!(buffer.text(), base_text_1);
18759 });
18760 buffer_2.update(cx, |buffer, _| {
18761 assert_eq!(buffer.text(), base_text_2);
18762 });
18763 buffer_3.update(cx, |buffer, _| {
18764 assert_eq!(buffer.text(), base_text_3);
18765 });
18766
18767 editor.update_in(cx, |editor, window, cx| {
18768 editor.undo(&Default::default(), window, cx);
18769 });
18770
18771 editor.update_in(cx, |editor, window, cx| {
18772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18773 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18774 });
18775 editor.git_restore(&Default::default(), window, cx);
18776 });
18777
18778 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18779 // but not affect buffer_2 and its related excerpts.
18780 editor.update(cx, |editor, cx| {
18781 assert_eq!(
18782 editor.text(cx),
18783 "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}"
18784 );
18785 });
18786 buffer_1.update(cx, |buffer, _| {
18787 assert_eq!(buffer.text(), base_text_1);
18788 });
18789 buffer_2.update(cx, |buffer, _| {
18790 assert_eq!(
18791 buffer.text(),
18792 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18793 );
18794 });
18795 buffer_3.update(cx, |buffer, _| {
18796 assert_eq!(
18797 buffer.text(),
18798 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18799 );
18800 });
18801
18802 fn edit_first_char_of_every_line(text: &str) -> String {
18803 text.split('\n')
18804 .map(|line| format!("X{}", &line[1..]))
18805 .collect::<Vec<_>>()
18806 .join("\n")
18807 }
18808}
18809
18810#[gpui::test]
18811async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18812 init_test(cx, |_| {});
18813
18814 let cols = 4;
18815 let rows = 10;
18816 let sample_text_1 = sample_text(rows, cols, 'a');
18817 assert_eq!(
18818 sample_text_1,
18819 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18820 );
18821 let sample_text_2 = sample_text(rows, cols, 'l');
18822 assert_eq!(
18823 sample_text_2,
18824 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18825 );
18826 let sample_text_3 = sample_text(rows, cols, 'v');
18827 assert_eq!(
18828 sample_text_3,
18829 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18830 );
18831
18832 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18833 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18834 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18835
18836 let multi_buffer = cx.new(|cx| {
18837 let mut multibuffer = MultiBuffer::new(ReadWrite);
18838 multibuffer.push_excerpts(
18839 buffer_1.clone(),
18840 [
18841 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18842 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18843 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18844 ],
18845 cx,
18846 );
18847 multibuffer.push_excerpts(
18848 buffer_2.clone(),
18849 [
18850 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18851 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18852 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18853 ],
18854 cx,
18855 );
18856 multibuffer.push_excerpts(
18857 buffer_3.clone(),
18858 [
18859 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18860 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18861 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18862 ],
18863 cx,
18864 );
18865 multibuffer
18866 });
18867
18868 let fs = FakeFs::new(cx.executor());
18869 fs.insert_tree(
18870 "/a",
18871 json!({
18872 "main.rs": sample_text_1,
18873 "other.rs": sample_text_2,
18874 "lib.rs": sample_text_3,
18875 }),
18876 )
18877 .await;
18878 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18879 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18880 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18881 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18882 Editor::new(
18883 EditorMode::full(),
18884 multi_buffer,
18885 Some(project.clone()),
18886 window,
18887 cx,
18888 )
18889 });
18890 let multibuffer_item_id = workspace
18891 .update(cx, |workspace, window, cx| {
18892 assert!(
18893 workspace.active_item(cx).is_none(),
18894 "active item should be None before the first item is added"
18895 );
18896 workspace.add_item_to_active_pane(
18897 Box::new(multi_buffer_editor.clone()),
18898 None,
18899 true,
18900 window,
18901 cx,
18902 );
18903 let active_item = workspace
18904 .active_item(cx)
18905 .expect("should have an active item after adding the multi buffer");
18906 assert_eq!(
18907 active_item.buffer_kind(cx),
18908 ItemBufferKind::Multibuffer,
18909 "A multi buffer was expected to active after adding"
18910 );
18911 active_item.item_id()
18912 })
18913 .unwrap();
18914 cx.executor().run_until_parked();
18915
18916 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18917 editor.change_selections(
18918 SelectionEffects::scroll(Autoscroll::Next),
18919 window,
18920 cx,
18921 |s| s.select_ranges(Some(1..2)),
18922 );
18923 editor.open_excerpts(&OpenExcerpts, window, cx);
18924 });
18925 cx.executor().run_until_parked();
18926 let first_item_id = workspace
18927 .update(cx, |workspace, window, cx| {
18928 let active_item = workspace
18929 .active_item(cx)
18930 .expect("should have an active item after navigating into the 1st buffer");
18931 let first_item_id = active_item.item_id();
18932 assert_ne!(
18933 first_item_id, multibuffer_item_id,
18934 "Should navigate into the 1st buffer and activate it"
18935 );
18936 assert_eq!(
18937 active_item.buffer_kind(cx),
18938 ItemBufferKind::Singleton,
18939 "New active item should be a singleton buffer"
18940 );
18941 assert_eq!(
18942 active_item
18943 .act_as::<Editor>(cx)
18944 .expect("should have navigated into an editor for the 1st buffer")
18945 .read(cx)
18946 .text(cx),
18947 sample_text_1
18948 );
18949
18950 workspace
18951 .go_back(workspace.active_pane().downgrade(), window, cx)
18952 .detach_and_log_err(cx);
18953
18954 first_item_id
18955 })
18956 .unwrap();
18957 cx.executor().run_until_parked();
18958 workspace
18959 .update(cx, |workspace, _, cx| {
18960 let active_item = workspace
18961 .active_item(cx)
18962 .expect("should have an active item after navigating back");
18963 assert_eq!(
18964 active_item.item_id(),
18965 multibuffer_item_id,
18966 "Should navigate back to the multi buffer"
18967 );
18968 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18969 })
18970 .unwrap();
18971
18972 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18973 editor.change_selections(
18974 SelectionEffects::scroll(Autoscroll::Next),
18975 window,
18976 cx,
18977 |s| s.select_ranges(Some(39..40)),
18978 );
18979 editor.open_excerpts(&OpenExcerpts, window, cx);
18980 });
18981 cx.executor().run_until_parked();
18982 let second_item_id = workspace
18983 .update(cx, |workspace, window, cx| {
18984 let active_item = workspace
18985 .active_item(cx)
18986 .expect("should have an active item after navigating into the 2nd buffer");
18987 let second_item_id = active_item.item_id();
18988 assert_ne!(
18989 second_item_id, multibuffer_item_id,
18990 "Should navigate away from the multibuffer"
18991 );
18992 assert_ne!(
18993 second_item_id, first_item_id,
18994 "Should navigate into the 2nd buffer and activate it"
18995 );
18996 assert_eq!(
18997 active_item.buffer_kind(cx),
18998 ItemBufferKind::Singleton,
18999 "New active item should be a singleton buffer"
19000 );
19001 assert_eq!(
19002 active_item
19003 .act_as::<Editor>(cx)
19004 .expect("should have navigated into an editor")
19005 .read(cx)
19006 .text(cx),
19007 sample_text_2
19008 );
19009
19010 workspace
19011 .go_back(workspace.active_pane().downgrade(), window, cx)
19012 .detach_and_log_err(cx);
19013
19014 second_item_id
19015 })
19016 .unwrap();
19017 cx.executor().run_until_parked();
19018 workspace
19019 .update(cx, |workspace, _, cx| {
19020 let active_item = workspace
19021 .active_item(cx)
19022 .expect("should have an active item after navigating back from the 2nd buffer");
19023 assert_eq!(
19024 active_item.item_id(),
19025 multibuffer_item_id,
19026 "Should navigate back from the 2nd buffer to the multi buffer"
19027 );
19028 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19029 })
19030 .unwrap();
19031
19032 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19033 editor.change_selections(
19034 SelectionEffects::scroll(Autoscroll::Next),
19035 window,
19036 cx,
19037 |s| s.select_ranges(Some(70..70)),
19038 );
19039 editor.open_excerpts(&OpenExcerpts, window, cx);
19040 });
19041 cx.executor().run_until_parked();
19042 workspace
19043 .update(cx, |workspace, window, cx| {
19044 let active_item = workspace
19045 .active_item(cx)
19046 .expect("should have an active item after navigating into the 3rd buffer");
19047 let third_item_id = active_item.item_id();
19048 assert_ne!(
19049 third_item_id, multibuffer_item_id,
19050 "Should navigate into the 3rd buffer and activate it"
19051 );
19052 assert_ne!(third_item_id, first_item_id);
19053 assert_ne!(third_item_id, second_item_id);
19054 assert_eq!(
19055 active_item.buffer_kind(cx),
19056 ItemBufferKind::Singleton,
19057 "New active item should be a singleton buffer"
19058 );
19059 assert_eq!(
19060 active_item
19061 .act_as::<Editor>(cx)
19062 .expect("should have navigated into an editor")
19063 .read(cx)
19064 .text(cx),
19065 sample_text_3
19066 );
19067
19068 workspace
19069 .go_back(workspace.active_pane().downgrade(), window, cx)
19070 .detach_and_log_err(cx);
19071 })
19072 .unwrap();
19073 cx.executor().run_until_parked();
19074 workspace
19075 .update(cx, |workspace, _, cx| {
19076 let active_item = workspace
19077 .active_item(cx)
19078 .expect("should have an active item after navigating back from the 3rd buffer");
19079 assert_eq!(
19080 active_item.item_id(),
19081 multibuffer_item_id,
19082 "Should navigate back from the 3rd buffer to the multi buffer"
19083 );
19084 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19085 })
19086 .unwrap();
19087}
19088
19089#[gpui::test]
19090async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19091 init_test(cx, |_| {});
19092
19093 let mut cx = EditorTestContext::new(cx).await;
19094
19095 let diff_base = r#"
19096 use some::mod;
19097
19098 const A: u32 = 42;
19099
19100 fn main() {
19101 println!("hello");
19102
19103 println!("world");
19104 }
19105 "#
19106 .unindent();
19107
19108 cx.set_state(
19109 &r#"
19110 use some::modified;
19111
19112 ˇ
19113 fn main() {
19114 println!("hello there");
19115
19116 println!("around the");
19117 println!("world");
19118 }
19119 "#
19120 .unindent(),
19121 );
19122
19123 cx.set_head_text(&diff_base);
19124 executor.run_until_parked();
19125
19126 cx.update_editor(|editor, window, cx| {
19127 editor.go_to_next_hunk(&GoToHunk, window, cx);
19128 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19129 });
19130 executor.run_until_parked();
19131 cx.assert_state_with_diff(
19132 r#"
19133 use some::modified;
19134
19135
19136 fn main() {
19137 - println!("hello");
19138 + ˇ println!("hello there");
19139
19140 println!("around the");
19141 println!("world");
19142 }
19143 "#
19144 .unindent(),
19145 );
19146
19147 cx.update_editor(|editor, window, cx| {
19148 for _ in 0..2 {
19149 editor.go_to_next_hunk(&GoToHunk, window, cx);
19150 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19151 }
19152 });
19153 executor.run_until_parked();
19154 cx.assert_state_with_diff(
19155 r#"
19156 - use some::mod;
19157 + ˇuse some::modified;
19158
19159
19160 fn main() {
19161 - println!("hello");
19162 + println!("hello there");
19163
19164 + println!("around the");
19165 println!("world");
19166 }
19167 "#
19168 .unindent(),
19169 );
19170
19171 cx.update_editor(|editor, window, cx| {
19172 editor.go_to_next_hunk(&GoToHunk, window, cx);
19173 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19174 });
19175 executor.run_until_parked();
19176 cx.assert_state_with_diff(
19177 r#"
19178 - use some::mod;
19179 + use some::modified;
19180
19181 - const A: u32 = 42;
19182 ˇ
19183 fn main() {
19184 - println!("hello");
19185 + println!("hello there");
19186
19187 + println!("around the");
19188 println!("world");
19189 }
19190 "#
19191 .unindent(),
19192 );
19193
19194 cx.update_editor(|editor, window, cx| {
19195 editor.cancel(&Cancel, window, cx);
19196 });
19197
19198 cx.assert_state_with_diff(
19199 r#"
19200 use some::modified;
19201
19202 ˇ
19203 fn main() {
19204 println!("hello there");
19205
19206 println!("around the");
19207 println!("world");
19208 }
19209 "#
19210 .unindent(),
19211 );
19212}
19213
19214#[gpui::test]
19215async fn test_diff_base_change_with_expanded_diff_hunks(
19216 executor: BackgroundExecutor,
19217 cx: &mut TestAppContext,
19218) {
19219 init_test(cx, |_| {});
19220
19221 let mut cx = EditorTestContext::new(cx).await;
19222
19223 let diff_base = r#"
19224 use some::mod1;
19225 use some::mod2;
19226
19227 const A: u32 = 42;
19228 const B: u32 = 42;
19229 const C: u32 = 42;
19230
19231 fn main() {
19232 println!("hello");
19233
19234 println!("world");
19235 }
19236 "#
19237 .unindent();
19238
19239 cx.set_state(
19240 &r#"
19241 use some::mod2;
19242
19243 const A: u32 = 42;
19244 const C: u32 = 42;
19245
19246 fn main(ˇ) {
19247 //println!("hello");
19248
19249 println!("world");
19250 //
19251 //
19252 }
19253 "#
19254 .unindent(),
19255 );
19256
19257 cx.set_head_text(&diff_base);
19258 executor.run_until_parked();
19259
19260 cx.update_editor(|editor, window, cx| {
19261 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19262 });
19263 executor.run_until_parked();
19264 cx.assert_state_with_diff(
19265 r#"
19266 - use some::mod1;
19267 use some::mod2;
19268
19269 const A: u32 = 42;
19270 - const B: u32 = 42;
19271 const C: u32 = 42;
19272
19273 fn main(ˇ) {
19274 - println!("hello");
19275 + //println!("hello");
19276
19277 println!("world");
19278 + //
19279 + //
19280 }
19281 "#
19282 .unindent(),
19283 );
19284
19285 cx.set_head_text("new diff base!");
19286 executor.run_until_parked();
19287 cx.assert_state_with_diff(
19288 r#"
19289 - new diff base!
19290 + use some::mod2;
19291 +
19292 + const A: u32 = 42;
19293 + const C: u32 = 42;
19294 +
19295 + fn main(ˇ) {
19296 + //println!("hello");
19297 +
19298 + println!("world");
19299 + //
19300 + //
19301 + }
19302 "#
19303 .unindent(),
19304 );
19305}
19306
19307#[gpui::test]
19308async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19309 init_test(cx, |_| {});
19310
19311 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19312 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19313 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19314 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19315 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19316 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19317
19318 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19319 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19320 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19321
19322 let multi_buffer = cx.new(|cx| {
19323 let mut multibuffer = MultiBuffer::new(ReadWrite);
19324 multibuffer.push_excerpts(
19325 buffer_1.clone(),
19326 [
19327 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19328 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19329 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19330 ],
19331 cx,
19332 );
19333 multibuffer.push_excerpts(
19334 buffer_2.clone(),
19335 [
19336 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19337 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19338 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19339 ],
19340 cx,
19341 );
19342 multibuffer.push_excerpts(
19343 buffer_3.clone(),
19344 [
19345 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19346 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19347 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19348 ],
19349 cx,
19350 );
19351 multibuffer
19352 });
19353
19354 let editor =
19355 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19356 editor
19357 .update(cx, |editor, _window, cx| {
19358 for (buffer, diff_base) in [
19359 (buffer_1.clone(), file_1_old),
19360 (buffer_2.clone(), file_2_old),
19361 (buffer_3.clone(), file_3_old),
19362 ] {
19363 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19364 editor
19365 .buffer
19366 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19367 }
19368 })
19369 .unwrap();
19370
19371 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19372 cx.run_until_parked();
19373
19374 cx.assert_editor_state(
19375 &"
19376 ˇaaa
19377 ccc
19378 ddd
19379
19380 ggg
19381 hhh
19382
19383
19384 lll
19385 mmm
19386 NNN
19387
19388 qqq
19389 rrr
19390
19391 uuu
19392 111
19393 222
19394 333
19395
19396 666
19397 777
19398
19399 000
19400 !!!"
19401 .unindent(),
19402 );
19403
19404 cx.update_editor(|editor, window, cx| {
19405 editor.select_all(&SelectAll, window, cx);
19406 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19407 });
19408 cx.executor().run_until_parked();
19409
19410 cx.assert_state_with_diff(
19411 "
19412 «aaa
19413 - bbb
19414 ccc
19415 ddd
19416
19417 ggg
19418 hhh
19419
19420
19421 lll
19422 mmm
19423 - nnn
19424 + NNN
19425
19426 qqq
19427 rrr
19428
19429 uuu
19430 111
19431 222
19432 333
19433
19434 + 666
19435 777
19436
19437 000
19438 !!!ˇ»"
19439 .unindent(),
19440 );
19441}
19442
19443#[gpui::test]
19444async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19445 init_test(cx, |_| {});
19446
19447 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19448 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19449
19450 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19451 let multi_buffer = cx.new(|cx| {
19452 let mut multibuffer = MultiBuffer::new(ReadWrite);
19453 multibuffer.push_excerpts(
19454 buffer.clone(),
19455 [
19456 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19457 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19458 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19459 ],
19460 cx,
19461 );
19462 multibuffer
19463 });
19464
19465 let editor =
19466 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19467 editor
19468 .update(cx, |editor, _window, cx| {
19469 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19470 editor
19471 .buffer
19472 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19473 })
19474 .unwrap();
19475
19476 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19477 cx.run_until_parked();
19478
19479 cx.update_editor(|editor, window, cx| {
19480 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19481 });
19482 cx.executor().run_until_parked();
19483
19484 // When the start of a hunk coincides with the start of its excerpt,
19485 // the hunk is expanded. When the start of a hunk is earlier than
19486 // the start of its excerpt, the hunk is not expanded.
19487 cx.assert_state_with_diff(
19488 "
19489 ˇaaa
19490 - bbb
19491 + BBB
19492
19493 - ddd
19494 - eee
19495 + DDD
19496 + EEE
19497 fff
19498
19499 iii
19500 "
19501 .unindent(),
19502 );
19503}
19504
19505#[gpui::test]
19506async fn test_edits_around_expanded_insertion_hunks(
19507 executor: BackgroundExecutor,
19508 cx: &mut TestAppContext,
19509) {
19510 init_test(cx, |_| {});
19511
19512 let mut cx = EditorTestContext::new(cx).await;
19513
19514 let diff_base = r#"
19515 use some::mod1;
19516 use some::mod2;
19517
19518 const A: u32 = 42;
19519
19520 fn main() {
19521 println!("hello");
19522
19523 println!("world");
19524 }
19525 "#
19526 .unindent();
19527 executor.run_until_parked();
19528 cx.set_state(
19529 &r#"
19530 use some::mod1;
19531 use some::mod2;
19532
19533 const A: u32 = 42;
19534 const B: u32 = 42;
19535 const C: u32 = 42;
19536 ˇ
19537
19538 fn main() {
19539 println!("hello");
19540
19541 println!("world");
19542 }
19543 "#
19544 .unindent(),
19545 );
19546
19547 cx.set_head_text(&diff_base);
19548 executor.run_until_parked();
19549
19550 cx.update_editor(|editor, window, cx| {
19551 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19552 });
19553 executor.run_until_parked();
19554
19555 cx.assert_state_with_diff(
19556 r#"
19557 use some::mod1;
19558 use some::mod2;
19559
19560 const A: u32 = 42;
19561 + const B: u32 = 42;
19562 + const C: u32 = 42;
19563 + ˇ
19564
19565 fn main() {
19566 println!("hello");
19567
19568 println!("world");
19569 }
19570 "#
19571 .unindent(),
19572 );
19573
19574 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19575 executor.run_until_parked();
19576
19577 cx.assert_state_with_diff(
19578 r#"
19579 use some::mod1;
19580 use some::mod2;
19581
19582 const A: u32 = 42;
19583 + const B: u32 = 42;
19584 + const C: u32 = 42;
19585 + const D: u32 = 42;
19586 + ˇ
19587
19588 fn main() {
19589 println!("hello");
19590
19591 println!("world");
19592 }
19593 "#
19594 .unindent(),
19595 );
19596
19597 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19598 executor.run_until_parked();
19599
19600 cx.assert_state_with_diff(
19601 r#"
19602 use some::mod1;
19603 use some::mod2;
19604
19605 const A: u32 = 42;
19606 + const B: u32 = 42;
19607 + const C: u32 = 42;
19608 + const D: u32 = 42;
19609 + const E: u32 = 42;
19610 + ˇ
19611
19612 fn main() {
19613 println!("hello");
19614
19615 println!("world");
19616 }
19617 "#
19618 .unindent(),
19619 );
19620
19621 cx.update_editor(|editor, window, cx| {
19622 editor.delete_line(&DeleteLine, window, cx);
19623 });
19624 executor.run_until_parked();
19625
19626 cx.assert_state_with_diff(
19627 r#"
19628 use some::mod1;
19629 use some::mod2;
19630
19631 const A: u32 = 42;
19632 + const B: u32 = 42;
19633 + const C: u32 = 42;
19634 + const D: u32 = 42;
19635 + const E: u32 = 42;
19636 ˇ
19637 fn main() {
19638 println!("hello");
19639
19640 println!("world");
19641 }
19642 "#
19643 .unindent(),
19644 );
19645
19646 cx.update_editor(|editor, window, cx| {
19647 editor.move_up(&MoveUp, window, cx);
19648 editor.delete_line(&DeleteLine, window, cx);
19649 editor.move_up(&MoveUp, window, cx);
19650 editor.delete_line(&DeleteLine, window, cx);
19651 editor.move_up(&MoveUp, window, cx);
19652 editor.delete_line(&DeleteLine, window, cx);
19653 });
19654 executor.run_until_parked();
19655 cx.assert_state_with_diff(
19656 r#"
19657 use some::mod1;
19658 use some::mod2;
19659
19660 const A: u32 = 42;
19661 + const B: u32 = 42;
19662 ˇ
19663 fn main() {
19664 println!("hello");
19665
19666 println!("world");
19667 }
19668 "#
19669 .unindent(),
19670 );
19671
19672 cx.update_editor(|editor, window, cx| {
19673 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19674 editor.delete_line(&DeleteLine, window, cx);
19675 });
19676 executor.run_until_parked();
19677 cx.assert_state_with_diff(
19678 r#"
19679 ˇ
19680 fn main() {
19681 println!("hello");
19682
19683 println!("world");
19684 }
19685 "#
19686 .unindent(),
19687 );
19688}
19689
19690#[gpui::test]
19691async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19692 init_test(cx, |_| {});
19693
19694 let mut cx = EditorTestContext::new(cx).await;
19695 cx.set_head_text(indoc! { "
19696 one
19697 two
19698 three
19699 four
19700 five
19701 "
19702 });
19703 cx.set_state(indoc! { "
19704 one
19705 ˇthree
19706 five
19707 "});
19708 cx.run_until_parked();
19709 cx.update_editor(|editor, window, cx| {
19710 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19711 });
19712 cx.assert_state_with_diff(
19713 indoc! { "
19714 one
19715 - two
19716 ˇthree
19717 - four
19718 five
19719 "}
19720 .to_string(),
19721 );
19722 cx.update_editor(|editor, window, cx| {
19723 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19724 });
19725
19726 cx.assert_state_with_diff(
19727 indoc! { "
19728 one
19729 ˇthree
19730 five
19731 "}
19732 .to_string(),
19733 );
19734
19735 cx.set_state(indoc! { "
19736 one
19737 ˇTWO
19738 three
19739 four
19740 five
19741 "});
19742 cx.run_until_parked();
19743 cx.update_editor(|editor, window, cx| {
19744 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19745 });
19746
19747 cx.assert_state_with_diff(
19748 indoc! { "
19749 one
19750 - two
19751 + ˇTWO
19752 three
19753 four
19754 five
19755 "}
19756 .to_string(),
19757 );
19758 cx.update_editor(|editor, window, cx| {
19759 editor.move_up(&Default::default(), window, cx);
19760 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19761 });
19762 cx.assert_state_with_diff(
19763 indoc! { "
19764 one
19765 ˇTWO
19766 three
19767 four
19768 five
19769 "}
19770 .to_string(),
19771 );
19772}
19773
19774#[gpui::test]
19775async fn test_edits_around_expanded_deletion_hunks(
19776 executor: BackgroundExecutor,
19777 cx: &mut TestAppContext,
19778) {
19779 init_test(cx, |_| {});
19780
19781 let mut cx = EditorTestContext::new(cx).await;
19782
19783 let diff_base = r#"
19784 use some::mod1;
19785 use some::mod2;
19786
19787 const A: u32 = 42;
19788 const B: u32 = 42;
19789 const C: u32 = 42;
19790
19791
19792 fn main() {
19793 println!("hello");
19794
19795 println!("world");
19796 }
19797 "#
19798 .unindent();
19799 executor.run_until_parked();
19800 cx.set_state(
19801 &r#"
19802 use some::mod1;
19803 use some::mod2;
19804
19805 ˇconst B: u32 = 42;
19806 const C: u32 = 42;
19807
19808
19809 fn main() {
19810 println!("hello");
19811
19812 println!("world");
19813 }
19814 "#
19815 .unindent(),
19816 );
19817
19818 cx.set_head_text(&diff_base);
19819 executor.run_until_parked();
19820
19821 cx.update_editor(|editor, window, cx| {
19822 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19823 });
19824 executor.run_until_parked();
19825
19826 cx.assert_state_with_diff(
19827 r#"
19828 use some::mod1;
19829 use some::mod2;
19830
19831 - const A: u32 = 42;
19832 ˇconst B: u32 = 42;
19833 const C: u32 = 42;
19834
19835
19836 fn main() {
19837 println!("hello");
19838
19839 println!("world");
19840 }
19841 "#
19842 .unindent(),
19843 );
19844
19845 cx.update_editor(|editor, window, cx| {
19846 editor.delete_line(&DeleteLine, window, cx);
19847 });
19848 executor.run_until_parked();
19849 cx.assert_state_with_diff(
19850 r#"
19851 use some::mod1;
19852 use some::mod2;
19853
19854 - const A: u32 = 42;
19855 - const B: u32 = 42;
19856 ˇconst C: u32 = 42;
19857
19858
19859 fn main() {
19860 println!("hello");
19861
19862 println!("world");
19863 }
19864 "#
19865 .unindent(),
19866 );
19867
19868 cx.update_editor(|editor, window, cx| {
19869 editor.delete_line(&DeleteLine, window, cx);
19870 });
19871 executor.run_until_parked();
19872 cx.assert_state_with_diff(
19873 r#"
19874 use some::mod1;
19875 use some::mod2;
19876
19877 - const A: u32 = 42;
19878 - const B: u32 = 42;
19879 - const C: u32 = 42;
19880 ˇ
19881
19882 fn main() {
19883 println!("hello");
19884
19885 println!("world");
19886 }
19887 "#
19888 .unindent(),
19889 );
19890
19891 cx.update_editor(|editor, window, cx| {
19892 editor.handle_input("replacement", window, cx);
19893 });
19894 executor.run_until_parked();
19895 cx.assert_state_with_diff(
19896 r#"
19897 use some::mod1;
19898 use some::mod2;
19899
19900 - const A: u32 = 42;
19901 - const B: u32 = 42;
19902 - const C: u32 = 42;
19903 -
19904 + replacementˇ
19905
19906 fn main() {
19907 println!("hello");
19908
19909 println!("world");
19910 }
19911 "#
19912 .unindent(),
19913 );
19914}
19915
19916#[gpui::test]
19917async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19918 init_test(cx, |_| {});
19919
19920 let mut cx = EditorTestContext::new(cx).await;
19921
19922 let base_text = r#"
19923 one
19924 two
19925 three
19926 four
19927 five
19928 "#
19929 .unindent();
19930 executor.run_until_parked();
19931 cx.set_state(
19932 &r#"
19933 one
19934 two
19935 fˇour
19936 five
19937 "#
19938 .unindent(),
19939 );
19940
19941 cx.set_head_text(&base_text);
19942 executor.run_until_parked();
19943
19944 cx.update_editor(|editor, window, cx| {
19945 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19946 });
19947 executor.run_until_parked();
19948
19949 cx.assert_state_with_diff(
19950 r#"
19951 one
19952 two
19953 - three
19954 fˇour
19955 five
19956 "#
19957 .unindent(),
19958 );
19959
19960 cx.update_editor(|editor, window, cx| {
19961 editor.backspace(&Backspace, window, cx);
19962 editor.backspace(&Backspace, window, cx);
19963 });
19964 executor.run_until_parked();
19965 cx.assert_state_with_diff(
19966 r#"
19967 one
19968 two
19969 - threeˇ
19970 - four
19971 + our
19972 five
19973 "#
19974 .unindent(),
19975 );
19976}
19977
19978#[gpui::test]
19979async fn test_edit_after_expanded_modification_hunk(
19980 executor: BackgroundExecutor,
19981 cx: &mut TestAppContext,
19982) {
19983 init_test(cx, |_| {});
19984
19985 let mut cx = EditorTestContext::new(cx).await;
19986
19987 let diff_base = r#"
19988 use some::mod1;
19989 use some::mod2;
19990
19991 const A: u32 = 42;
19992 const B: u32 = 42;
19993 const C: u32 = 42;
19994 const D: u32 = 42;
19995
19996
19997 fn main() {
19998 println!("hello");
19999
20000 println!("world");
20001 }"#
20002 .unindent();
20003
20004 cx.set_state(
20005 &r#"
20006 use some::mod1;
20007 use some::mod2;
20008
20009 const A: u32 = 42;
20010 const B: u32 = 42;
20011 const C: u32 = 43ˇ
20012 const D: u32 = 42;
20013
20014
20015 fn main() {
20016 println!("hello");
20017
20018 println!("world");
20019 }"#
20020 .unindent(),
20021 );
20022
20023 cx.set_head_text(&diff_base);
20024 executor.run_until_parked();
20025 cx.update_editor(|editor, window, cx| {
20026 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20027 });
20028 executor.run_until_parked();
20029
20030 cx.assert_state_with_diff(
20031 r#"
20032 use some::mod1;
20033 use some::mod2;
20034
20035 const A: u32 = 42;
20036 const B: u32 = 42;
20037 - const C: u32 = 42;
20038 + const C: u32 = 43ˇ
20039 const D: u32 = 42;
20040
20041
20042 fn main() {
20043 println!("hello");
20044
20045 println!("world");
20046 }"#
20047 .unindent(),
20048 );
20049
20050 cx.update_editor(|editor, window, cx| {
20051 editor.handle_input("\nnew_line\n", window, cx);
20052 });
20053 executor.run_until_parked();
20054
20055 cx.assert_state_with_diff(
20056 r#"
20057 use some::mod1;
20058 use some::mod2;
20059
20060 const A: u32 = 42;
20061 const B: u32 = 42;
20062 - const C: u32 = 42;
20063 + const C: u32 = 43
20064 + new_line
20065 + ˇ
20066 const D: u32 = 42;
20067
20068
20069 fn main() {
20070 println!("hello");
20071
20072 println!("world");
20073 }"#
20074 .unindent(),
20075 );
20076}
20077
20078#[gpui::test]
20079async fn test_stage_and_unstage_added_file_hunk(
20080 executor: BackgroundExecutor,
20081 cx: &mut TestAppContext,
20082) {
20083 init_test(cx, |_| {});
20084
20085 let mut cx = EditorTestContext::new(cx).await;
20086 cx.update_editor(|editor, _, cx| {
20087 editor.set_expand_all_diff_hunks(cx);
20088 });
20089
20090 let working_copy = r#"
20091 ˇfn main() {
20092 println!("hello, world!");
20093 }
20094 "#
20095 .unindent();
20096
20097 cx.set_state(&working_copy);
20098 executor.run_until_parked();
20099
20100 cx.assert_state_with_diff(
20101 r#"
20102 + ˇfn main() {
20103 + println!("hello, world!");
20104 + }
20105 "#
20106 .unindent(),
20107 );
20108 cx.assert_index_text(None);
20109
20110 cx.update_editor(|editor, window, cx| {
20111 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20112 });
20113 executor.run_until_parked();
20114 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20115 cx.assert_state_with_diff(
20116 r#"
20117 + ˇfn main() {
20118 + println!("hello, world!");
20119 + }
20120 "#
20121 .unindent(),
20122 );
20123
20124 cx.update_editor(|editor, window, cx| {
20125 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20126 });
20127 executor.run_until_parked();
20128 cx.assert_index_text(None);
20129}
20130
20131async fn setup_indent_guides_editor(
20132 text: &str,
20133 cx: &mut TestAppContext,
20134) -> (BufferId, EditorTestContext) {
20135 init_test(cx, |_| {});
20136
20137 let mut cx = EditorTestContext::new(cx).await;
20138
20139 let buffer_id = cx.update_editor(|editor, window, cx| {
20140 editor.set_text(text, window, cx);
20141 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20142
20143 buffer_ids[0]
20144 });
20145
20146 (buffer_id, cx)
20147}
20148
20149fn assert_indent_guides(
20150 range: Range<u32>,
20151 expected: Vec<IndentGuide>,
20152 active_indices: Option<Vec<usize>>,
20153 cx: &mut EditorTestContext,
20154) {
20155 let indent_guides = cx.update_editor(|editor, window, cx| {
20156 let snapshot = editor.snapshot(window, cx).display_snapshot;
20157 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20158 editor,
20159 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20160 true,
20161 &snapshot,
20162 cx,
20163 );
20164
20165 indent_guides.sort_by(|a, b| {
20166 a.depth.cmp(&b.depth).then(
20167 a.start_row
20168 .cmp(&b.start_row)
20169 .then(a.end_row.cmp(&b.end_row)),
20170 )
20171 });
20172 indent_guides
20173 });
20174
20175 if let Some(expected) = active_indices {
20176 let active_indices = cx.update_editor(|editor, window, cx| {
20177 let snapshot = editor.snapshot(window, cx).display_snapshot;
20178 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20179 });
20180
20181 assert_eq!(
20182 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20183 expected,
20184 "Active indent guide indices do not match"
20185 );
20186 }
20187
20188 assert_eq!(indent_guides, expected, "Indent guides do not match");
20189}
20190
20191fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20192 IndentGuide {
20193 buffer_id,
20194 start_row: MultiBufferRow(start_row),
20195 end_row: MultiBufferRow(end_row),
20196 depth,
20197 tab_size: 4,
20198 settings: IndentGuideSettings {
20199 enabled: true,
20200 line_width: 1,
20201 active_line_width: 1,
20202 coloring: IndentGuideColoring::default(),
20203 background_coloring: IndentGuideBackgroundColoring::default(),
20204 },
20205 }
20206}
20207
20208#[gpui::test]
20209async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20210 let (buffer_id, mut cx) = setup_indent_guides_editor(
20211 &"
20212 fn main() {
20213 let a = 1;
20214 }"
20215 .unindent(),
20216 cx,
20217 )
20218 .await;
20219
20220 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20221}
20222
20223#[gpui::test]
20224async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20225 let (buffer_id, mut cx) = setup_indent_guides_editor(
20226 &"
20227 fn main() {
20228 let a = 1;
20229 let b = 2;
20230 }"
20231 .unindent(),
20232 cx,
20233 )
20234 .await;
20235
20236 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20237}
20238
20239#[gpui::test]
20240async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20241 let (buffer_id, mut cx) = setup_indent_guides_editor(
20242 &"
20243 fn main() {
20244 let a = 1;
20245 if a == 3 {
20246 let b = 2;
20247 } else {
20248 let c = 3;
20249 }
20250 }"
20251 .unindent(),
20252 cx,
20253 )
20254 .await;
20255
20256 assert_indent_guides(
20257 0..8,
20258 vec![
20259 indent_guide(buffer_id, 1, 6, 0),
20260 indent_guide(buffer_id, 3, 3, 1),
20261 indent_guide(buffer_id, 5, 5, 1),
20262 ],
20263 None,
20264 &mut cx,
20265 );
20266}
20267
20268#[gpui::test]
20269async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20270 let (buffer_id, mut cx) = setup_indent_guides_editor(
20271 &"
20272 fn main() {
20273 let a = 1;
20274 let b = 2;
20275 let c = 3;
20276 }"
20277 .unindent(),
20278 cx,
20279 )
20280 .await;
20281
20282 assert_indent_guides(
20283 0..5,
20284 vec![
20285 indent_guide(buffer_id, 1, 3, 0),
20286 indent_guide(buffer_id, 2, 2, 1),
20287 ],
20288 None,
20289 &mut cx,
20290 );
20291}
20292
20293#[gpui::test]
20294async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20295 let (buffer_id, mut cx) = setup_indent_guides_editor(
20296 &"
20297 fn main() {
20298 let a = 1;
20299
20300 let c = 3;
20301 }"
20302 .unindent(),
20303 cx,
20304 )
20305 .await;
20306
20307 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20308}
20309
20310#[gpui::test]
20311async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20312 let (buffer_id, mut cx) = setup_indent_guides_editor(
20313 &"
20314 fn main() {
20315 let a = 1;
20316
20317 let c = 3;
20318
20319 if a == 3 {
20320 let b = 2;
20321 } else {
20322 let c = 3;
20323 }
20324 }"
20325 .unindent(),
20326 cx,
20327 )
20328 .await;
20329
20330 assert_indent_guides(
20331 0..11,
20332 vec![
20333 indent_guide(buffer_id, 1, 9, 0),
20334 indent_guide(buffer_id, 6, 6, 1),
20335 indent_guide(buffer_id, 8, 8, 1),
20336 ],
20337 None,
20338 &mut cx,
20339 );
20340}
20341
20342#[gpui::test]
20343async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20344 let (buffer_id, mut cx) = setup_indent_guides_editor(
20345 &"
20346 fn main() {
20347 let a = 1;
20348
20349 let c = 3;
20350
20351 if a == 3 {
20352 let b = 2;
20353 } else {
20354 let c = 3;
20355 }
20356 }"
20357 .unindent(),
20358 cx,
20359 )
20360 .await;
20361
20362 assert_indent_guides(
20363 1..11,
20364 vec![
20365 indent_guide(buffer_id, 1, 9, 0),
20366 indent_guide(buffer_id, 6, 6, 1),
20367 indent_guide(buffer_id, 8, 8, 1),
20368 ],
20369 None,
20370 &mut cx,
20371 );
20372}
20373
20374#[gpui::test]
20375async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20376 let (buffer_id, mut cx) = setup_indent_guides_editor(
20377 &"
20378 fn main() {
20379 let a = 1;
20380
20381 let c = 3;
20382
20383 if a == 3 {
20384 let b = 2;
20385 } else {
20386 let c = 3;
20387 }
20388 }"
20389 .unindent(),
20390 cx,
20391 )
20392 .await;
20393
20394 assert_indent_guides(
20395 1..10,
20396 vec![
20397 indent_guide(buffer_id, 1, 9, 0),
20398 indent_guide(buffer_id, 6, 6, 1),
20399 indent_guide(buffer_id, 8, 8, 1),
20400 ],
20401 None,
20402 &mut cx,
20403 );
20404}
20405
20406#[gpui::test]
20407async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20408 let (buffer_id, mut cx) = setup_indent_guides_editor(
20409 &"
20410 fn main() {
20411 if a {
20412 b(
20413 c,
20414 d,
20415 )
20416 } else {
20417 e(
20418 f
20419 )
20420 }
20421 }"
20422 .unindent(),
20423 cx,
20424 )
20425 .await;
20426
20427 assert_indent_guides(
20428 0..11,
20429 vec![
20430 indent_guide(buffer_id, 1, 10, 0),
20431 indent_guide(buffer_id, 2, 5, 1),
20432 indent_guide(buffer_id, 7, 9, 1),
20433 indent_guide(buffer_id, 3, 4, 2),
20434 indent_guide(buffer_id, 8, 8, 2),
20435 ],
20436 None,
20437 &mut cx,
20438 );
20439
20440 cx.update_editor(|editor, window, cx| {
20441 editor.fold_at(MultiBufferRow(2), window, cx);
20442 assert_eq!(
20443 editor.display_text(cx),
20444 "
20445 fn main() {
20446 if a {
20447 b(⋯
20448 )
20449 } else {
20450 e(
20451 f
20452 )
20453 }
20454 }"
20455 .unindent()
20456 );
20457 });
20458
20459 assert_indent_guides(
20460 0..11,
20461 vec![
20462 indent_guide(buffer_id, 1, 10, 0),
20463 indent_guide(buffer_id, 2, 5, 1),
20464 indent_guide(buffer_id, 7, 9, 1),
20465 indent_guide(buffer_id, 8, 8, 2),
20466 ],
20467 None,
20468 &mut cx,
20469 );
20470}
20471
20472#[gpui::test]
20473async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20474 let (buffer_id, mut cx) = setup_indent_guides_editor(
20475 &"
20476 block1
20477 block2
20478 block3
20479 block4
20480 block2
20481 block1
20482 block1"
20483 .unindent(),
20484 cx,
20485 )
20486 .await;
20487
20488 assert_indent_guides(
20489 1..10,
20490 vec![
20491 indent_guide(buffer_id, 1, 4, 0),
20492 indent_guide(buffer_id, 2, 3, 1),
20493 indent_guide(buffer_id, 3, 3, 2),
20494 ],
20495 None,
20496 &mut cx,
20497 );
20498}
20499
20500#[gpui::test]
20501async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20502 let (buffer_id, mut cx) = setup_indent_guides_editor(
20503 &"
20504 block1
20505 block2
20506 block3
20507
20508 block1
20509 block1"
20510 .unindent(),
20511 cx,
20512 )
20513 .await;
20514
20515 assert_indent_guides(
20516 0..6,
20517 vec![
20518 indent_guide(buffer_id, 1, 2, 0),
20519 indent_guide(buffer_id, 2, 2, 1),
20520 ],
20521 None,
20522 &mut cx,
20523 );
20524}
20525
20526#[gpui::test]
20527async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20528 let (buffer_id, mut cx) = setup_indent_guides_editor(
20529 &"
20530 function component() {
20531 \treturn (
20532 \t\t\t
20533 \t\t<div>
20534 \t\t\t<abc></abc>
20535 \t\t</div>
20536 \t)
20537 }"
20538 .unindent(),
20539 cx,
20540 )
20541 .await;
20542
20543 assert_indent_guides(
20544 0..8,
20545 vec![
20546 indent_guide(buffer_id, 1, 6, 0),
20547 indent_guide(buffer_id, 2, 5, 1),
20548 indent_guide(buffer_id, 4, 4, 2),
20549 ],
20550 None,
20551 &mut cx,
20552 );
20553}
20554
20555#[gpui::test]
20556async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20557 let (buffer_id, mut cx) = setup_indent_guides_editor(
20558 &"
20559 function component() {
20560 \treturn (
20561 \t
20562 \t\t<div>
20563 \t\t\t<abc></abc>
20564 \t\t</div>
20565 \t)
20566 }"
20567 .unindent(),
20568 cx,
20569 )
20570 .await;
20571
20572 assert_indent_guides(
20573 0..8,
20574 vec![
20575 indent_guide(buffer_id, 1, 6, 0),
20576 indent_guide(buffer_id, 2, 5, 1),
20577 indent_guide(buffer_id, 4, 4, 2),
20578 ],
20579 None,
20580 &mut cx,
20581 );
20582}
20583
20584#[gpui::test]
20585async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20586 let (buffer_id, mut cx) = setup_indent_guides_editor(
20587 &"
20588 block1
20589
20590
20591
20592 block2
20593 "
20594 .unindent(),
20595 cx,
20596 )
20597 .await;
20598
20599 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20600}
20601
20602#[gpui::test]
20603async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20604 let (buffer_id, mut cx) = setup_indent_guides_editor(
20605 &"
20606 def a:
20607 \tb = 3
20608 \tif True:
20609 \t\tc = 4
20610 \t\td = 5
20611 \tprint(b)
20612 "
20613 .unindent(),
20614 cx,
20615 )
20616 .await;
20617
20618 assert_indent_guides(
20619 0..6,
20620 vec![
20621 indent_guide(buffer_id, 1, 5, 0),
20622 indent_guide(buffer_id, 3, 4, 1),
20623 ],
20624 None,
20625 &mut cx,
20626 );
20627}
20628
20629#[gpui::test]
20630async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20631 let (buffer_id, mut cx) = setup_indent_guides_editor(
20632 &"
20633 fn main() {
20634 let a = 1;
20635 }"
20636 .unindent(),
20637 cx,
20638 )
20639 .await;
20640
20641 cx.update_editor(|editor, window, cx| {
20642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20643 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20644 });
20645 });
20646
20647 assert_indent_guides(
20648 0..3,
20649 vec![indent_guide(buffer_id, 1, 1, 0)],
20650 Some(vec![0]),
20651 &mut cx,
20652 );
20653}
20654
20655#[gpui::test]
20656async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20657 let (buffer_id, mut cx) = setup_indent_guides_editor(
20658 &"
20659 fn main() {
20660 if 1 == 2 {
20661 let a = 1;
20662 }
20663 }"
20664 .unindent(),
20665 cx,
20666 )
20667 .await;
20668
20669 cx.update_editor(|editor, window, cx| {
20670 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20671 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20672 });
20673 });
20674
20675 assert_indent_guides(
20676 0..4,
20677 vec![
20678 indent_guide(buffer_id, 1, 3, 0),
20679 indent_guide(buffer_id, 2, 2, 1),
20680 ],
20681 Some(vec![1]),
20682 &mut cx,
20683 );
20684
20685 cx.update_editor(|editor, window, cx| {
20686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20687 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20688 });
20689 });
20690
20691 assert_indent_guides(
20692 0..4,
20693 vec![
20694 indent_guide(buffer_id, 1, 3, 0),
20695 indent_guide(buffer_id, 2, 2, 1),
20696 ],
20697 Some(vec![1]),
20698 &mut cx,
20699 );
20700
20701 cx.update_editor(|editor, window, cx| {
20702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20703 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20704 });
20705 });
20706
20707 assert_indent_guides(
20708 0..4,
20709 vec![
20710 indent_guide(buffer_id, 1, 3, 0),
20711 indent_guide(buffer_id, 2, 2, 1),
20712 ],
20713 Some(vec![0]),
20714 &mut cx,
20715 );
20716}
20717
20718#[gpui::test]
20719async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20720 let (buffer_id, mut cx) = setup_indent_guides_editor(
20721 &"
20722 fn main() {
20723 let a = 1;
20724
20725 let b = 2;
20726 }"
20727 .unindent(),
20728 cx,
20729 )
20730 .await;
20731
20732 cx.update_editor(|editor, window, cx| {
20733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20734 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20735 });
20736 });
20737
20738 assert_indent_guides(
20739 0..5,
20740 vec![indent_guide(buffer_id, 1, 3, 0)],
20741 Some(vec![0]),
20742 &mut cx,
20743 );
20744}
20745
20746#[gpui::test]
20747async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20748 let (buffer_id, mut cx) = setup_indent_guides_editor(
20749 &"
20750 def m:
20751 a = 1
20752 pass"
20753 .unindent(),
20754 cx,
20755 )
20756 .await;
20757
20758 cx.update_editor(|editor, window, cx| {
20759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20760 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20761 });
20762 });
20763
20764 assert_indent_guides(
20765 0..3,
20766 vec![indent_guide(buffer_id, 1, 2, 0)],
20767 Some(vec![0]),
20768 &mut cx,
20769 );
20770}
20771
20772#[gpui::test]
20773async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20774 init_test(cx, |_| {});
20775 let mut cx = EditorTestContext::new(cx).await;
20776 let text = indoc! {
20777 "
20778 impl A {
20779 fn b() {
20780 0;
20781 3;
20782 5;
20783 6;
20784 7;
20785 }
20786 }
20787 "
20788 };
20789 let base_text = indoc! {
20790 "
20791 impl A {
20792 fn b() {
20793 0;
20794 1;
20795 2;
20796 3;
20797 4;
20798 }
20799 fn c() {
20800 5;
20801 6;
20802 7;
20803 }
20804 }
20805 "
20806 };
20807
20808 cx.update_editor(|editor, window, cx| {
20809 editor.set_text(text, window, cx);
20810
20811 editor.buffer().update(cx, |multibuffer, cx| {
20812 let buffer = multibuffer.as_singleton().unwrap();
20813 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20814
20815 multibuffer.set_all_diff_hunks_expanded(cx);
20816 multibuffer.add_diff(diff, cx);
20817
20818 buffer.read(cx).remote_id()
20819 })
20820 });
20821 cx.run_until_parked();
20822
20823 cx.assert_state_with_diff(
20824 indoc! { "
20825 impl A {
20826 fn b() {
20827 0;
20828 - 1;
20829 - 2;
20830 3;
20831 - 4;
20832 - }
20833 - fn c() {
20834 5;
20835 6;
20836 7;
20837 }
20838 }
20839 ˇ"
20840 }
20841 .to_string(),
20842 );
20843
20844 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20845 editor
20846 .snapshot(window, cx)
20847 .buffer_snapshot()
20848 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20849 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20850 .collect::<Vec<_>>()
20851 });
20852 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20853 assert_eq!(
20854 actual_guides,
20855 vec![
20856 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20857 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20858 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20859 ]
20860 );
20861}
20862
20863#[gpui::test]
20864async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20865 init_test(cx, |_| {});
20866 let mut cx = EditorTestContext::new(cx).await;
20867
20868 let diff_base = r#"
20869 a
20870 b
20871 c
20872 "#
20873 .unindent();
20874
20875 cx.set_state(
20876 &r#"
20877 ˇA
20878 b
20879 C
20880 "#
20881 .unindent(),
20882 );
20883 cx.set_head_text(&diff_base);
20884 cx.update_editor(|editor, window, cx| {
20885 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20886 });
20887 executor.run_until_parked();
20888
20889 let both_hunks_expanded = r#"
20890 - a
20891 + ˇA
20892 b
20893 - c
20894 + C
20895 "#
20896 .unindent();
20897
20898 cx.assert_state_with_diff(both_hunks_expanded.clone());
20899
20900 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20901 let snapshot = editor.snapshot(window, cx);
20902 let hunks = editor
20903 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20904 .collect::<Vec<_>>();
20905 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20906 let buffer_id = hunks[0].buffer_id;
20907 hunks
20908 .into_iter()
20909 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20910 .collect::<Vec<_>>()
20911 });
20912 assert_eq!(hunk_ranges.len(), 2);
20913
20914 cx.update_editor(|editor, _, cx| {
20915 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20916 });
20917 executor.run_until_parked();
20918
20919 let second_hunk_expanded = r#"
20920 ˇA
20921 b
20922 - c
20923 + C
20924 "#
20925 .unindent();
20926
20927 cx.assert_state_with_diff(second_hunk_expanded);
20928
20929 cx.update_editor(|editor, _, cx| {
20930 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20931 });
20932 executor.run_until_parked();
20933
20934 cx.assert_state_with_diff(both_hunks_expanded.clone());
20935
20936 cx.update_editor(|editor, _, cx| {
20937 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20938 });
20939 executor.run_until_parked();
20940
20941 let first_hunk_expanded = r#"
20942 - a
20943 + ˇA
20944 b
20945 C
20946 "#
20947 .unindent();
20948
20949 cx.assert_state_with_diff(first_hunk_expanded);
20950
20951 cx.update_editor(|editor, _, cx| {
20952 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20953 });
20954 executor.run_until_parked();
20955
20956 cx.assert_state_with_diff(both_hunks_expanded);
20957
20958 cx.set_state(
20959 &r#"
20960 ˇA
20961 b
20962 "#
20963 .unindent(),
20964 );
20965 cx.run_until_parked();
20966
20967 // TODO this cursor position seems bad
20968 cx.assert_state_with_diff(
20969 r#"
20970 - ˇa
20971 + A
20972 b
20973 "#
20974 .unindent(),
20975 );
20976
20977 cx.update_editor(|editor, window, cx| {
20978 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20979 });
20980
20981 cx.assert_state_with_diff(
20982 r#"
20983 - ˇa
20984 + A
20985 b
20986 - c
20987 "#
20988 .unindent(),
20989 );
20990
20991 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20992 let snapshot = editor.snapshot(window, cx);
20993 let hunks = editor
20994 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20995 .collect::<Vec<_>>();
20996 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20997 let buffer_id = hunks[0].buffer_id;
20998 hunks
20999 .into_iter()
21000 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21001 .collect::<Vec<_>>()
21002 });
21003 assert_eq!(hunk_ranges.len(), 2);
21004
21005 cx.update_editor(|editor, _, cx| {
21006 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21007 });
21008 executor.run_until_parked();
21009
21010 cx.assert_state_with_diff(
21011 r#"
21012 - ˇa
21013 + A
21014 b
21015 "#
21016 .unindent(),
21017 );
21018}
21019
21020#[gpui::test]
21021async fn test_toggle_deletion_hunk_at_start_of_file(
21022 executor: BackgroundExecutor,
21023 cx: &mut TestAppContext,
21024) {
21025 init_test(cx, |_| {});
21026 let mut cx = EditorTestContext::new(cx).await;
21027
21028 let diff_base = r#"
21029 a
21030 b
21031 c
21032 "#
21033 .unindent();
21034
21035 cx.set_state(
21036 &r#"
21037 ˇb
21038 c
21039 "#
21040 .unindent(),
21041 );
21042 cx.set_head_text(&diff_base);
21043 cx.update_editor(|editor, window, cx| {
21044 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21045 });
21046 executor.run_until_parked();
21047
21048 let hunk_expanded = r#"
21049 - a
21050 ˇb
21051 c
21052 "#
21053 .unindent();
21054
21055 cx.assert_state_with_diff(hunk_expanded.clone());
21056
21057 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21058 let snapshot = editor.snapshot(window, cx);
21059 let hunks = editor
21060 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21061 .collect::<Vec<_>>();
21062 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21063 let buffer_id = hunks[0].buffer_id;
21064 hunks
21065 .into_iter()
21066 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21067 .collect::<Vec<_>>()
21068 });
21069 assert_eq!(hunk_ranges.len(), 1);
21070
21071 cx.update_editor(|editor, _, cx| {
21072 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21073 });
21074 executor.run_until_parked();
21075
21076 let hunk_collapsed = r#"
21077 ˇb
21078 c
21079 "#
21080 .unindent();
21081
21082 cx.assert_state_with_diff(hunk_collapsed);
21083
21084 cx.update_editor(|editor, _, cx| {
21085 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21086 });
21087 executor.run_until_parked();
21088
21089 cx.assert_state_with_diff(hunk_expanded);
21090}
21091
21092#[gpui::test]
21093async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21094 init_test(cx, |_| {});
21095
21096 let fs = FakeFs::new(cx.executor());
21097 fs.insert_tree(
21098 path!("/test"),
21099 json!({
21100 ".git": {},
21101 "file-1": "ONE\n",
21102 "file-2": "TWO\n",
21103 "file-3": "THREE\n",
21104 }),
21105 )
21106 .await;
21107
21108 fs.set_head_for_repo(
21109 path!("/test/.git").as_ref(),
21110 &[
21111 ("file-1", "one\n".into()),
21112 ("file-2", "two\n".into()),
21113 ("file-3", "three\n".into()),
21114 ],
21115 "deadbeef",
21116 );
21117
21118 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21119 let mut buffers = vec![];
21120 for i in 1..=3 {
21121 let buffer = project
21122 .update(cx, |project, cx| {
21123 let path = format!(path!("/test/file-{}"), i);
21124 project.open_local_buffer(path, cx)
21125 })
21126 .await
21127 .unwrap();
21128 buffers.push(buffer);
21129 }
21130
21131 let multibuffer = cx.new(|cx| {
21132 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21133 multibuffer.set_all_diff_hunks_expanded(cx);
21134 for buffer in &buffers {
21135 let snapshot = buffer.read(cx).snapshot();
21136 multibuffer.set_excerpts_for_path(
21137 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21138 buffer.clone(),
21139 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21140 2,
21141 cx,
21142 );
21143 }
21144 multibuffer
21145 });
21146
21147 let editor = cx.add_window(|window, cx| {
21148 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21149 });
21150 cx.run_until_parked();
21151
21152 let snapshot = editor
21153 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21154 .unwrap();
21155 let hunks = snapshot
21156 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21157 .map(|hunk| match hunk {
21158 DisplayDiffHunk::Unfolded {
21159 display_row_range, ..
21160 } => display_row_range,
21161 DisplayDiffHunk::Folded { .. } => unreachable!(),
21162 })
21163 .collect::<Vec<_>>();
21164 assert_eq!(
21165 hunks,
21166 [
21167 DisplayRow(2)..DisplayRow(4),
21168 DisplayRow(7)..DisplayRow(9),
21169 DisplayRow(12)..DisplayRow(14),
21170 ]
21171 );
21172}
21173
21174#[gpui::test]
21175async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21176 init_test(cx, |_| {});
21177
21178 let mut cx = EditorTestContext::new(cx).await;
21179 cx.set_head_text(indoc! { "
21180 one
21181 two
21182 three
21183 four
21184 five
21185 "
21186 });
21187 cx.set_index_text(indoc! { "
21188 one
21189 two
21190 three
21191 four
21192 five
21193 "
21194 });
21195 cx.set_state(indoc! {"
21196 one
21197 TWO
21198 ˇTHREE
21199 FOUR
21200 five
21201 "});
21202 cx.run_until_parked();
21203 cx.update_editor(|editor, window, cx| {
21204 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21205 });
21206 cx.run_until_parked();
21207 cx.assert_index_text(Some(indoc! {"
21208 one
21209 TWO
21210 THREE
21211 FOUR
21212 five
21213 "}));
21214 cx.set_state(indoc! { "
21215 one
21216 TWO
21217 ˇTHREE-HUNDRED
21218 FOUR
21219 five
21220 "});
21221 cx.run_until_parked();
21222 cx.update_editor(|editor, window, cx| {
21223 let snapshot = editor.snapshot(window, cx);
21224 let hunks = editor
21225 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21226 .collect::<Vec<_>>();
21227 assert_eq!(hunks.len(), 1);
21228 assert_eq!(
21229 hunks[0].status(),
21230 DiffHunkStatus {
21231 kind: DiffHunkStatusKind::Modified,
21232 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21233 }
21234 );
21235
21236 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21237 });
21238 cx.run_until_parked();
21239 cx.assert_index_text(Some(indoc! {"
21240 one
21241 TWO
21242 THREE-HUNDRED
21243 FOUR
21244 five
21245 "}));
21246}
21247
21248#[gpui::test]
21249fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21250 init_test(cx, |_| {});
21251
21252 let editor = cx.add_window(|window, cx| {
21253 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21254 build_editor(buffer, window, cx)
21255 });
21256
21257 let render_args = Arc::new(Mutex::new(None));
21258 let snapshot = editor
21259 .update(cx, |editor, window, cx| {
21260 let snapshot = editor.buffer().read(cx).snapshot(cx);
21261 let range =
21262 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21263
21264 struct RenderArgs {
21265 row: MultiBufferRow,
21266 folded: bool,
21267 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21268 }
21269
21270 let crease = Crease::inline(
21271 range,
21272 FoldPlaceholder::test(),
21273 {
21274 let toggle_callback = render_args.clone();
21275 move |row, folded, callback, _window, _cx| {
21276 *toggle_callback.lock() = Some(RenderArgs {
21277 row,
21278 folded,
21279 callback,
21280 });
21281 div()
21282 }
21283 },
21284 |_row, _folded, _window, _cx| div(),
21285 );
21286
21287 editor.insert_creases(Some(crease), cx);
21288 let snapshot = editor.snapshot(window, cx);
21289 let _div =
21290 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21291 snapshot
21292 })
21293 .unwrap();
21294
21295 let render_args = render_args.lock().take().unwrap();
21296 assert_eq!(render_args.row, MultiBufferRow(1));
21297 assert!(!render_args.folded);
21298 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21299
21300 cx.update_window(*editor, |_, window, cx| {
21301 (render_args.callback)(true, window, cx)
21302 })
21303 .unwrap();
21304 let snapshot = editor
21305 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21306 .unwrap();
21307 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21308
21309 cx.update_window(*editor, |_, window, cx| {
21310 (render_args.callback)(false, window, cx)
21311 })
21312 .unwrap();
21313 let snapshot = editor
21314 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21315 .unwrap();
21316 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21317}
21318
21319#[gpui::test]
21320async fn test_input_text(cx: &mut TestAppContext) {
21321 init_test(cx, |_| {});
21322 let mut cx = EditorTestContext::new(cx).await;
21323
21324 cx.set_state(
21325 &r#"ˇone
21326 two
21327
21328 three
21329 fourˇ
21330 five
21331
21332 siˇx"#
21333 .unindent(),
21334 );
21335
21336 cx.dispatch_action(HandleInput(String::new()));
21337 cx.assert_editor_state(
21338 &r#"ˇone
21339 two
21340
21341 three
21342 fourˇ
21343 five
21344
21345 siˇx"#
21346 .unindent(),
21347 );
21348
21349 cx.dispatch_action(HandleInput("AAAA".to_string()));
21350 cx.assert_editor_state(
21351 &r#"AAAAˇone
21352 two
21353
21354 three
21355 fourAAAAˇ
21356 five
21357
21358 siAAAAˇx"#
21359 .unindent(),
21360 );
21361}
21362
21363#[gpui::test]
21364async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21365 init_test(cx, |_| {});
21366
21367 let mut cx = EditorTestContext::new(cx).await;
21368 cx.set_state(
21369 r#"let foo = 1;
21370let foo = 2;
21371let foo = 3;
21372let fooˇ = 4;
21373let foo = 5;
21374let foo = 6;
21375let foo = 7;
21376let foo = 8;
21377let foo = 9;
21378let foo = 10;
21379let foo = 11;
21380let foo = 12;
21381let foo = 13;
21382let foo = 14;
21383let foo = 15;"#,
21384 );
21385
21386 cx.update_editor(|e, window, cx| {
21387 assert_eq!(
21388 e.next_scroll_position,
21389 NextScrollCursorCenterTopBottom::Center,
21390 "Default next scroll direction is center",
21391 );
21392
21393 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21394 assert_eq!(
21395 e.next_scroll_position,
21396 NextScrollCursorCenterTopBottom::Top,
21397 "After center, next scroll direction should be top",
21398 );
21399
21400 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21401 assert_eq!(
21402 e.next_scroll_position,
21403 NextScrollCursorCenterTopBottom::Bottom,
21404 "After top, next scroll direction should be bottom",
21405 );
21406
21407 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21408 assert_eq!(
21409 e.next_scroll_position,
21410 NextScrollCursorCenterTopBottom::Center,
21411 "After bottom, scrolling should start over",
21412 );
21413
21414 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21415 assert_eq!(
21416 e.next_scroll_position,
21417 NextScrollCursorCenterTopBottom::Top,
21418 "Scrolling continues if retriggered fast enough"
21419 );
21420 });
21421
21422 cx.executor()
21423 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21424 cx.executor().run_until_parked();
21425 cx.update_editor(|e, _, _| {
21426 assert_eq!(
21427 e.next_scroll_position,
21428 NextScrollCursorCenterTopBottom::Center,
21429 "If scrolling is not triggered fast enough, it should reset"
21430 );
21431 });
21432}
21433
21434#[gpui::test]
21435async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21436 init_test(cx, |_| {});
21437 let mut cx = EditorLspTestContext::new_rust(
21438 lsp::ServerCapabilities {
21439 definition_provider: Some(lsp::OneOf::Left(true)),
21440 references_provider: Some(lsp::OneOf::Left(true)),
21441 ..lsp::ServerCapabilities::default()
21442 },
21443 cx,
21444 )
21445 .await;
21446
21447 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21448 let go_to_definition = cx
21449 .lsp
21450 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21451 move |params, _| async move {
21452 if empty_go_to_definition {
21453 Ok(None)
21454 } else {
21455 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21456 uri: params.text_document_position_params.text_document.uri,
21457 range: lsp::Range::new(
21458 lsp::Position::new(4, 3),
21459 lsp::Position::new(4, 6),
21460 ),
21461 })))
21462 }
21463 },
21464 );
21465 let references = cx
21466 .lsp
21467 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21468 Ok(Some(vec![lsp::Location {
21469 uri: params.text_document_position.text_document.uri,
21470 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21471 }]))
21472 });
21473 (go_to_definition, references)
21474 };
21475
21476 cx.set_state(
21477 &r#"fn one() {
21478 let mut a = ˇtwo();
21479 }
21480
21481 fn two() {}"#
21482 .unindent(),
21483 );
21484 set_up_lsp_handlers(false, &mut cx);
21485 let navigated = cx
21486 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21487 .await
21488 .expect("Failed to navigate to definition");
21489 assert_eq!(
21490 navigated,
21491 Navigated::Yes,
21492 "Should have navigated to definition from the GetDefinition response"
21493 );
21494 cx.assert_editor_state(
21495 &r#"fn one() {
21496 let mut a = two();
21497 }
21498
21499 fn «twoˇ»() {}"#
21500 .unindent(),
21501 );
21502
21503 let editors = cx.update_workspace(|workspace, _, cx| {
21504 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21505 });
21506 cx.update_editor(|_, _, test_editor_cx| {
21507 assert_eq!(
21508 editors.len(),
21509 1,
21510 "Initially, only one, test, editor should be open in the workspace"
21511 );
21512 assert_eq!(
21513 test_editor_cx.entity(),
21514 editors.last().expect("Asserted len is 1").clone()
21515 );
21516 });
21517
21518 set_up_lsp_handlers(true, &mut cx);
21519 let navigated = cx
21520 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21521 .await
21522 .expect("Failed to navigate to lookup references");
21523 assert_eq!(
21524 navigated,
21525 Navigated::Yes,
21526 "Should have navigated to references as a fallback after empty GoToDefinition response"
21527 );
21528 // We should not change the selections in the existing file,
21529 // if opening another milti buffer with the references
21530 cx.assert_editor_state(
21531 &r#"fn one() {
21532 let mut a = two();
21533 }
21534
21535 fn «twoˇ»() {}"#
21536 .unindent(),
21537 );
21538 let editors = cx.update_workspace(|workspace, _, cx| {
21539 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21540 });
21541 cx.update_editor(|_, _, test_editor_cx| {
21542 assert_eq!(
21543 editors.len(),
21544 2,
21545 "After falling back to references search, we open a new editor with the results"
21546 );
21547 let references_fallback_text = editors
21548 .into_iter()
21549 .find(|new_editor| *new_editor != test_editor_cx.entity())
21550 .expect("Should have one non-test editor now")
21551 .read(test_editor_cx)
21552 .text(test_editor_cx);
21553 assert_eq!(
21554 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21555 "Should use the range from the references response and not the GoToDefinition one"
21556 );
21557 });
21558}
21559
21560#[gpui::test]
21561async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21562 init_test(cx, |_| {});
21563 cx.update(|cx| {
21564 let mut editor_settings = EditorSettings::get_global(cx).clone();
21565 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21566 EditorSettings::override_global(editor_settings, cx);
21567 });
21568 let mut cx = EditorLspTestContext::new_rust(
21569 lsp::ServerCapabilities {
21570 definition_provider: Some(lsp::OneOf::Left(true)),
21571 references_provider: Some(lsp::OneOf::Left(true)),
21572 ..lsp::ServerCapabilities::default()
21573 },
21574 cx,
21575 )
21576 .await;
21577 let original_state = r#"fn one() {
21578 let mut a = ˇtwo();
21579 }
21580
21581 fn two() {}"#
21582 .unindent();
21583 cx.set_state(&original_state);
21584
21585 let mut go_to_definition = cx
21586 .lsp
21587 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21588 move |_, _| async move { Ok(None) },
21589 );
21590 let _references = cx
21591 .lsp
21592 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21593 panic!("Should not call for references with no go to definition fallback")
21594 });
21595
21596 let navigated = cx
21597 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21598 .await
21599 .expect("Failed to navigate to lookup references");
21600 go_to_definition
21601 .next()
21602 .await
21603 .expect("Should have called the go_to_definition handler");
21604
21605 assert_eq!(
21606 navigated,
21607 Navigated::No,
21608 "Should have navigated to references as a fallback after empty GoToDefinition response"
21609 );
21610 cx.assert_editor_state(&original_state);
21611 let editors = cx.update_workspace(|workspace, _, cx| {
21612 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21613 });
21614 cx.update_editor(|_, _, _| {
21615 assert_eq!(
21616 editors.len(),
21617 1,
21618 "After unsuccessful fallback, no other editor should have been opened"
21619 );
21620 });
21621}
21622
21623#[gpui::test]
21624async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21625 init_test(cx, |_| {});
21626 let mut cx = EditorLspTestContext::new_rust(
21627 lsp::ServerCapabilities {
21628 references_provider: Some(lsp::OneOf::Left(true)),
21629 ..lsp::ServerCapabilities::default()
21630 },
21631 cx,
21632 )
21633 .await;
21634
21635 cx.set_state(
21636 &r#"
21637 fn one() {
21638 let mut a = two();
21639 }
21640
21641 fn ˇtwo() {}"#
21642 .unindent(),
21643 );
21644 cx.lsp
21645 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21646 Ok(Some(vec![
21647 lsp::Location {
21648 uri: params.text_document_position.text_document.uri.clone(),
21649 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21650 },
21651 lsp::Location {
21652 uri: params.text_document_position.text_document.uri,
21653 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21654 },
21655 ]))
21656 });
21657 let navigated = cx
21658 .update_editor(|editor, window, cx| {
21659 editor.find_all_references(&FindAllReferences, window, cx)
21660 })
21661 .unwrap()
21662 .await
21663 .expect("Failed to navigate to references");
21664 assert_eq!(
21665 navigated,
21666 Navigated::Yes,
21667 "Should have navigated to references from the FindAllReferences response"
21668 );
21669 cx.assert_editor_state(
21670 &r#"fn one() {
21671 let mut a = two();
21672 }
21673
21674 fn ˇtwo() {}"#
21675 .unindent(),
21676 );
21677
21678 let editors = cx.update_workspace(|workspace, _, cx| {
21679 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21680 });
21681 cx.update_editor(|_, _, _| {
21682 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21683 });
21684
21685 cx.set_state(
21686 &r#"fn one() {
21687 let mut a = ˇtwo();
21688 }
21689
21690 fn two() {}"#
21691 .unindent(),
21692 );
21693 let navigated = cx
21694 .update_editor(|editor, window, cx| {
21695 editor.find_all_references(&FindAllReferences, window, cx)
21696 })
21697 .unwrap()
21698 .await
21699 .expect("Failed to navigate to references");
21700 assert_eq!(
21701 navigated,
21702 Navigated::Yes,
21703 "Should have navigated to references from the FindAllReferences response"
21704 );
21705 cx.assert_editor_state(
21706 &r#"fn one() {
21707 let mut a = ˇtwo();
21708 }
21709
21710 fn two() {}"#
21711 .unindent(),
21712 );
21713 let editors = cx.update_workspace(|workspace, _, cx| {
21714 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21715 });
21716 cx.update_editor(|_, _, _| {
21717 assert_eq!(
21718 editors.len(),
21719 2,
21720 "should have re-used the previous multibuffer"
21721 );
21722 });
21723
21724 cx.set_state(
21725 &r#"fn one() {
21726 let mut a = ˇtwo();
21727 }
21728 fn three() {}
21729 fn two() {}"#
21730 .unindent(),
21731 );
21732 cx.lsp
21733 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21734 Ok(Some(vec![
21735 lsp::Location {
21736 uri: params.text_document_position.text_document.uri.clone(),
21737 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21738 },
21739 lsp::Location {
21740 uri: params.text_document_position.text_document.uri,
21741 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21742 },
21743 ]))
21744 });
21745 let navigated = cx
21746 .update_editor(|editor, window, cx| {
21747 editor.find_all_references(&FindAllReferences, window, cx)
21748 })
21749 .unwrap()
21750 .await
21751 .expect("Failed to navigate to references");
21752 assert_eq!(
21753 navigated,
21754 Navigated::Yes,
21755 "Should have navigated to references from the FindAllReferences response"
21756 );
21757 cx.assert_editor_state(
21758 &r#"fn one() {
21759 let mut a = ˇtwo();
21760 }
21761 fn three() {}
21762 fn two() {}"#
21763 .unindent(),
21764 );
21765 let editors = cx.update_workspace(|workspace, _, cx| {
21766 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21767 });
21768 cx.update_editor(|_, _, _| {
21769 assert_eq!(
21770 editors.len(),
21771 3,
21772 "should have used a new multibuffer as offsets changed"
21773 );
21774 });
21775}
21776#[gpui::test]
21777async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21778 init_test(cx, |_| {});
21779
21780 let language = Arc::new(Language::new(
21781 LanguageConfig::default(),
21782 Some(tree_sitter_rust::LANGUAGE.into()),
21783 ));
21784
21785 let text = r#"
21786 #[cfg(test)]
21787 mod tests() {
21788 #[test]
21789 fn runnable_1() {
21790 let a = 1;
21791 }
21792
21793 #[test]
21794 fn runnable_2() {
21795 let a = 1;
21796 let b = 2;
21797 }
21798 }
21799 "#
21800 .unindent();
21801
21802 let fs = FakeFs::new(cx.executor());
21803 fs.insert_file("/file.rs", Default::default()).await;
21804
21805 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21806 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21807 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21808 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21809 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21810
21811 let editor = cx.new_window_entity(|window, cx| {
21812 Editor::new(
21813 EditorMode::full(),
21814 multi_buffer,
21815 Some(project.clone()),
21816 window,
21817 cx,
21818 )
21819 });
21820
21821 editor.update_in(cx, |editor, window, cx| {
21822 let snapshot = editor.buffer().read(cx).snapshot(cx);
21823 editor.tasks.insert(
21824 (buffer.read(cx).remote_id(), 3),
21825 RunnableTasks {
21826 templates: vec![],
21827 offset: snapshot.anchor_before(43),
21828 column: 0,
21829 extra_variables: HashMap::default(),
21830 context_range: BufferOffset(43)..BufferOffset(85),
21831 },
21832 );
21833 editor.tasks.insert(
21834 (buffer.read(cx).remote_id(), 8),
21835 RunnableTasks {
21836 templates: vec![],
21837 offset: snapshot.anchor_before(86),
21838 column: 0,
21839 extra_variables: HashMap::default(),
21840 context_range: BufferOffset(86)..BufferOffset(191),
21841 },
21842 );
21843
21844 // Test finding task when cursor is inside function body
21845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21846 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21847 });
21848 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21849 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21850
21851 // Test finding task when cursor is on function name
21852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21853 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21854 });
21855 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21856 assert_eq!(row, 8, "Should find task when cursor is on function name");
21857 });
21858}
21859
21860#[gpui::test]
21861async fn test_folding_buffers(cx: &mut TestAppContext) {
21862 init_test(cx, |_| {});
21863
21864 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21865 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21866 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21867
21868 let fs = FakeFs::new(cx.executor());
21869 fs.insert_tree(
21870 path!("/a"),
21871 json!({
21872 "first.rs": sample_text_1,
21873 "second.rs": sample_text_2,
21874 "third.rs": sample_text_3,
21875 }),
21876 )
21877 .await;
21878 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21879 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21880 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21881 let worktree = project.update(cx, |project, cx| {
21882 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21883 assert_eq!(worktrees.len(), 1);
21884 worktrees.pop().unwrap()
21885 });
21886 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21887
21888 let buffer_1 = project
21889 .update(cx, |project, cx| {
21890 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21891 })
21892 .await
21893 .unwrap();
21894 let buffer_2 = project
21895 .update(cx, |project, cx| {
21896 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21897 })
21898 .await
21899 .unwrap();
21900 let buffer_3 = project
21901 .update(cx, |project, cx| {
21902 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21903 })
21904 .await
21905 .unwrap();
21906
21907 let multi_buffer = cx.new(|cx| {
21908 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21909 multi_buffer.push_excerpts(
21910 buffer_1.clone(),
21911 [
21912 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21913 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21914 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21915 ],
21916 cx,
21917 );
21918 multi_buffer.push_excerpts(
21919 buffer_2.clone(),
21920 [
21921 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21922 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21923 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21924 ],
21925 cx,
21926 );
21927 multi_buffer.push_excerpts(
21928 buffer_3.clone(),
21929 [
21930 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21931 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21932 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21933 ],
21934 cx,
21935 );
21936 multi_buffer
21937 });
21938 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21939 Editor::new(
21940 EditorMode::full(),
21941 multi_buffer.clone(),
21942 Some(project.clone()),
21943 window,
21944 cx,
21945 )
21946 });
21947
21948 assert_eq!(
21949 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21950 "\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",
21951 );
21952
21953 multi_buffer_editor.update(cx, |editor, cx| {
21954 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21955 });
21956 assert_eq!(
21957 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21958 "\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",
21959 "After folding the first buffer, its text should not be displayed"
21960 );
21961
21962 multi_buffer_editor.update(cx, |editor, cx| {
21963 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21964 });
21965 assert_eq!(
21966 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21967 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21968 "After folding the second buffer, its text should not be displayed"
21969 );
21970
21971 multi_buffer_editor.update(cx, |editor, cx| {
21972 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21973 });
21974 assert_eq!(
21975 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21976 "\n\n\n\n\n",
21977 "After folding the third buffer, its text should not be displayed"
21978 );
21979
21980 // Emulate selection inside the fold logic, that should work
21981 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21982 editor
21983 .snapshot(window, cx)
21984 .next_line_boundary(Point::new(0, 4));
21985 });
21986
21987 multi_buffer_editor.update(cx, |editor, cx| {
21988 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21989 });
21990 assert_eq!(
21991 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21992 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21993 "After unfolding the second buffer, its text should be displayed"
21994 );
21995
21996 // Typing inside of buffer 1 causes that buffer to be unfolded.
21997 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21998 assert_eq!(
21999 multi_buffer
22000 .read(cx)
22001 .snapshot(cx)
22002 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22003 .collect::<String>(),
22004 "bbbb"
22005 );
22006 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22007 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22008 });
22009 editor.handle_input("B", window, cx);
22010 });
22011
22012 assert_eq!(
22013 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22014 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22015 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22016 );
22017
22018 multi_buffer_editor.update(cx, |editor, cx| {
22019 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22020 });
22021 assert_eq!(
22022 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22023 "\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",
22024 "After unfolding the all buffers, all original text should be displayed"
22025 );
22026}
22027
22028#[gpui::test]
22029async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22030 init_test(cx, |_| {});
22031
22032 let sample_text_1 = "1111\n2222\n3333".to_string();
22033 let sample_text_2 = "4444\n5555\n6666".to_string();
22034 let sample_text_3 = "7777\n8888\n9999".to_string();
22035
22036 let fs = FakeFs::new(cx.executor());
22037 fs.insert_tree(
22038 path!("/a"),
22039 json!({
22040 "first.rs": sample_text_1,
22041 "second.rs": sample_text_2,
22042 "third.rs": sample_text_3,
22043 }),
22044 )
22045 .await;
22046 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22047 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22048 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22049 let worktree = project.update(cx, |project, cx| {
22050 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22051 assert_eq!(worktrees.len(), 1);
22052 worktrees.pop().unwrap()
22053 });
22054 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22055
22056 let buffer_1 = project
22057 .update(cx, |project, cx| {
22058 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22059 })
22060 .await
22061 .unwrap();
22062 let buffer_2 = project
22063 .update(cx, |project, cx| {
22064 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22065 })
22066 .await
22067 .unwrap();
22068 let buffer_3 = project
22069 .update(cx, |project, cx| {
22070 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22071 })
22072 .await
22073 .unwrap();
22074
22075 let multi_buffer = cx.new(|cx| {
22076 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22077 multi_buffer.push_excerpts(
22078 buffer_1.clone(),
22079 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22080 cx,
22081 );
22082 multi_buffer.push_excerpts(
22083 buffer_2.clone(),
22084 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22085 cx,
22086 );
22087 multi_buffer.push_excerpts(
22088 buffer_3.clone(),
22089 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22090 cx,
22091 );
22092 multi_buffer
22093 });
22094
22095 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22096 Editor::new(
22097 EditorMode::full(),
22098 multi_buffer,
22099 Some(project.clone()),
22100 window,
22101 cx,
22102 )
22103 });
22104
22105 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22106 assert_eq!(
22107 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22108 full_text,
22109 );
22110
22111 multi_buffer_editor.update(cx, |editor, cx| {
22112 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22113 });
22114 assert_eq!(
22115 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22116 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22117 "After folding the first buffer, its text should not be displayed"
22118 );
22119
22120 multi_buffer_editor.update(cx, |editor, cx| {
22121 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22122 });
22123
22124 assert_eq!(
22125 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22126 "\n\n\n\n\n\n7777\n8888\n9999",
22127 "After folding the second buffer, its text should not be displayed"
22128 );
22129
22130 multi_buffer_editor.update(cx, |editor, cx| {
22131 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22132 });
22133 assert_eq!(
22134 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22135 "\n\n\n\n\n",
22136 "After folding the third buffer, its text should not be displayed"
22137 );
22138
22139 multi_buffer_editor.update(cx, |editor, cx| {
22140 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22141 });
22142 assert_eq!(
22143 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22144 "\n\n\n\n4444\n5555\n6666\n\n",
22145 "After unfolding the second buffer, its text should be displayed"
22146 );
22147
22148 multi_buffer_editor.update(cx, |editor, cx| {
22149 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22150 });
22151 assert_eq!(
22152 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22153 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22154 "After unfolding the first buffer, its text should be displayed"
22155 );
22156
22157 multi_buffer_editor.update(cx, |editor, cx| {
22158 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22159 });
22160 assert_eq!(
22161 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22162 full_text,
22163 "After unfolding all buffers, all original text should be displayed"
22164 );
22165}
22166
22167#[gpui::test]
22168async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22169 init_test(cx, |_| {});
22170
22171 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22172
22173 let fs = FakeFs::new(cx.executor());
22174 fs.insert_tree(
22175 path!("/a"),
22176 json!({
22177 "main.rs": sample_text,
22178 }),
22179 )
22180 .await;
22181 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22182 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22183 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22184 let worktree = project.update(cx, |project, cx| {
22185 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22186 assert_eq!(worktrees.len(), 1);
22187 worktrees.pop().unwrap()
22188 });
22189 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22190
22191 let buffer_1 = project
22192 .update(cx, |project, cx| {
22193 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22194 })
22195 .await
22196 .unwrap();
22197
22198 let multi_buffer = cx.new(|cx| {
22199 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22200 multi_buffer.push_excerpts(
22201 buffer_1.clone(),
22202 [ExcerptRange::new(
22203 Point::new(0, 0)
22204 ..Point::new(
22205 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22206 0,
22207 ),
22208 )],
22209 cx,
22210 );
22211 multi_buffer
22212 });
22213 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22214 Editor::new(
22215 EditorMode::full(),
22216 multi_buffer,
22217 Some(project.clone()),
22218 window,
22219 cx,
22220 )
22221 });
22222
22223 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22224 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22225 enum TestHighlight {}
22226 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22227 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22228 editor.highlight_text::<TestHighlight>(
22229 vec![highlight_range.clone()],
22230 HighlightStyle::color(Hsla::green()),
22231 cx,
22232 );
22233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22234 s.select_ranges(Some(highlight_range))
22235 });
22236 });
22237
22238 let full_text = format!("\n\n{sample_text}");
22239 assert_eq!(
22240 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22241 full_text,
22242 );
22243}
22244
22245#[gpui::test]
22246async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22247 init_test(cx, |_| {});
22248 cx.update(|cx| {
22249 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22250 "keymaps/default-linux.json",
22251 cx,
22252 )
22253 .unwrap();
22254 cx.bind_keys(default_key_bindings);
22255 });
22256
22257 let (editor, cx) = cx.add_window_view(|window, cx| {
22258 let multi_buffer = MultiBuffer::build_multi(
22259 [
22260 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22261 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22262 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22263 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22264 ],
22265 cx,
22266 );
22267 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22268
22269 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22270 // fold all but the second buffer, so that we test navigating between two
22271 // adjacent folded buffers, as well as folded buffers at the start and
22272 // end the multibuffer
22273 editor.fold_buffer(buffer_ids[0], cx);
22274 editor.fold_buffer(buffer_ids[2], cx);
22275 editor.fold_buffer(buffer_ids[3], cx);
22276
22277 editor
22278 });
22279 cx.simulate_resize(size(px(1000.), px(1000.)));
22280
22281 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22282 cx.assert_excerpts_with_selections(indoc! {"
22283 [EXCERPT]
22284 ˇ[FOLDED]
22285 [EXCERPT]
22286 a1
22287 b1
22288 [EXCERPT]
22289 [FOLDED]
22290 [EXCERPT]
22291 [FOLDED]
22292 "
22293 });
22294 cx.simulate_keystroke("down");
22295 cx.assert_excerpts_with_selections(indoc! {"
22296 [EXCERPT]
22297 [FOLDED]
22298 [EXCERPT]
22299 ˇa1
22300 b1
22301 [EXCERPT]
22302 [FOLDED]
22303 [EXCERPT]
22304 [FOLDED]
22305 "
22306 });
22307 cx.simulate_keystroke("down");
22308 cx.assert_excerpts_with_selections(indoc! {"
22309 [EXCERPT]
22310 [FOLDED]
22311 [EXCERPT]
22312 a1
22313 ˇb1
22314 [EXCERPT]
22315 [FOLDED]
22316 [EXCERPT]
22317 [FOLDED]
22318 "
22319 });
22320 cx.simulate_keystroke("down");
22321 cx.assert_excerpts_with_selections(indoc! {"
22322 [EXCERPT]
22323 [FOLDED]
22324 [EXCERPT]
22325 a1
22326 b1
22327 ˇ[EXCERPT]
22328 [FOLDED]
22329 [EXCERPT]
22330 [FOLDED]
22331 "
22332 });
22333 cx.simulate_keystroke("down");
22334 cx.assert_excerpts_with_selections(indoc! {"
22335 [EXCERPT]
22336 [FOLDED]
22337 [EXCERPT]
22338 a1
22339 b1
22340 [EXCERPT]
22341 ˇ[FOLDED]
22342 [EXCERPT]
22343 [FOLDED]
22344 "
22345 });
22346 for _ in 0..5 {
22347 cx.simulate_keystroke("down");
22348 cx.assert_excerpts_with_selections(indoc! {"
22349 [EXCERPT]
22350 [FOLDED]
22351 [EXCERPT]
22352 a1
22353 b1
22354 [EXCERPT]
22355 [FOLDED]
22356 [EXCERPT]
22357 ˇ[FOLDED]
22358 "
22359 });
22360 }
22361
22362 cx.simulate_keystroke("up");
22363 cx.assert_excerpts_with_selections(indoc! {"
22364 [EXCERPT]
22365 [FOLDED]
22366 [EXCERPT]
22367 a1
22368 b1
22369 [EXCERPT]
22370 ˇ[FOLDED]
22371 [EXCERPT]
22372 [FOLDED]
22373 "
22374 });
22375 cx.simulate_keystroke("up");
22376 cx.assert_excerpts_with_selections(indoc! {"
22377 [EXCERPT]
22378 [FOLDED]
22379 [EXCERPT]
22380 a1
22381 b1
22382 ˇ[EXCERPT]
22383 [FOLDED]
22384 [EXCERPT]
22385 [FOLDED]
22386 "
22387 });
22388 cx.simulate_keystroke("up");
22389 cx.assert_excerpts_with_selections(indoc! {"
22390 [EXCERPT]
22391 [FOLDED]
22392 [EXCERPT]
22393 a1
22394 ˇb1
22395 [EXCERPT]
22396 [FOLDED]
22397 [EXCERPT]
22398 [FOLDED]
22399 "
22400 });
22401 cx.simulate_keystroke("up");
22402 cx.assert_excerpts_with_selections(indoc! {"
22403 [EXCERPT]
22404 [FOLDED]
22405 [EXCERPT]
22406 ˇa1
22407 b1
22408 [EXCERPT]
22409 [FOLDED]
22410 [EXCERPT]
22411 [FOLDED]
22412 "
22413 });
22414 for _ in 0..5 {
22415 cx.simulate_keystroke("up");
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 }
22429}
22430
22431#[gpui::test]
22432async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22433 init_test(cx, |_| {});
22434
22435 // Simple insertion
22436 assert_highlighted_edits(
22437 "Hello, world!",
22438 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22439 true,
22440 cx,
22441 |highlighted_edits, cx| {
22442 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22443 assert_eq!(highlighted_edits.highlights.len(), 1);
22444 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22445 assert_eq!(
22446 highlighted_edits.highlights[0].1.background_color,
22447 Some(cx.theme().status().created_background)
22448 );
22449 },
22450 )
22451 .await;
22452
22453 // Replacement
22454 assert_highlighted_edits(
22455 "This is a test.",
22456 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22457 false,
22458 cx,
22459 |highlighted_edits, cx| {
22460 assert_eq!(highlighted_edits.text, "That is a test.");
22461 assert_eq!(highlighted_edits.highlights.len(), 1);
22462 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22463 assert_eq!(
22464 highlighted_edits.highlights[0].1.background_color,
22465 Some(cx.theme().status().created_background)
22466 );
22467 },
22468 )
22469 .await;
22470
22471 // Multiple edits
22472 assert_highlighted_edits(
22473 "Hello, world!",
22474 vec![
22475 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22476 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22477 ],
22478 false,
22479 cx,
22480 |highlighted_edits, cx| {
22481 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22482 assert_eq!(highlighted_edits.highlights.len(), 2);
22483 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22484 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22485 assert_eq!(
22486 highlighted_edits.highlights[0].1.background_color,
22487 Some(cx.theme().status().created_background)
22488 );
22489 assert_eq!(
22490 highlighted_edits.highlights[1].1.background_color,
22491 Some(cx.theme().status().created_background)
22492 );
22493 },
22494 )
22495 .await;
22496
22497 // Multiple lines with edits
22498 assert_highlighted_edits(
22499 "First line\nSecond line\nThird line\nFourth line",
22500 vec![
22501 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22502 (
22503 Point::new(2, 0)..Point::new(2, 10),
22504 "New third line".to_string(),
22505 ),
22506 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22507 ],
22508 false,
22509 cx,
22510 |highlighted_edits, cx| {
22511 assert_eq!(
22512 highlighted_edits.text,
22513 "Second modified\nNew third line\nFourth updated line"
22514 );
22515 assert_eq!(highlighted_edits.highlights.len(), 3);
22516 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22517 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22518 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22519 for highlight in &highlighted_edits.highlights {
22520 assert_eq!(
22521 highlight.1.background_color,
22522 Some(cx.theme().status().created_background)
22523 );
22524 }
22525 },
22526 )
22527 .await;
22528}
22529
22530#[gpui::test]
22531async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22532 init_test(cx, |_| {});
22533
22534 // Deletion
22535 assert_highlighted_edits(
22536 "Hello, world!",
22537 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22538 true,
22539 cx,
22540 |highlighted_edits, cx| {
22541 assert_eq!(highlighted_edits.text, "Hello, world!");
22542 assert_eq!(highlighted_edits.highlights.len(), 1);
22543 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22544 assert_eq!(
22545 highlighted_edits.highlights[0].1.background_color,
22546 Some(cx.theme().status().deleted_background)
22547 );
22548 },
22549 )
22550 .await;
22551
22552 // Insertion
22553 assert_highlighted_edits(
22554 "Hello, world!",
22555 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22556 true,
22557 cx,
22558 |highlighted_edits, cx| {
22559 assert_eq!(highlighted_edits.highlights.len(), 1);
22560 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22561 assert_eq!(
22562 highlighted_edits.highlights[0].1.background_color,
22563 Some(cx.theme().status().created_background)
22564 );
22565 },
22566 )
22567 .await;
22568}
22569
22570async fn assert_highlighted_edits(
22571 text: &str,
22572 edits: Vec<(Range<Point>, String)>,
22573 include_deletions: bool,
22574 cx: &mut TestAppContext,
22575 assertion_fn: impl Fn(HighlightedText, &App),
22576) {
22577 let window = cx.add_window(|window, cx| {
22578 let buffer = MultiBuffer::build_simple(text, cx);
22579 Editor::new(EditorMode::full(), buffer, None, window, cx)
22580 });
22581 let cx = &mut VisualTestContext::from_window(*window, cx);
22582
22583 let (buffer, snapshot) = window
22584 .update(cx, |editor, _window, cx| {
22585 (
22586 editor.buffer().clone(),
22587 editor.buffer().read(cx).snapshot(cx),
22588 )
22589 })
22590 .unwrap();
22591
22592 let edits = edits
22593 .into_iter()
22594 .map(|(range, edit)| {
22595 (
22596 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22597 edit,
22598 )
22599 })
22600 .collect::<Vec<_>>();
22601
22602 let text_anchor_edits = edits
22603 .clone()
22604 .into_iter()
22605 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22606 .collect::<Vec<_>>();
22607
22608 let edit_preview = window
22609 .update(cx, |_, _window, cx| {
22610 buffer
22611 .read(cx)
22612 .as_singleton()
22613 .unwrap()
22614 .read(cx)
22615 .preview_edits(text_anchor_edits.into(), cx)
22616 })
22617 .unwrap()
22618 .await;
22619
22620 cx.update(|_window, cx| {
22621 let highlighted_edits = edit_prediction_edit_text(
22622 snapshot.as_singleton().unwrap().2,
22623 &edits,
22624 &edit_preview,
22625 include_deletions,
22626 cx,
22627 );
22628 assertion_fn(highlighted_edits, cx)
22629 });
22630}
22631
22632#[track_caller]
22633fn assert_breakpoint(
22634 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22635 path: &Arc<Path>,
22636 expected: Vec<(u32, Breakpoint)>,
22637) {
22638 if expected.is_empty() {
22639 assert!(!breakpoints.contains_key(path), "{}", path.display());
22640 } else {
22641 let mut breakpoint = breakpoints
22642 .get(path)
22643 .unwrap()
22644 .iter()
22645 .map(|breakpoint| {
22646 (
22647 breakpoint.row,
22648 Breakpoint {
22649 message: breakpoint.message.clone(),
22650 state: breakpoint.state,
22651 condition: breakpoint.condition.clone(),
22652 hit_condition: breakpoint.hit_condition.clone(),
22653 },
22654 )
22655 })
22656 .collect::<Vec<_>>();
22657
22658 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22659
22660 assert_eq!(expected, breakpoint);
22661 }
22662}
22663
22664fn add_log_breakpoint_at_cursor(
22665 editor: &mut Editor,
22666 log_message: &str,
22667 window: &mut Window,
22668 cx: &mut Context<Editor>,
22669) {
22670 let (anchor, bp) = editor
22671 .breakpoints_at_cursors(window, cx)
22672 .first()
22673 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22674 .unwrap_or_else(|| {
22675 let cursor_position: Point = editor.selections.newest(cx).head();
22676
22677 let breakpoint_position = editor
22678 .snapshot(window, cx)
22679 .display_snapshot
22680 .buffer_snapshot()
22681 .anchor_before(Point::new(cursor_position.row, 0));
22682
22683 (breakpoint_position, Breakpoint::new_log(log_message))
22684 });
22685
22686 editor.edit_breakpoint_at_anchor(
22687 anchor,
22688 bp,
22689 BreakpointEditAction::EditLogMessage(log_message.into()),
22690 cx,
22691 );
22692}
22693
22694#[gpui::test]
22695async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22696 init_test(cx, |_| {});
22697
22698 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22699 let fs = FakeFs::new(cx.executor());
22700 fs.insert_tree(
22701 path!("/a"),
22702 json!({
22703 "main.rs": sample_text,
22704 }),
22705 )
22706 .await;
22707 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22708 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22709 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22710
22711 let fs = FakeFs::new(cx.executor());
22712 fs.insert_tree(
22713 path!("/a"),
22714 json!({
22715 "main.rs": sample_text,
22716 }),
22717 )
22718 .await;
22719 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22720 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22721 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22722 let worktree_id = workspace
22723 .update(cx, |workspace, _window, cx| {
22724 workspace.project().update(cx, |project, cx| {
22725 project.worktrees(cx).next().unwrap().read(cx).id()
22726 })
22727 })
22728 .unwrap();
22729
22730 let buffer = project
22731 .update(cx, |project, cx| {
22732 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22733 })
22734 .await
22735 .unwrap();
22736
22737 let (editor, cx) = cx.add_window_view(|window, cx| {
22738 Editor::new(
22739 EditorMode::full(),
22740 MultiBuffer::build_from_buffer(buffer, cx),
22741 Some(project.clone()),
22742 window,
22743 cx,
22744 )
22745 });
22746
22747 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22748 let abs_path = project.read_with(cx, |project, cx| {
22749 project
22750 .absolute_path(&project_path, cx)
22751 .map(Arc::from)
22752 .unwrap()
22753 });
22754
22755 // assert we can add breakpoint on the first line
22756 editor.update_in(cx, |editor, window, cx| {
22757 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22758 editor.move_to_end(&MoveToEnd, window, cx);
22759 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22760 });
22761
22762 let breakpoints = editor.update(cx, |editor, cx| {
22763 editor
22764 .breakpoint_store()
22765 .as_ref()
22766 .unwrap()
22767 .read(cx)
22768 .all_source_breakpoints(cx)
22769 });
22770
22771 assert_eq!(1, breakpoints.len());
22772 assert_breakpoint(
22773 &breakpoints,
22774 &abs_path,
22775 vec![
22776 (0, Breakpoint::new_standard()),
22777 (3, Breakpoint::new_standard()),
22778 ],
22779 );
22780
22781 editor.update_in(cx, |editor, window, cx| {
22782 editor.move_to_beginning(&MoveToBeginning, window, cx);
22783 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22784 });
22785
22786 let breakpoints = editor.update(cx, |editor, cx| {
22787 editor
22788 .breakpoint_store()
22789 .as_ref()
22790 .unwrap()
22791 .read(cx)
22792 .all_source_breakpoints(cx)
22793 });
22794
22795 assert_eq!(1, breakpoints.len());
22796 assert_breakpoint(
22797 &breakpoints,
22798 &abs_path,
22799 vec![(3, Breakpoint::new_standard())],
22800 );
22801
22802 editor.update_in(cx, |editor, window, cx| {
22803 editor.move_to_end(&MoveToEnd, window, cx);
22804 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22805 });
22806
22807 let breakpoints = editor.update(cx, |editor, cx| {
22808 editor
22809 .breakpoint_store()
22810 .as_ref()
22811 .unwrap()
22812 .read(cx)
22813 .all_source_breakpoints(cx)
22814 });
22815
22816 assert_eq!(0, breakpoints.len());
22817 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22818}
22819
22820#[gpui::test]
22821async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22822 init_test(cx, |_| {});
22823
22824 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22825
22826 let fs = FakeFs::new(cx.executor());
22827 fs.insert_tree(
22828 path!("/a"),
22829 json!({
22830 "main.rs": sample_text,
22831 }),
22832 )
22833 .await;
22834 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22835 let (workspace, cx) =
22836 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22837
22838 let worktree_id = workspace.update(cx, |workspace, cx| {
22839 workspace.project().update(cx, |project, cx| {
22840 project.worktrees(cx).next().unwrap().read(cx).id()
22841 })
22842 });
22843
22844 let buffer = project
22845 .update(cx, |project, cx| {
22846 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22847 })
22848 .await
22849 .unwrap();
22850
22851 let (editor, cx) = cx.add_window_view(|window, cx| {
22852 Editor::new(
22853 EditorMode::full(),
22854 MultiBuffer::build_from_buffer(buffer, cx),
22855 Some(project.clone()),
22856 window,
22857 cx,
22858 )
22859 });
22860
22861 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22862 let abs_path = project.read_with(cx, |project, cx| {
22863 project
22864 .absolute_path(&project_path, cx)
22865 .map(Arc::from)
22866 .unwrap()
22867 });
22868
22869 editor.update_in(cx, |editor, window, cx| {
22870 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22871 });
22872
22873 let breakpoints = editor.update(cx, |editor, cx| {
22874 editor
22875 .breakpoint_store()
22876 .as_ref()
22877 .unwrap()
22878 .read(cx)
22879 .all_source_breakpoints(cx)
22880 });
22881
22882 assert_breakpoint(
22883 &breakpoints,
22884 &abs_path,
22885 vec![(0, Breakpoint::new_log("hello world"))],
22886 );
22887
22888 // Removing a log message from a log breakpoint should remove it
22889 editor.update_in(cx, |editor, window, cx| {
22890 add_log_breakpoint_at_cursor(editor, "", window, cx);
22891 });
22892
22893 let breakpoints = editor.update(cx, |editor, cx| {
22894 editor
22895 .breakpoint_store()
22896 .as_ref()
22897 .unwrap()
22898 .read(cx)
22899 .all_source_breakpoints(cx)
22900 });
22901
22902 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22903
22904 editor.update_in(cx, |editor, window, cx| {
22905 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22906 editor.move_to_end(&MoveToEnd, window, cx);
22907 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22908 // Not adding a log message to a standard breakpoint shouldn't remove it
22909 add_log_breakpoint_at_cursor(editor, "", window, cx);
22910 });
22911
22912 let breakpoints = editor.update(cx, |editor, cx| {
22913 editor
22914 .breakpoint_store()
22915 .as_ref()
22916 .unwrap()
22917 .read(cx)
22918 .all_source_breakpoints(cx)
22919 });
22920
22921 assert_breakpoint(
22922 &breakpoints,
22923 &abs_path,
22924 vec![
22925 (0, Breakpoint::new_standard()),
22926 (3, Breakpoint::new_standard()),
22927 ],
22928 );
22929
22930 editor.update_in(cx, |editor, window, cx| {
22931 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22932 });
22933
22934 let breakpoints = editor.update(cx, |editor, cx| {
22935 editor
22936 .breakpoint_store()
22937 .as_ref()
22938 .unwrap()
22939 .read(cx)
22940 .all_source_breakpoints(cx)
22941 });
22942
22943 assert_breakpoint(
22944 &breakpoints,
22945 &abs_path,
22946 vec![
22947 (0, Breakpoint::new_standard()),
22948 (3, Breakpoint::new_log("hello world")),
22949 ],
22950 );
22951
22952 editor.update_in(cx, |editor, window, cx| {
22953 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22954 });
22955
22956 let breakpoints = editor.update(cx, |editor, cx| {
22957 editor
22958 .breakpoint_store()
22959 .as_ref()
22960 .unwrap()
22961 .read(cx)
22962 .all_source_breakpoints(cx)
22963 });
22964
22965 assert_breakpoint(
22966 &breakpoints,
22967 &abs_path,
22968 vec![
22969 (0, Breakpoint::new_standard()),
22970 (3, Breakpoint::new_log("hello Earth!!")),
22971 ],
22972 );
22973}
22974
22975/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22976/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22977/// or when breakpoints were placed out of order. This tests for a regression too
22978#[gpui::test]
22979async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22980 init_test(cx, |_| {});
22981
22982 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22983 let fs = FakeFs::new(cx.executor());
22984 fs.insert_tree(
22985 path!("/a"),
22986 json!({
22987 "main.rs": sample_text,
22988 }),
22989 )
22990 .await;
22991 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22992 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22993 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22994
22995 let fs = FakeFs::new(cx.executor());
22996 fs.insert_tree(
22997 path!("/a"),
22998 json!({
22999 "main.rs": sample_text,
23000 }),
23001 )
23002 .await;
23003 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23004 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23005 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23006 let worktree_id = workspace
23007 .update(cx, |workspace, _window, cx| {
23008 workspace.project().update(cx, |project, cx| {
23009 project.worktrees(cx).next().unwrap().read(cx).id()
23010 })
23011 })
23012 .unwrap();
23013
23014 let buffer = project
23015 .update(cx, |project, cx| {
23016 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23017 })
23018 .await
23019 .unwrap();
23020
23021 let (editor, cx) = cx.add_window_view(|window, cx| {
23022 Editor::new(
23023 EditorMode::full(),
23024 MultiBuffer::build_from_buffer(buffer, cx),
23025 Some(project.clone()),
23026 window,
23027 cx,
23028 )
23029 });
23030
23031 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23032 let abs_path = project.read_with(cx, |project, cx| {
23033 project
23034 .absolute_path(&project_path, cx)
23035 .map(Arc::from)
23036 .unwrap()
23037 });
23038
23039 // assert we can add breakpoint on the first line
23040 editor.update_in(cx, |editor, window, cx| {
23041 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23042 editor.move_to_end(&MoveToEnd, window, cx);
23043 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23044 editor.move_up(&MoveUp, window, cx);
23045 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23046 });
23047
23048 let breakpoints = editor.update(cx, |editor, cx| {
23049 editor
23050 .breakpoint_store()
23051 .as_ref()
23052 .unwrap()
23053 .read(cx)
23054 .all_source_breakpoints(cx)
23055 });
23056
23057 assert_eq!(1, breakpoints.len());
23058 assert_breakpoint(
23059 &breakpoints,
23060 &abs_path,
23061 vec![
23062 (0, Breakpoint::new_standard()),
23063 (2, Breakpoint::new_standard()),
23064 (3, Breakpoint::new_standard()),
23065 ],
23066 );
23067
23068 editor.update_in(cx, |editor, window, cx| {
23069 editor.move_to_beginning(&MoveToBeginning, window, cx);
23070 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23071 editor.move_to_end(&MoveToEnd, window, cx);
23072 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23073 // Disabling a breakpoint that doesn't exist should do nothing
23074 editor.move_up(&MoveUp, window, cx);
23075 editor.move_up(&MoveUp, window, cx);
23076 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23077 });
23078
23079 let breakpoints = editor.update(cx, |editor, cx| {
23080 editor
23081 .breakpoint_store()
23082 .as_ref()
23083 .unwrap()
23084 .read(cx)
23085 .all_source_breakpoints(cx)
23086 });
23087
23088 let disable_breakpoint = {
23089 let mut bp = Breakpoint::new_standard();
23090 bp.state = BreakpointState::Disabled;
23091 bp
23092 };
23093
23094 assert_eq!(1, breakpoints.len());
23095 assert_breakpoint(
23096 &breakpoints,
23097 &abs_path,
23098 vec![
23099 (0, disable_breakpoint.clone()),
23100 (2, Breakpoint::new_standard()),
23101 (3, disable_breakpoint.clone()),
23102 ],
23103 );
23104
23105 editor.update_in(cx, |editor, window, cx| {
23106 editor.move_to_beginning(&MoveToBeginning, window, cx);
23107 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23108 editor.move_to_end(&MoveToEnd, window, cx);
23109 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23110 editor.move_up(&MoveUp, window, cx);
23111 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23112 });
23113
23114 let breakpoints = editor.update(cx, |editor, cx| {
23115 editor
23116 .breakpoint_store()
23117 .as_ref()
23118 .unwrap()
23119 .read(cx)
23120 .all_source_breakpoints(cx)
23121 });
23122
23123 assert_eq!(1, breakpoints.len());
23124 assert_breakpoint(
23125 &breakpoints,
23126 &abs_path,
23127 vec![
23128 (0, Breakpoint::new_standard()),
23129 (2, disable_breakpoint),
23130 (3, Breakpoint::new_standard()),
23131 ],
23132 );
23133}
23134
23135#[gpui::test]
23136async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23137 init_test(cx, |_| {});
23138 let capabilities = lsp::ServerCapabilities {
23139 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23140 prepare_provider: Some(true),
23141 work_done_progress_options: Default::default(),
23142 })),
23143 ..Default::default()
23144 };
23145 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23146
23147 cx.set_state(indoc! {"
23148 struct Fˇoo {}
23149 "});
23150
23151 cx.update_editor(|editor, _, cx| {
23152 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23153 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23154 editor.highlight_background::<DocumentHighlightRead>(
23155 &[highlight_range],
23156 |theme| theme.colors().editor_document_highlight_read_background,
23157 cx,
23158 );
23159 });
23160
23161 let mut prepare_rename_handler = cx
23162 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23163 move |_, _, _| async move {
23164 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23165 start: lsp::Position {
23166 line: 0,
23167 character: 7,
23168 },
23169 end: lsp::Position {
23170 line: 0,
23171 character: 10,
23172 },
23173 })))
23174 },
23175 );
23176 let prepare_rename_task = cx
23177 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23178 .expect("Prepare rename was not started");
23179 prepare_rename_handler.next().await.unwrap();
23180 prepare_rename_task.await.expect("Prepare rename failed");
23181
23182 let mut rename_handler =
23183 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23184 let edit = lsp::TextEdit {
23185 range: lsp::Range {
23186 start: lsp::Position {
23187 line: 0,
23188 character: 7,
23189 },
23190 end: lsp::Position {
23191 line: 0,
23192 character: 10,
23193 },
23194 },
23195 new_text: "FooRenamed".to_string(),
23196 };
23197 Ok(Some(lsp::WorkspaceEdit::new(
23198 // Specify the same edit twice
23199 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23200 )))
23201 });
23202 let rename_task = cx
23203 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23204 .expect("Confirm rename was not started");
23205 rename_handler.next().await.unwrap();
23206 rename_task.await.expect("Confirm rename failed");
23207 cx.run_until_parked();
23208
23209 // Despite two edits, only one is actually applied as those are identical
23210 cx.assert_editor_state(indoc! {"
23211 struct FooRenamedˇ {}
23212 "});
23213}
23214
23215#[gpui::test]
23216async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23217 init_test(cx, |_| {});
23218 // These capabilities indicate that the server does not support prepare rename.
23219 let capabilities = lsp::ServerCapabilities {
23220 rename_provider: Some(lsp::OneOf::Left(true)),
23221 ..Default::default()
23222 };
23223 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23224
23225 cx.set_state(indoc! {"
23226 struct Fˇoo {}
23227 "});
23228
23229 cx.update_editor(|editor, _window, cx| {
23230 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23231 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23232 editor.highlight_background::<DocumentHighlightRead>(
23233 &[highlight_range],
23234 |theme| theme.colors().editor_document_highlight_read_background,
23235 cx,
23236 );
23237 });
23238
23239 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23240 .expect("Prepare rename was not started")
23241 .await
23242 .expect("Prepare rename failed");
23243
23244 let mut rename_handler =
23245 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23246 let edit = lsp::TextEdit {
23247 range: lsp::Range {
23248 start: lsp::Position {
23249 line: 0,
23250 character: 7,
23251 },
23252 end: lsp::Position {
23253 line: 0,
23254 character: 10,
23255 },
23256 },
23257 new_text: "FooRenamed".to_string(),
23258 };
23259 Ok(Some(lsp::WorkspaceEdit::new(
23260 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23261 )))
23262 });
23263 let rename_task = cx
23264 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23265 .expect("Confirm rename was not started");
23266 rename_handler.next().await.unwrap();
23267 rename_task.await.expect("Confirm rename failed");
23268 cx.run_until_parked();
23269
23270 // Correct range is renamed, as `surrounding_word` is used to find it.
23271 cx.assert_editor_state(indoc! {"
23272 struct FooRenamedˇ {}
23273 "});
23274}
23275
23276#[gpui::test]
23277async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23278 init_test(cx, |_| {});
23279 let mut cx = EditorTestContext::new(cx).await;
23280
23281 let language = Arc::new(
23282 Language::new(
23283 LanguageConfig::default(),
23284 Some(tree_sitter_html::LANGUAGE.into()),
23285 )
23286 .with_brackets_query(
23287 r#"
23288 ("<" @open "/>" @close)
23289 ("</" @open ">" @close)
23290 ("<" @open ">" @close)
23291 ("\"" @open "\"" @close)
23292 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23293 "#,
23294 )
23295 .unwrap(),
23296 );
23297 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23298
23299 cx.set_state(indoc! {"
23300 <span>ˇ</span>
23301 "});
23302 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23303 cx.assert_editor_state(indoc! {"
23304 <span>
23305 ˇ
23306 </span>
23307 "});
23308
23309 cx.set_state(indoc! {"
23310 <span><span></span>ˇ</span>
23311 "});
23312 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23313 cx.assert_editor_state(indoc! {"
23314 <span><span></span>
23315 ˇ</span>
23316 "});
23317
23318 cx.set_state(indoc! {"
23319 <span>ˇ
23320 </span>
23321 "});
23322 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23323 cx.assert_editor_state(indoc! {"
23324 <span>
23325 ˇ
23326 </span>
23327 "});
23328}
23329
23330#[gpui::test(iterations = 10)]
23331async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23332 init_test(cx, |_| {});
23333
23334 let fs = FakeFs::new(cx.executor());
23335 fs.insert_tree(
23336 path!("/dir"),
23337 json!({
23338 "a.ts": "a",
23339 }),
23340 )
23341 .await;
23342
23343 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23344 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23345 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23346
23347 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23348 language_registry.add(Arc::new(Language::new(
23349 LanguageConfig {
23350 name: "TypeScript".into(),
23351 matcher: LanguageMatcher {
23352 path_suffixes: vec!["ts".to_string()],
23353 ..Default::default()
23354 },
23355 ..Default::default()
23356 },
23357 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23358 )));
23359 let mut fake_language_servers = language_registry.register_fake_lsp(
23360 "TypeScript",
23361 FakeLspAdapter {
23362 capabilities: lsp::ServerCapabilities {
23363 code_lens_provider: Some(lsp::CodeLensOptions {
23364 resolve_provider: Some(true),
23365 }),
23366 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23367 commands: vec!["_the/command".to_string()],
23368 ..lsp::ExecuteCommandOptions::default()
23369 }),
23370 ..lsp::ServerCapabilities::default()
23371 },
23372 ..FakeLspAdapter::default()
23373 },
23374 );
23375
23376 let editor = workspace
23377 .update(cx, |workspace, window, cx| {
23378 workspace.open_abs_path(
23379 PathBuf::from(path!("/dir/a.ts")),
23380 OpenOptions::default(),
23381 window,
23382 cx,
23383 )
23384 })
23385 .unwrap()
23386 .await
23387 .unwrap()
23388 .downcast::<Editor>()
23389 .unwrap();
23390 cx.executor().run_until_parked();
23391
23392 let fake_server = fake_language_servers.next().await.unwrap();
23393
23394 let buffer = editor.update(cx, |editor, cx| {
23395 editor
23396 .buffer()
23397 .read(cx)
23398 .as_singleton()
23399 .expect("have opened a single file by path")
23400 });
23401
23402 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23403 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23404 drop(buffer_snapshot);
23405 let actions = cx
23406 .update_window(*workspace, |_, window, cx| {
23407 project.code_actions(&buffer, anchor..anchor, window, cx)
23408 })
23409 .unwrap();
23410
23411 fake_server
23412 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23413 Ok(Some(vec![
23414 lsp::CodeLens {
23415 range: lsp::Range::default(),
23416 command: Some(lsp::Command {
23417 title: "Code lens command".to_owned(),
23418 command: "_the/command".to_owned(),
23419 arguments: None,
23420 }),
23421 data: None,
23422 },
23423 lsp::CodeLens {
23424 range: lsp::Range::default(),
23425 command: Some(lsp::Command {
23426 title: "Command not in capabilities".to_owned(),
23427 command: "not in capabilities".to_owned(),
23428 arguments: None,
23429 }),
23430 data: None,
23431 },
23432 lsp::CodeLens {
23433 range: lsp::Range {
23434 start: lsp::Position {
23435 line: 1,
23436 character: 1,
23437 },
23438 end: lsp::Position {
23439 line: 1,
23440 character: 1,
23441 },
23442 },
23443 command: Some(lsp::Command {
23444 title: "Command not in range".to_owned(),
23445 command: "_the/command".to_owned(),
23446 arguments: None,
23447 }),
23448 data: None,
23449 },
23450 ]))
23451 })
23452 .next()
23453 .await;
23454
23455 let actions = actions.await.unwrap();
23456 assert_eq!(
23457 actions.len(),
23458 1,
23459 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23460 );
23461 let action = actions[0].clone();
23462 let apply = project.update(cx, |project, cx| {
23463 project.apply_code_action(buffer.clone(), action, true, cx)
23464 });
23465
23466 // Resolving the code action does not populate its edits. In absence of
23467 // edits, we must execute the given command.
23468 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23469 |mut lens, _| async move {
23470 let lens_command = lens.command.as_mut().expect("should have a command");
23471 assert_eq!(lens_command.title, "Code lens command");
23472 lens_command.arguments = Some(vec![json!("the-argument")]);
23473 Ok(lens)
23474 },
23475 );
23476
23477 // While executing the command, the language server sends the editor
23478 // a `workspaceEdit` request.
23479 fake_server
23480 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23481 let fake = fake_server.clone();
23482 move |params, _| {
23483 assert_eq!(params.command, "_the/command");
23484 let fake = fake.clone();
23485 async move {
23486 fake.server
23487 .request::<lsp::request::ApplyWorkspaceEdit>(
23488 lsp::ApplyWorkspaceEditParams {
23489 label: None,
23490 edit: lsp::WorkspaceEdit {
23491 changes: Some(
23492 [(
23493 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23494 vec![lsp::TextEdit {
23495 range: lsp::Range::new(
23496 lsp::Position::new(0, 0),
23497 lsp::Position::new(0, 0),
23498 ),
23499 new_text: "X".into(),
23500 }],
23501 )]
23502 .into_iter()
23503 .collect(),
23504 ),
23505 ..lsp::WorkspaceEdit::default()
23506 },
23507 },
23508 )
23509 .await
23510 .into_response()
23511 .unwrap();
23512 Ok(Some(json!(null)))
23513 }
23514 }
23515 })
23516 .next()
23517 .await;
23518
23519 // Applying the code lens command returns a project transaction containing the edits
23520 // sent by the language server in its `workspaceEdit` request.
23521 let transaction = apply.await.unwrap();
23522 assert!(transaction.0.contains_key(&buffer));
23523 buffer.update(cx, |buffer, cx| {
23524 assert_eq!(buffer.text(), "Xa");
23525 buffer.undo(cx);
23526 assert_eq!(buffer.text(), "a");
23527 });
23528
23529 let actions_after_edits = cx
23530 .update_window(*workspace, |_, window, cx| {
23531 project.code_actions(&buffer, anchor..anchor, window, cx)
23532 })
23533 .unwrap()
23534 .await
23535 .unwrap();
23536 assert_eq!(
23537 actions, actions_after_edits,
23538 "For the same selection, same code lens actions should be returned"
23539 );
23540
23541 let _responses =
23542 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23543 panic!("No more code lens requests are expected");
23544 });
23545 editor.update_in(cx, |editor, window, cx| {
23546 editor.select_all(&SelectAll, window, cx);
23547 });
23548 cx.executor().run_until_parked();
23549 let new_actions = cx
23550 .update_window(*workspace, |_, window, cx| {
23551 project.code_actions(&buffer, anchor..anchor, window, cx)
23552 })
23553 .unwrap()
23554 .await
23555 .unwrap();
23556 assert_eq!(
23557 actions, new_actions,
23558 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23559 );
23560}
23561
23562#[gpui::test]
23563async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23564 init_test(cx, |_| {});
23565
23566 let fs = FakeFs::new(cx.executor());
23567 let main_text = r#"fn main() {
23568println!("1");
23569println!("2");
23570println!("3");
23571println!("4");
23572println!("5");
23573}"#;
23574 let lib_text = "mod foo {}";
23575 fs.insert_tree(
23576 path!("/a"),
23577 json!({
23578 "lib.rs": lib_text,
23579 "main.rs": main_text,
23580 }),
23581 )
23582 .await;
23583
23584 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23585 let (workspace, cx) =
23586 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23587 let worktree_id = workspace.update(cx, |workspace, cx| {
23588 workspace.project().update(cx, |project, cx| {
23589 project.worktrees(cx).next().unwrap().read(cx).id()
23590 })
23591 });
23592
23593 let expected_ranges = vec![
23594 Point::new(0, 0)..Point::new(0, 0),
23595 Point::new(1, 0)..Point::new(1, 1),
23596 Point::new(2, 0)..Point::new(2, 2),
23597 Point::new(3, 0)..Point::new(3, 3),
23598 ];
23599
23600 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23601 let editor_1 = workspace
23602 .update_in(cx, |workspace, window, cx| {
23603 workspace.open_path(
23604 (worktree_id, rel_path("main.rs")),
23605 Some(pane_1.downgrade()),
23606 true,
23607 window,
23608 cx,
23609 )
23610 })
23611 .unwrap()
23612 .await
23613 .downcast::<Editor>()
23614 .unwrap();
23615 pane_1.update(cx, |pane, cx| {
23616 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23617 open_editor.update(cx, |editor, cx| {
23618 assert_eq!(
23619 editor.display_text(cx),
23620 main_text,
23621 "Original main.rs text on initial open",
23622 );
23623 assert_eq!(
23624 editor
23625 .selections
23626 .all::<Point>(cx)
23627 .into_iter()
23628 .map(|s| s.range())
23629 .collect::<Vec<_>>(),
23630 vec![Point::zero()..Point::zero()],
23631 "Default selections on initial open",
23632 );
23633 })
23634 });
23635 editor_1.update_in(cx, |editor, window, cx| {
23636 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23637 s.select_ranges(expected_ranges.clone());
23638 });
23639 });
23640
23641 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23642 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23643 });
23644 let editor_2 = workspace
23645 .update_in(cx, |workspace, window, cx| {
23646 workspace.open_path(
23647 (worktree_id, rel_path("main.rs")),
23648 Some(pane_2.downgrade()),
23649 true,
23650 window,
23651 cx,
23652 )
23653 })
23654 .unwrap()
23655 .await
23656 .downcast::<Editor>()
23657 .unwrap();
23658 pane_2.update(cx, |pane, cx| {
23659 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23660 open_editor.update(cx, |editor, cx| {
23661 assert_eq!(
23662 editor.display_text(cx),
23663 main_text,
23664 "Original main.rs text on initial open in another panel",
23665 );
23666 assert_eq!(
23667 editor
23668 .selections
23669 .all::<Point>(cx)
23670 .into_iter()
23671 .map(|s| s.range())
23672 .collect::<Vec<_>>(),
23673 vec![Point::zero()..Point::zero()],
23674 "Default selections on initial open in another panel",
23675 );
23676 })
23677 });
23678
23679 editor_2.update_in(cx, |editor, window, cx| {
23680 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23681 });
23682
23683 let _other_editor_1 = workspace
23684 .update_in(cx, |workspace, window, cx| {
23685 workspace.open_path(
23686 (worktree_id, rel_path("lib.rs")),
23687 Some(pane_1.downgrade()),
23688 true,
23689 window,
23690 cx,
23691 )
23692 })
23693 .unwrap()
23694 .await
23695 .downcast::<Editor>()
23696 .unwrap();
23697 pane_1
23698 .update_in(cx, |pane, window, cx| {
23699 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23700 })
23701 .await
23702 .unwrap();
23703 drop(editor_1);
23704 pane_1.update(cx, |pane, cx| {
23705 pane.active_item()
23706 .unwrap()
23707 .downcast::<Editor>()
23708 .unwrap()
23709 .update(cx, |editor, cx| {
23710 assert_eq!(
23711 editor.display_text(cx),
23712 lib_text,
23713 "Other file should be open and active",
23714 );
23715 });
23716 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23717 });
23718
23719 let _other_editor_2 = workspace
23720 .update_in(cx, |workspace, window, cx| {
23721 workspace.open_path(
23722 (worktree_id, rel_path("lib.rs")),
23723 Some(pane_2.downgrade()),
23724 true,
23725 window,
23726 cx,
23727 )
23728 })
23729 .unwrap()
23730 .await
23731 .downcast::<Editor>()
23732 .unwrap();
23733 pane_2
23734 .update_in(cx, |pane, window, cx| {
23735 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23736 })
23737 .await
23738 .unwrap();
23739 drop(editor_2);
23740 pane_2.update(cx, |pane, cx| {
23741 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23742 open_editor.update(cx, |editor, cx| {
23743 assert_eq!(
23744 editor.display_text(cx),
23745 lib_text,
23746 "Other file should be open and active in another panel too",
23747 );
23748 });
23749 assert_eq!(
23750 pane.items().count(),
23751 1,
23752 "No other editors should be open in another pane",
23753 );
23754 });
23755
23756 let _editor_1_reopened = workspace
23757 .update_in(cx, |workspace, window, cx| {
23758 workspace.open_path(
23759 (worktree_id, rel_path("main.rs")),
23760 Some(pane_1.downgrade()),
23761 true,
23762 window,
23763 cx,
23764 )
23765 })
23766 .unwrap()
23767 .await
23768 .downcast::<Editor>()
23769 .unwrap();
23770 let _editor_2_reopened = workspace
23771 .update_in(cx, |workspace, window, cx| {
23772 workspace.open_path(
23773 (worktree_id, rel_path("main.rs")),
23774 Some(pane_2.downgrade()),
23775 true,
23776 window,
23777 cx,
23778 )
23779 })
23780 .unwrap()
23781 .await
23782 .downcast::<Editor>()
23783 .unwrap();
23784 pane_1.update(cx, |pane, cx| {
23785 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23786 open_editor.update(cx, |editor, cx| {
23787 assert_eq!(
23788 editor.display_text(cx),
23789 main_text,
23790 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23791 );
23792 assert_eq!(
23793 editor
23794 .selections
23795 .all::<Point>(cx)
23796 .into_iter()
23797 .map(|s| s.range())
23798 .collect::<Vec<_>>(),
23799 expected_ranges,
23800 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23801 );
23802 })
23803 });
23804 pane_2.update(cx, |pane, cx| {
23805 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23806 open_editor.update(cx, |editor, cx| {
23807 assert_eq!(
23808 editor.display_text(cx),
23809 r#"fn main() {
23810⋯rintln!("1");
23811⋯intln!("2");
23812⋯ntln!("3");
23813println!("4");
23814println!("5");
23815}"#,
23816 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23817 );
23818 assert_eq!(
23819 editor
23820 .selections
23821 .all::<Point>(cx)
23822 .into_iter()
23823 .map(|s| s.range())
23824 .collect::<Vec<_>>(),
23825 vec![Point::zero()..Point::zero()],
23826 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23827 );
23828 })
23829 });
23830}
23831
23832#[gpui::test]
23833async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23834 init_test(cx, |_| {});
23835
23836 let fs = FakeFs::new(cx.executor());
23837 let main_text = r#"fn main() {
23838println!("1");
23839println!("2");
23840println!("3");
23841println!("4");
23842println!("5");
23843}"#;
23844 let lib_text = "mod foo {}";
23845 fs.insert_tree(
23846 path!("/a"),
23847 json!({
23848 "lib.rs": lib_text,
23849 "main.rs": main_text,
23850 }),
23851 )
23852 .await;
23853
23854 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23855 let (workspace, cx) =
23856 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23857 let worktree_id = workspace.update(cx, |workspace, cx| {
23858 workspace.project().update(cx, |project, cx| {
23859 project.worktrees(cx).next().unwrap().read(cx).id()
23860 })
23861 });
23862
23863 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23864 let editor = workspace
23865 .update_in(cx, |workspace, window, cx| {
23866 workspace.open_path(
23867 (worktree_id, rel_path("main.rs")),
23868 Some(pane.downgrade()),
23869 true,
23870 window,
23871 cx,
23872 )
23873 })
23874 .unwrap()
23875 .await
23876 .downcast::<Editor>()
23877 .unwrap();
23878 pane.update(cx, |pane, cx| {
23879 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23880 open_editor.update(cx, |editor, cx| {
23881 assert_eq!(
23882 editor.display_text(cx),
23883 main_text,
23884 "Original main.rs text on initial open",
23885 );
23886 })
23887 });
23888 editor.update_in(cx, |editor, window, cx| {
23889 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23890 });
23891
23892 cx.update_global(|store: &mut SettingsStore, cx| {
23893 store.update_user_settings(cx, |s| {
23894 s.workspace.restore_on_file_reopen = Some(false);
23895 });
23896 });
23897 editor.update_in(cx, |editor, window, cx| {
23898 editor.fold_ranges(
23899 vec![
23900 Point::new(1, 0)..Point::new(1, 1),
23901 Point::new(2, 0)..Point::new(2, 2),
23902 Point::new(3, 0)..Point::new(3, 3),
23903 ],
23904 false,
23905 window,
23906 cx,
23907 );
23908 });
23909 pane.update_in(cx, |pane, window, cx| {
23910 pane.close_all_items(&CloseAllItems::default(), window, cx)
23911 })
23912 .await
23913 .unwrap();
23914 pane.update(cx, |pane, _| {
23915 assert!(pane.active_item().is_none());
23916 });
23917 cx.update_global(|store: &mut SettingsStore, cx| {
23918 store.update_user_settings(cx, |s| {
23919 s.workspace.restore_on_file_reopen = Some(true);
23920 });
23921 });
23922
23923 let _editor_reopened = workspace
23924 .update_in(cx, |workspace, window, cx| {
23925 workspace.open_path(
23926 (worktree_id, rel_path("main.rs")),
23927 Some(pane.downgrade()),
23928 true,
23929 window,
23930 cx,
23931 )
23932 })
23933 .unwrap()
23934 .await
23935 .downcast::<Editor>()
23936 .unwrap();
23937 pane.update(cx, |pane, cx| {
23938 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23939 open_editor.update(cx, |editor, cx| {
23940 assert_eq!(
23941 editor.display_text(cx),
23942 main_text,
23943 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23944 );
23945 })
23946 });
23947}
23948
23949#[gpui::test]
23950async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23951 struct EmptyModalView {
23952 focus_handle: gpui::FocusHandle,
23953 }
23954 impl EventEmitter<DismissEvent> for EmptyModalView {}
23955 impl Render for EmptyModalView {
23956 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23957 div()
23958 }
23959 }
23960 impl Focusable for EmptyModalView {
23961 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23962 self.focus_handle.clone()
23963 }
23964 }
23965 impl workspace::ModalView for EmptyModalView {}
23966 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23967 EmptyModalView {
23968 focus_handle: cx.focus_handle(),
23969 }
23970 }
23971
23972 init_test(cx, |_| {});
23973
23974 let fs = FakeFs::new(cx.executor());
23975 let project = Project::test(fs, [], cx).await;
23976 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23977 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23978 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23979 let editor = cx.new_window_entity(|window, cx| {
23980 Editor::new(
23981 EditorMode::full(),
23982 buffer,
23983 Some(project.clone()),
23984 window,
23985 cx,
23986 )
23987 });
23988 workspace
23989 .update(cx, |workspace, window, cx| {
23990 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23991 })
23992 .unwrap();
23993 editor.update_in(cx, |editor, window, cx| {
23994 editor.open_context_menu(&OpenContextMenu, window, cx);
23995 assert!(editor.mouse_context_menu.is_some());
23996 });
23997 workspace
23998 .update(cx, |workspace, window, cx| {
23999 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24000 })
24001 .unwrap();
24002 cx.read(|cx| {
24003 assert!(editor.read(cx).mouse_context_menu.is_none());
24004 });
24005}
24006
24007fn set_linked_edit_ranges(
24008 opening: (Point, Point),
24009 closing: (Point, Point),
24010 editor: &mut Editor,
24011 cx: &mut Context<Editor>,
24012) {
24013 let Some((buffer, _)) = editor
24014 .buffer
24015 .read(cx)
24016 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24017 else {
24018 panic!("Failed to get buffer for selection position");
24019 };
24020 let buffer = buffer.read(cx);
24021 let buffer_id = buffer.remote_id();
24022 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24023 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24024 let mut linked_ranges = HashMap::default();
24025 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24026 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24027}
24028
24029#[gpui::test]
24030async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24031 init_test(cx, |_| {});
24032
24033 let fs = FakeFs::new(cx.executor());
24034 fs.insert_file(path!("/file.html"), Default::default())
24035 .await;
24036
24037 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24038
24039 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24040 let html_language = Arc::new(Language::new(
24041 LanguageConfig {
24042 name: "HTML".into(),
24043 matcher: LanguageMatcher {
24044 path_suffixes: vec!["html".to_string()],
24045 ..LanguageMatcher::default()
24046 },
24047 brackets: BracketPairConfig {
24048 pairs: vec![BracketPair {
24049 start: "<".into(),
24050 end: ">".into(),
24051 close: true,
24052 ..Default::default()
24053 }],
24054 ..Default::default()
24055 },
24056 ..Default::default()
24057 },
24058 Some(tree_sitter_html::LANGUAGE.into()),
24059 ));
24060 language_registry.add(html_language);
24061 let mut fake_servers = language_registry.register_fake_lsp(
24062 "HTML",
24063 FakeLspAdapter {
24064 capabilities: lsp::ServerCapabilities {
24065 completion_provider: Some(lsp::CompletionOptions {
24066 resolve_provider: Some(true),
24067 ..Default::default()
24068 }),
24069 ..Default::default()
24070 },
24071 ..Default::default()
24072 },
24073 );
24074
24075 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24076 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24077
24078 let worktree_id = workspace
24079 .update(cx, |workspace, _window, cx| {
24080 workspace.project().update(cx, |project, cx| {
24081 project.worktrees(cx).next().unwrap().read(cx).id()
24082 })
24083 })
24084 .unwrap();
24085 project
24086 .update(cx, |project, cx| {
24087 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24088 })
24089 .await
24090 .unwrap();
24091 let editor = workspace
24092 .update(cx, |workspace, window, cx| {
24093 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24094 })
24095 .unwrap()
24096 .await
24097 .unwrap()
24098 .downcast::<Editor>()
24099 .unwrap();
24100
24101 let fake_server = fake_servers.next().await.unwrap();
24102 editor.update_in(cx, |editor, window, cx| {
24103 editor.set_text("<ad></ad>", window, cx);
24104 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24105 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24106 });
24107 set_linked_edit_ranges(
24108 (Point::new(0, 1), Point::new(0, 3)),
24109 (Point::new(0, 6), Point::new(0, 8)),
24110 editor,
24111 cx,
24112 );
24113 });
24114 let mut completion_handle =
24115 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24116 Ok(Some(lsp::CompletionResponse::Array(vec![
24117 lsp::CompletionItem {
24118 label: "head".to_string(),
24119 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24120 lsp::InsertReplaceEdit {
24121 new_text: "head".to_string(),
24122 insert: lsp::Range::new(
24123 lsp::Position::new(0, 1),
24124 lsp::Position::new(0, 3),
24125 ),
24126 replace: lsp::Range::new(
24127 lsp::Position::new(0, 1),
24128 lsp::Position::new(0, 3),
24129 ),
24130 },
24131 )),
24132 ..Default::default()
24133 },
24134 ])))
24135 });
24136 editor.update_in(cx, |editor, window, cx| {
24137 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24138 });
24139 cx.run_until_parked();
24140 completion_handle.next().await.unwrap();
24141 editor.update(cx, |editor, _| {
24142 assert!(
24143 editor.context_menu_visible(),
24144 "Completion menu should be visible"
24145 );
24146 });
24147 editor.update_in(cx, |editor, window, cx| {
24148 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24149 });
24150 cx.executor().run_until_parked();
24151 editor.update(cx, |editor, cx| {
24152 assert_eq!(editor.text(cx), "<head></head>");
24153 });
24154}
24155
24156#[gpui::test]
24157async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24158 init_test(cx, |_| {});
24159
24160 let mut cx = EditorTestContext::new(cx).await;
24161 let language = Arc::new(Language::new(
24162 LanguageConfig {
24163 name: "TSX".into(),
24164 matcher: LanguageMatcher {
24165 path_suffixes: vec!["tsx".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 linked_edit_characters: HashSet::from_iter(['.']),
24178 ..Default::default()
24179 },
24180 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24181 ));
24182 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24183
24184 // Test typing > does not extend linked pair
24185 cx.set_state("<divˇ<div></div>");
24186 cx.update_editor(|editor, _, cx| {
24187 set_linked_edit_ranges(
24188 (Point::new(0, 1), Point::new(0, 4)),
24189 (Point::new(0, 11), Point::new(0, 14)),
24190 editor,
24191 cx,
24192 );
24193 });
24194 cx.update_editor(|editor, window, cx| {
24195 editor.handle_input(">", window, cx);
24196 });
24197 cx.assert_editor_state("<div>ˇ<div></div>");
24198
24199 // Test typing . do extend linked pair
24200 cx.set_state("<Animatedˇ></Animated>");
24201 cx.update_editor(|editor, _, cx| {
24202 set_linked_edit_ranges(
24203 (Point::new(0, 1), Point::new(0, 9)),
24204 (Point::new(0, 12), Point::new(0, 20)),
24205 editor,
24206 cx,
24207 );
24208 });
24209 cx.update_editor(|editor, window, cx| {
24210 editor.handle_input(".", window, cx);
24211 });
24212 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24213 cx.update_editor(|editor, _, cx| {
24214 set_linked_edit_ranges(
24215 (Point::new(0, 1), Point::new(0, 10)),
24216 (Point::new(0, 13), Point::new(0, 21)),
24217 editor,
24218 cx,
24219 );
24220 });
24221 cx.update_editor(|editor, window, cx| {
24222 editor.handle_input("V", window, cx);
24223 });
24224 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24225}
24226
24227#[gpui::test]
24228async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24229 init_test(cx, |_| {});
24230
24231 let fs = FakeFs::new(cx.executor());
24232 fs.insert_tree(
24233 path!("/root"),
24234 json!({
24235 "a": {
24236 "main.rs": "fn main() {}",
24237 },
24238 "foo": {
24239 "bar": {
24240 "external_file.rs": "pub mod external {}",
24241 }
24242 }
24243 }),
24244 )
24245 .await;
24246
24247 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24248 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24249 language_registry.add(rust_lang());
24250 let _fake_servers = language_registry.register_fake_lsp(
24251 "Rust",
24252 FakeLspAdapter {
24253 ..FakeLspAdapter::default()
24254 },
24255 );
24256 let (workspace, cx) =
24257 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24258 let worktree_id = workspace.update(cx, |workspace, cx| {
24259 workspace.project().update(cx, |project, cx| {
24260 project.worktrees(cx).next().unwrap().read(cx).id()
24261 })
24262 });
24263
24264 let assert_language_servers_count =
24265 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24266 project.update(cx, |project, cx| {
24267 let current = project
24268 .lsp_store()
24269 .read(cx)
24270 .as_local()
24271 .unwrap()
24272 .language_servers
24273 .len();
24274 assert_eq!(expected, current, "{context}");
24275 });
24276 };
24277
24278 assert_language_servers_count(
24279 0,
24280 "No servers should be running before any file is open",
24281 cx,
24282 );
24283 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24284 let main_editor = workspace
24285 .update_in(cx, |workspace, window, cx| {
24286 workspace.open_path(
24287 (worktree_id, rel_path("main.rs")),
24288 Some(pane.downgrade()),
24289 true,
24290 window,
24291 cx,
24292 )
24293 })
24294 .unwrap()
24295 .await
24296 .downcast::<Editor>()
24297 .unwrap();
24298 pane.update(cx, |pane, cx| {
24299 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24300 open_editor.update(cx, |editor, cx| {
24301 assert_eq!(
24302 editor.display_text(cx),
24303 "fn main() {}",
24304 "Original main.rs text on initial open",
24305 );
24306 });
24307 assert_eq!(open_editor, main_editor);
24308 });
24309 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24310
24311 let external_editor = workspace
24312 .update_in(cx, |workspace, window, cx| {
24313 workspace.open_abs_path(
24314 PathBuf::from("/root/foo/bar/external_file.rs"),
24315 OpenOptions::default(),
24316 window,
24317 cx,
24318 )
24319 })
24320 .await
24321 .expect("opening external file")
24322 .downcast::<Editor>()
24323 .expect("downcasted external file's open element to editor");
24324 pane.update(cx, |pane, cx| {
24325 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24326 open_editor.update(cx, |editor, cx| {
24327 assert_eq!(
24328 editor.display_text(cx),
24329 "pub mod external {}",
24330 "External file is open now",
24331 );
24332 });
24333 assert_eq!(open_editor, external_editor);
24334 });
24335 assert_language_servers_count(
24336 1,
24337 "Second, external, *.rs file should join the existing server",
24338 cx,
24339 );
24340
24341 pane.update_in(cx, |pane, window, cx| {
24342 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24343 })
24344 .await
24345 .unwrap();
24346 pane.update_in(cx, |pane, window, cx| {
24347 pane.navigate_backward(&Default::default(), window, cx);
24348 });
24349 cx.run_until_parked();
24350 pane.update(cx, |pane, cx| {
24351 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24352 open_editor.update(cx, |editor, cx| {
24353 assert_eq!(
24354 editor.display_text(cx),
24355 "pub mod external {}",
24356 "External file is open now",
24357 );
24358 });
24359 });
24360 assert_language_servers_count(
24361 1,
24362 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24363 cx,
24364 );
24365
24366 cx.update(|_, cx| {
24367 workspace::reload(cx);
24368 });
24369 assert_language_servers_count(
24370 1,
24371 "After reloading the worktree with local and external files opened, only one project should be started",
24372 cx,
24373 );
24374}
24375
24376#[gpui::test]
24377async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24378 init_test(cx, |_| {});
24379
24380 let mut cx = EditorTestContext::new(cx).await;
24381 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24382 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24383
24384 // test cursor move to start of each line on tab
24385 // for `if`, `elif`, `else`, `while`, `with` and `for`
24386 cx.set_state(indoc! {"
24387 def main():
24388 ˇ for item in items:
24389 ˇ while item.active:
24390 ˇ if item.value > 10:
24391 ˇ continue
24392 ˇ elif item.value < 0:
24393 ˇ break
24394 ˇ else:
24395 ˇ with item.context() as ctx:
24396 ˇ yield count
24397 ˇ else:
24398 ˇ log('while else')
24399 ˇ else:
24400 ˇ log('for else')
24401 "});
24402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24403 cx.assert_editor_state(indoc! {"
24404 def main():
24405 ˇfor item in items:
24406 ˇwhile item.active:
24407 ˇif item.value > 10:
24408 ˇcontinue
24409 ˇelif item.value < 0:
24410 ˇbreak
24411 ˇelse:
24412 ˇwith item.context() as ctx:
24413 ˇyield count
24414 ˇelse:
24415 ˇlog('while else')
24416 ˇelse:
24417 ˇlog('for else')
24418 "});
24419 // test relative indent is preserved when tab
24420 // for `if`, `elif`, `else`, `while`, `with` and `for`
24421 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24422 cx.assert_editor_state(indoc! {"
24423 def main():
24424 ˇfor item in items:
24425 ˇwhile item.active:
24426 ˇif item.value > 10:
24427 ˇcontinue
24428 ˇelif item.value < 0:
24429 ˇbreak
24430 ˇelse:
24431 ˇwith item.context() as ctx:
24432 ˇyield count
24433 ˇelse:
24434 ˇlog('while else')
24435 ˇelse:
24436 ˇlog('for else')
24437 "});
24438
24439 // test cursor move to start of each line on tab
24440 // for `try`, `except`, `else`, `finally`, `match` and `def`
24441 cx.set_state(indoc! {"
24442 def main():
24443 ˇ try:
24444 ˇ fetch()
24445 ˇ except ValueError:
24446 ˇ handle_error()
24447 ˇ else:
24448 ˇ match value:
24449 ˇ case _:
24450 ˇ finally:
24451 ˇ def status():
24452 ˇ return 0
24453 "});
24454 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24455 cx.assert_editor_state(indoc! {"
24456 def main():
24457 ˇtry:
24458 ˇfetch()
24459 ˇexcept ValueError:
24460 ˇhandle_error()
24461 ˇelse:
24462 ˇmatch value:
24463 ˇcase _:
24464 ˇfinally:
24465 ˇdef status():
24466 ˇreturn 0
24467 "});
24468 // test relative indent is preserved when tab
24469 // for `try`, `except`, `else`, `finally`, `match` and `def`
24470 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24471 cx.assert_editor_state(indoc! {"
24472 def main():
24473 ˇtry:
24474 ˇfetch()
24475 ˇexcept ValueError:
24476 ˇhandle_error()
24477 ˇelse:
24478 ˇmatch value:
24479 ˇcase _:
24480 ˇfinally:
24481 ˇdef status():
24482 ˇreturn 0
24483 "});
24484}
24485
24486#[gpui::test]
24487async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24488 init_test(cx, |_| {});
24489
24490 let mut cx = EditorTestContext::new(cx).await;
24491 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24492 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24493
24494 // test `else` auto outdents when typed inside `if` block
24495 cx.set_state(indoc! {"
24496 def main():
24497 if i == 2:
24498 return
24499 ˇ
24500 "});
24501 cx.update_editor(|editor, window, cx| {
24502 editor.handle_input("else:", window, cx);
24503 });
24504 cx.assert_editor_state(indoc! {"
24505 def main():
24506 if i == 2:
24507 return
24508 else:ˇ
24509 "});
24510
24511 // test `except` auto outdents when typed inside `try` block
24512 cx.set_state(indoc! {"
24513 def main():
24514 try:
24515 i = 2
24516 ˇ
24517 "});
24518 cx.update_editor(|editor, window, cx| {
24519 editor.handle_input("except:", window, cx);
24520 });
24521 cx.assert_editor_state(indoc! {"
24522 def main():
24523 try:
24524 i = 2
24525 except:ˇ
24526 "});
24527
24528 // test `else` auto outdents when typed inside `except` block
24529 cx.set_state(indoc! {"
24530 def main():
24531 try:
24532 i = 2
24533 except:
24534 j = 2
24535 ˇ
24536 "});
24537 cx.update_editor(|editor, window, cx| {
24538 editor.handle_input("else:", window, cx);
24539 });
24540 cx.assert_editor_state(indoc! {"
24541 def main():
24542 try:
24543 i = 2
24544 except:
24545 j = 2
24546 else:ˇ
24547 "});
24548
24549 // test `finally` auto outdents when typed inside `else` block
24550 cx.set_state(indoc! {"
24551 def main():
24552 try:
24553 i = 2
24554 except:
24555 j = 2
24556 else:
24557 k = 2
24558 ˇ
24559 "});
24560 cx.update_editor(|editor, window, cx| {
24561 editor.handle_input("finally:", window, cx);
24562 });
24563 cx.assert_editor_state(indoc! {"
24564 def main():
24565 try:
24566 i = 2
24567 except:
24568 j = 2
24569 else:
24570 k = 2
24571 finally:ˇ
24572 "});
24573
24574 // test `else` does not outdents when typed inside `except` block right after for block
24575 cx.set_state(indoc! {"
24576 def main():
24577 try:
24578 i = 2
24579 except:
24580 for i in range(n):
24581 pass
24582 ˇ
24583 "});
24584 cx.update_editor(|editor, window, cx| {
24585 editor.handle_input("else:", window, cx);
24586 });
24587 cx.assert_editor_state(indoc! {"
24588 def main():
24589 try:
24590 i = 2
24591 except:
24592 for i in range(n):
24593 pass
24594 else:ˇ
24595 "});
24596
24597 // test `finally` auto outdents when typed inside `else` block right after for block
24598 cx.set_state(indoc! {"
24599 def main():
24600 try:
24601 i = 2
24602 except:
24603 j = 2
24604 else:
24605 for i in range(n):
24606 pass
24607 ˇ
24608 "});
24609 cx.update_editor(|editor, window, cx| {
24610 editor.handle_input("finally:", window, cx);
24611 });
24612 cx.assert_editor_state(indoc! {"
24613 def main():
24614 try:
24615 i = 2
24616 except:
24617 j = 2
24618 else:
24619 for i in range(n):
24620 pass
24621 finally:ˇ
24622 "});
24623
24624 // test `except` outdents to inner "try" block
24625 cx.set_state(indoc! {"
24626 def main():
24627 try:
24628 i = 2
24629 if i == 2:
24630 try:
24631 i = 3
24632 ˇ
24633 "});
24634 cx.update_editor(|editor, window, cx| {
24635 editor.handle_input("except:", window, cx);
24636 });
24637 cx.assert_editor_state(indoc! {"
24638 def main():
24639 try:
24640 i = 2
24641 if i == 2:
24642 try:
24643 i = 3
24644 except:ˇ
24645 "});
24646
24647 // test `except` outdents to outer "try" block
24648 cx.set_state(indoc! {"
24649 def main():
24650 try:
24651 i = 2
24652 if i == 2:
24653 try:
24654 i = 3
24655 ˇ
24656 "});
24657 cx.update_editor(|editor, window, cx| {
24658 editor.handle_input("except:", window, cx);
24659 });
24660 cx.assert_editor_state(indoc! {"
24661 def main():
24662 try:
24663 i = 2
24664 if i == 2:
24665 try:
24666 i = 3
24667 except:ˇ
24668 "});
24669
24670 // test `else` stays at correct indent when typed after `for` block
24671 cx.set_state(indoc! {"
24672 def main():
24673 for i in range(10):
24674 if i == 3:
24675 break
24676 ˇ
24677 "});
24678 cx.update_editor(|editor, window, cx| {
24679 editor.handle_input("else:", window, cx);
24680 });
24681 cx.assert_editor_state(indoc! {"
24682 def main():
24683 for i in range(10):
24684 if i == 3:
24685 break
24686 else:ˇ
24687 "});
24688
24689 // test does not outdent on typing after line with square brackets
24690 cx.set_state(indoc! {"
24691 def f() -> list[str]:
24692 ˇ
24693 "});
24694 cx.update_editor(|editor, window, cx| {
24695 editor.handle_input("a", window, cx);
24696 });
24697 cx.assert_editor_state(indoc! {"
24698 def f() -> list[str]:
24699 aˇ
24700 "});
24701
24702 // test does not outdent on typing : after case keyword
24703 cx.set_state(indoc! {"
24704 match 1:
24705 caseˇ
24706 "});
24707 cx.update_editor(|editor, window, cx| {
24708 editor.handle_input(":", window, cx);
24709 });
24710 cx.assert_editor_state(indoc! {"
24711 match 1:
24712 case:ˇ
24713 "});
24714}
24715
24716#[gpui::test]
24717async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24718 init_test(cx, |_| {});
24719 update_test_language_settings(cx, |settings| {
24720 settings.defaults.extend_comment_on_newline = Some(false);
24721 });
24722 let mut cx = EditorTestContext::new(cx).await;
24723 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24724 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24725
24726 // test correct indent after newline on comment
24727 cx.set_state(indoc! {"
24728 # COMMENT:ˇ
24729 "});
24730 cx.update_editor(|editor, window, cx| {
24731 editor.newline(&Newline, window, cx);
24732 });
24733 cx.assert_editor_state(indoc! {"
24734 # COMMENT:
24735 ˇ
24736 "});
24737
24738 // test correct indent after newline in brackets
24739 cx.set_state(indoc! {"
24740 {ˇ}
24741 "});
24742 cx.update_editor(|editor, window, cx| {
24743 editor.newline(&Newline, window, cx);
24744 });
24745 cx.run_until_parked();
24746 cx.assert_editor_state(indoc! {"
24747 {
24748 ˇ
24749 }
24750 "});
24751
24752 cx.set_state(indoc! {"
24753 (ˇ)
24754 "});
24755 cx.update_editor(|editor, window, cx| {
24756 editor.newline(&Newline, window, cx);
24757 });
24758 cx.run_until_parked();
24759 cx.assert_editor_state(indoc! {"
24760 (
24761 ˇ
24762 )
24763 "});
24764
24765 // do not indent after empty lists or dictionaries
24766 cx.set_state(indoc! {"
24767 a = []ˇ
24768 "});
24769 cx.update_editor(|editor, window, cx| {
24770 editor.newline(&Newline, window, cx);
24771 });
24772 cx.run_until_parked();
24773 cx.assert_editor_state(indoc! {"
24774 a = []
24775 ˇ
24776 "});
24777}
24778
24779#[gpui::test]
24780async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24781 init_test(cx, |_| {});
24782
24783 let mut cx = EditorTestContext::new(cx).await;
24784 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24785 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24786
24787 // test cursor move to start of each line on tab
24788 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24789 cx.set_state(indoc! {"
24790 function main() {
24791 ˇ for item in $items; do
24792 ˇ while [ -n \"$item\" ]; do
24793 ˇ if [ \"$value\" -gt 10 ]; then
24794 ˇ continue
24795 ˇ elif [ \"$value\" -lt 0 ]; then
24796 ˇ break
24797 ˇ else
24798 ˇ echo \"$item\"
24799 ˇ fi
24800 ˇ done
24801 ˇ done
24802 ˇ}
24803 "});
24804 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24805 cx.assert_editor_state(indoc! {"
24806 function main() {
24807 ˇfor item in $items; do
24808 ˇwhile [ -n \"$item\" ]; do
24809 ˇif [ \"$value\" -gt 10 ]; then
24810 ˇcontinue
24811 ˇelif [ \"$value\" -lt 0 ]; then
24812 ˇbreak
24813 ˇelse
24814 ˇecho \"$item\"
24815 ˇfi
24816 ˇdone
24817 ˇdone
24818 ˇ}
24819 "});
24820 // test relative indent is preserved when tab
24821 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24822 cx.assert_editor_state(indoc! {"
24823 function main() {
24824 ˇfor item in $items; do
24825 ˇwhile [ -n \"$item\" ]; do
24826 ˇif [ \"$value\" -gt 10 ]; then
24827 ˇcontinue
24828 ˇelif [ \"$value\" -lt 0 ]; then
24829 ˇbreak
24830 ˇelse
24831 ˇecho \"$item\"
24832 ˇfi
24833 ˇdone
24834 ˇdone
24835 ˇ}
24836 "});
24837
24838 // test cursor move to start of each line on tab
24839 // for `case` statement with patterns
24840 cx.set_state(indoc! {"
24841 function handle() {
24842 ˇ case \"$1\" in
24843 ˇ start)
24844 ˇ echo \"a\"
24845 ˇ ;;
24846 ˇ stop)
24847 ˇ echo \"b\"
24848 ˇ ;;
24849 ˇ *)
24850 ˇ echo \"c\"
24851 ˇ ;;
24852 ˇ esac
24853 ˇ}
24854 "});
24855 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24856 cx.assert_editor_state(indoc! {"
24857 function handle() {
24858 ˇcase \"$1\" in
24859 ˇstart)
24860 ˇecho \"a\"
24861 ˇ;;
24862 ˇstop)
24863 ˇecho \"b\"
24864 ˇ;;
24865 ˇ*)
24866 ˇecho \"c\"
24867 ˇ;;
24868 ˇesac
24869 ˇ}
24870 "});
24871}
24872
24873#[gpui::test]
24874async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24875 init_test(cx, |_| {});
24876
24877 let mut cx = EditorTestContext::new(cx).await;
24878 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24880
24881 // test indents on comment insert
24882 cx.set_state(indoc! {"
24883 function main() {
24884 ˇ for item in $items; do
24885 ˇ while [ -n \"$item\" ]; do
24886 ˇ if [ \"$value\" -gt 10 ]; then
24887 ˇ continue
24888 ˇ elif [ \"$value\" -lt 0 ]; then
24889 ˇ break
24890 ˇ else
24891 ˇ echo \"$item\"
24892 ˇ fi
24893 ˇ done
24894 ˇ done
24895 ˇ}
24896 "});
24897 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24898 cx.assert_editor_state(indoc! {"
24899 function main() {
24900 #ˇ for item in $items; do
24901 #ˇ while [ -n \"$item\" ]; do
24902 #ˇ if [ \"$value\" -gt 10 ]; then
24903 #ˇ continue
24904 #ˇ elif [ \"$value\" -lt 0 ]; then
24905 #ˇ break
24906 #ˇ else
24907 #ˇ echo \"$item\"
24908 #ˇ fi
24909 #ˇ done
24910 #ˇ done
24911 #ˇ}
24912 "});
24913}
24914
24915#[gpui::test]
24916async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24917 init_test(cx, |_| {});
24918
24919 let mut cx = EditorTestContext::new(cx).await;
24920 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24921 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24922
24923 // test `else` auto outdents when typed inside `if` block
24924 cx.set_state(indoc! {"
24925 if [ \"$1\" = \"test\" ]; then
24926 echo \"foo bar\"
24927 ˇ
24928 "});
24929 cx.update_editor(|editor, window, cx| {
24930 editor.handle_input("else", window, cx);
24931 });
24932 cx.assert_editor_state(indoc! {"
24933 if [ \"$1\" = \"test\" ]; then
24934 echo \"foo bar\"
24935 elseˇ
24936 "});
24937
24938 // test `elif` auto outdents when typed inside `if` block
24939 cx.set_state(indoc! {"
24940 if [ \"$1\" = \"test\" ]; then
24941 echo \"foo bar\"
24942 ˇ
24943 "});
24944 cx.update_editor(|editor, window, cx| {
24945 editor.handle_input("elif", window, cx);
24946 });
24947 cx.assert_editor_state(indoc! {"
24948 if [ \"$1\" = \"test\" ]; then
24949 echo \"foo bar\"
24950 elifˇ
24951 "});
24952
24953 // test `fi` auto outdents when typed inside `else` block
24954 cx.set_state(indoc! {"
24955 if [ \"$1\" = \"test\" ]; then
24956 echo \"foo bar\"
24957 else
24958 echo \"bar baz\"
24959 ˇ
24960 "});
24961 cx.update_editor(|editor, window, cx| {
24962 editor.handle_input("fi", window, cx);
24963 });
24964 cx.assert_editor_state(indoc! {"
24965 if [ \"$1\" = \"test\" ]; then
24966 echo \"foo bar\"
24967 else
24968 echo \"bar baz\"
24969 fiˇ
24970 "});
24971
24972 // test `done` auto outdents when typed inside `while` block
24973 cx.set_state(indoc! {"
24974 while read line; do
24975 echo \"$line\"
24976 ˇ
24977 "});
24978 cx.update_editor(|editor, window, cx| {
24979 editor.handle_input("done", window, cx);
24980 });
24981 cx.assert_editor_state(indoc! {"
24982 while read line; do
24983 echo \"$line\"
24984 doneˇ
24985 "});
24986
24987 // test `done` auto outdents when typed inside `for` block
24988 cx.set_state(indoc! {"
24989 for file in *.txt; do
24990 cat \"$file\"
24991 ˇ
24992 "});
24993 cx.update_editor(|editor, window, cx| {
24994 editor.handle_input("done", window, cx);
24995 });
24996 cx.assert_editor_state(indoc! {"
24997 for file in *.txt; do
24998 cat \"$file\"
24999 doneˇ
25000 "});
25001
25002 // test `esac` auto outdents when typed inside `case` block
25003 cx.set_state(indoc! {"
25004 case \"$1\" in
25005 start)
25006 echo \"foo bar\"
25007 ;;
25008 stop)
25009 echo \"bar baz\"
25010 ;;
25011 ˇ
25012 "});
25013 cx.update_editor(|editor, window, cx| {
25014 editor.handle_input("esac", window, cx);
25015 });
25016 cx.assert_editor_state(indoc! {"
25017 case \"$1\" in
25018 start)
25019 echo \"foo bar\"
25020 ;;
25021 stop)
25022 echo \"bar baz\"
25023 ;;
25024 esacˇ
25025 "});
25026
25027 // test `*)` auto outdents when typed inside `case` block
25028 cx.set_state(indoc! {"
25029 case \"$1\" in
25030 start)
25031 echo \"foo bar\"
25032 ;;
25033 ˇ
25034 "});
25035 cx.update_editor(|editor, window, cx| {
25036 editor.handle_input("*)", window, cx);
25037 });
25038 cx.assert_editor_state(indoc! {"
25039 case \"$1\" in
25040 start)
25041 echo \"foo bar\"
25042 ;;
25043 *)ˇ
25044 "});
25045
25046 // test `fi` outdents to correct level with nested if blocks
25047 cx.set_state(indoc! {"
25048 if [ \"$1\" = \"test\" ]; then
25049 echo \"outer if\"
25050 if [ \"$2\" = \"debug\" ]; then
25051 echo \"inner if\"
25052 ˇ
25053 "});
25054 cx.update_editor(|editor, window, cx| {
25055 editor.handle_input("fi", window, cx);
25056 });
25057 cx.assert_editor_state(indoc! {"
25058 if [ \"$1\" = \"test\" ]; then
25059 echo \"outer if\"
25060 if [ \"$2\" = \"debug\" ]; then
25061 echo \"inner if\"
25062 fiˇ
25063 "});
25064}
25065
25066#[gpui::test]
25067async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25068 init_test(cx, |_| {});
25069 update_test_language_settings(cx, |settings| {
25070 settings.defaults.extend_comment_on_newline = Some(false);
25071 });
25072 let mut cx = EditorTestContext::new(cx).await;
25073 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25074 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25075
25076 // test correct indent after newline on comment
25077 cx.set_state(indoc! {"
25078 # COMMENT:ˇ
25079 "});
25080 cx.update_editor(|editor, window, cx| {
25081 editor.newline(&Newline, window, cx);
25082 });
25083 cx.assert_editor_state(indoc! {"
25084 # COMMENT:
25085 ˇ
25086 "});
25087
25088 // test correct indent after newline after `then`
25089 cx.set_state(indoc! {"
25090
25091 if [ \"$1\" = \"test\" ]; thenˇ
25092 "});
25093 cx.update_editor(|editor, window, cx| {
25094 editor.newline(&Newline, window, cx);
25095 });
25096 cx.run_until_parked();
25097 cx.assert_editor_state(indoc! {"
25098
25099 if [ \"$1\" = \"test\" ]; then
25100 ˇ
25101 "});
25102
25103 // test correct indent after newline after `else`
25104 cx.set_state(indoc! {"
25105 if [ \"$1\" = \"test\" ]; then
25106 elseˇ
25107 "});
25108 cx.update_editor(|editor, window, cx| {
25109 editor.newline(&Newline, window, cx);
25110 });
25111 cx.run_until_parked();
25112 cx.assert_editor_state(indoc! {"
25113 if [ \"$1\" = \"test\" ]; then
25114 else
25115 ˇ
25116 "});
25117
25118 // test correct indent after newline after `elif`
25119 cx.set_state(indoc! {"
25120 if [ \"$1\" = \"test\" ]; then
25121 elifˇ
25122 "});
25123 cx.update_editor(|editor, window, cx| {
25124 editor.newline(&Newline, window, cx);
25125 });
25126 cx.run_until_parked();
25127 cx.assert_editor_state(indoc! {"
25128 if [ \"$1\" = \"test\" ]; then
25129 elif
25130 ˇ
25131 "});
25132
25133 // test correct indent after newline after `do`
25134 cx.set_state(indoc! {"
25135 for file in *.txt; doˇ
25136 "});
25137 cx.update_editor(|editor, window, cx| {
25138 editor.newline(&Newline, window, cx);
25139 });
25140 cx.run_until_parked();
25141 cx.assert_editor_state(indoc! {"
25142 for file in *.txt; do
25143 ˇ
25144 "});
25145
25146 // test correct indent after newline after case pattern
25147 cx.set_state(indoc! {"
25148 case \"$1\" in
25149 start)ˇ
25150 "});
25151 cx.update_editor(|editor, window, cx| {
25152 editor.newline(&Newline, window, cx);
25153 });
25154 cx.run_until_parked();
25155 cx.assert_editor_state(indoc! {"
25156 case \"$1\" in
25157 start)
25158 ˇ
25159 "});
25160
25161 // test correct indent after newline after case pattern
25162 cx.set_state(indoc! {"
25163 case \"$1\" in
25164 start)
25165 ;;
25166 *)ˇ
25167 "});
25168 cx.update_editor(|editor, window, cx| {
25169 editor.newline(&Newline, window, cx);
25170 });
25171 cx.run_until_parked();
25172 cx.assert_editor_state(indoc! {"
25173 case \"$1\" in
25174 start)
25175 ;;
25176 *)
25177 ˇ
25178 "});
25179
25180 // test correct indent after newline after function opening brace
25181 cx.set_state(indoc! {"
25182 function test() {ˇ}
25183 "});
25184 cx.update_editor(|editor, window, cx| {
25185 editor.newline(&Newline, window, cx);
25186 });
25187 cx.run_until_parked();
25188 cx.assert_editor_state(indoc! {"
25189 function test() {
25190 ˇ
25191 }
25192 "});
25193
25194 // test no extra indent after semicolon on same line
25195 cx.set_state(indoc! {"
25196 echo \"test\";ˇ
25197 "});
25198 cx.update_editor(|editor, window, cx| {
25199 editor.newline(&Newline, window, cx);
25200 });
25201 cx.run_until_parked();
25202 cx.assert_editor_state(indoc! {"
25203 echo \"test\";
25204 ˇ
25205 "});
25206}
25207
25208fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25209 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25210 point..point
25211}
25212
25213#[track_caller]
25214fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25215 let (text, ranges) = marked_text_ranges(marked_text, true);
25216 assert_eq!(editor.text(cx), text);
25217 assert_eq!(
25218 editor.selections.ranges(cx),
25219 ranges,
25220 "Assert selections are {}",
25221 marked_text
25222 );
25223}
25224
25225pub fn handle_signature_help_request(
25226 cx: &mut EditorLspTestContext,
25227 mocked_response: lsp::SignatureHelp,
25228) -> impl Future<Output = ()> + use<> {
25229 let mut request =
25230 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25231 let mocked_response = mocked_response.clone();
25232 async move { Ok(Some(mocked_response)) }
25233 });
25234
25235 async move {
25236 request.next().await;
25237 }
25238}
25239
25240#[track_caller]
25241pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25242 cx.update_editor(|editor, _, _| {
25243 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25244 let entries = menu.entries.borrow();
25245 let entries = entries
25246 .iter()
25247 .map(|entry| entry.string.as_str())
25248 .collect::<Vec<_>>();
25249 assert_eq!(entries, expected);
25250 } else {
25251 panic!("Expected completions menu");
25252 }
25253 });
25254}
25255
25256/// Handle completion request passing a marked string specifying where the completion
25257/// should be triggered from using '|' character, what range should be replaced, and what completions
25258/// should be returned using '<' and '>' to delimit the range.
25259///
25260/// Also see `handle_completion_request_with_insert_and_replace`.
25261#[track_caller]
25262pub fn handle_completion_request(
25263 marked_string: &str,
25264 completions: Vec<&'static str>,
25265 is_incomplete: bool,
25266 counter: Arc<AtomicUsize>,
25267 cx: &mut EditorLspTestContext,
25268) -> impl Future<Output = ()> {
25269 let complete_from_marker: TextRangeMarker = '|'.into();
25270 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25271 let (_, mut marked_ranges) = marked_text_ranges_by(
25272 marked_string,
25273 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25274 );
25275
25276 let complete_from_position =
25277 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25278 let replace_range =
25279 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25280
25281 let mut request =
25282 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25283 let completions = completions.clone();
25284 counter.fetch_add(1, atomic::Ordering::Release);
25285 async move {
25286 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25287 assert_eq!(
25288 params.text_document_position.position,
25289 complete_from_position
25290 );
25291 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25292 is_incomplete,
25293 item_defaults: None,
25294 items: completions
25295 .iter()
25296 .map(|completion_text| lsp::CompletionItem {
25297 label: completion_text.to_string(),
25298 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25299 range: replace_range,
25300 new_text: completion_text.to_string(),
25301 })),
25302 ..Default::default()
25303 })
25304 .collect(),
25305 })))
25306 }
25307 });
25308
25309 async move {
25310 request.next().await;
25311 }
25312}
25313
25314/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25315/// given instead, which also contains an `insert` range.
25316///
25317/// This function uses markers to define ranges:
25318/// - `|` marks the cursor position
25319/// - `<>` marks the replace range
25320/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25321pub fn handle_completion_request_with_insert_and_replace(
25322 cx: &mut EditorLspTestContext,
25323 marked_string: &str,
25324 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25325 counter: Arc<AtomicUsize>,
25326) -> impl Future<Output = ()> {
25327 let complete_from_marker: TextRangeMarker = '|'.into();
25328 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25329 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25330
25331 let (_, mut marked_ranges) = marked_text_ranges_by(
25332 marked_string,
25333 vec![
25334 complete_from_marker.clone(),
25335 replace_range_marker.clone(),
25336 insert_range_marker.clone(),
25337 ],
25338 );
25339
25340 let complete_from_position =
25341 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25342 let replace_range =
25343 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25344
25345 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25346 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25347 _ => lsp::Range {
25348 start: replace_range.start,
25349 end: complete_from_position,
25350 },
25351 };
25352
25353 let mut request =
25354 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25355 let completions = completions.clone();
25356 counter.fetch_add(1, atomic::Ordering::Release);
25357 async move {
25358 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25359 assert_eq!(
25360 params.text_document_position.position, complete_from_position,
25361 "marker `|` position doesn't match",
25362 );
25363 Ok(Some(lsp::CompletionResponse::Array(
25364 completions
25365 .iter()
25366 .map(|(label, new_text)| lsp::CompletionItem {
25367 label: label.to_string(),
25368 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25369 lsp::InsertReplaceEdit {
25370 insert: insert_range,
25371 replace: replace_range,
25372 new_text: new_text.to_string(),
25373 },
25374 )),
25375 ..Default::default()
25376 })
25377 .collect(),
25378 )))
25379 }
25380 });
25381
25382 async move {
25383 request.next().await;
25384 }
25385}
25386
25387fn handle_resolve_completion_request(
25388 cx: &mut EditorLspTestContext,
25389 edits: Option<Vec<(&'static str, &'static str)>>,
25390) -> impl Future<Output = ()> {
25391 let edits = edits.map(|edits| {
25392 edits
25393 .iter()
25394 .map(|(marked_string, new_text)| {
25395 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25396 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25397 lsp::TextEdit::new(replace_range, new_text.to_string())
25398 })
25399 .collect::<Vec<_>>()
25400 });
25401
25402 let mut request =
25403 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25404 let edits = edits.clone();
25405 async move {
25406 Ok(lsp::CompletionItem {
25407 additional_text_edits: edits,
25408 ..Default::default()
25409 })
25410 }
25411 });
25412
25413 async move {
25414 request.next().await;
25415 }
25416}
25417
25418pub(crate) fn update_test_language_settings(
25419 cx: &mut TestAppContext,
25420 f: impl Fn(&mut AllLanguageSettingsContent),
25421) {
25422 cx.update(|cx| {
25423 SettingsStore::update_global(cx, |store, cx| {
25424 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25425 });
25426 });
25427}
25428
25429pub(crate) fn update_test_project_settings(
25430 cx: &mut TestAppContext,
25431 f: impl Fn(&mut ProjectSettingsContent),
25432) {
25433 cx.update(|cx| {
25434 SettingsStore::update_global(cx, |store, cx| {
25435 store.update_user_settings(cx, |settings| f(&mut settings.project));
25436 });
25437 });
25438}
25439
25440pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25441 cx.update(|cx| {
25442 assets::Assets.load_test_fonts(cx);
25443 let store = SettingsStore::test(cx);
25444 cx.set_global(store);
25445 theme::init(theme::LoadThemes::JustBase, cx);
25446 release_channel::init(SemanticVersion::default(), cx);
25447 client::init_settings(cx);
25448 language::init(cx);
25449 Project::init_settings(cx);
25450 workspace::init_settings(cx);
25451 crate::init(cx);
25452 });
25453 zlog::init_test();
25454 update_test_language_settings(cx, f);
25455}
25456
25457#[track_caller]
25458fn assert_hunk_revert(
25459 not_reverted_text_with_selections: &str,
25460 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25461 expected_reverted_text_with_selections: &str,
25462 base_text: &str,
25463 cx: &mut EditorLspTestContext,
25464) {
25465 cx.set_state(not_reverted_text_with_selections);
25466 cx.set_head_text(base_text);
25467 cx.executor().run_until_parked();
25468
25469 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25470 let snapshot = editor.snapshot(window, cx);
25471 let reverted_hunk_statuses = snapshot
25472 .buffer_snapshot()
25473 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25474 .map(|hunk| hunk.status().kind)
25475 .collect::<Vec<_>>();
25476
25477 editor.git_restore(&Default::default(), window, cx);
25478 reverted_hunk_statuses
25479 });
25480 cx.executor().run_until_parked();
25481 cx.assert_editor_state(expected_reverted_text_with_selections);
25482 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25483}
25484
25485#[gpui::test(iterations = 10)]
25486async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25487 init_test(cx, |_| {});
25488
25489 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25490 let counter = diagnostic_requests.clone();
25491
25492 let fs = FakeFs::new(cx.executor());
25493 fs.insert_tree(
25494 path!("/a"),
25495 json!({
25496 "first.rs": "fn main() { let a = 5; }",
25497 "second.rs": "// Test file",
25498 }),
25499 )
25500 .await;
25501
25502 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25503 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25504 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25505
25506 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25507 language_registry.add(rust_lang());
25508 let mut fake_servers = language_registry.register_fake_lsp(
25509 "Rust",
25510 FakeLspAdapter {
25511 capabilities: lsp::ServerCapabilities {
25512 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25513 lsp::DiagnosticOptions {
25514 identifier: None,
25515 inter_file_dependencies: true,
25516 workspace_diagnostics: true,
25517 work_done_progress_options: Default::default(),
25518 },
25519 )),
25520 ..Default::default()
25521 },
25522 ..Default::default()
25523 },
25524 );
25525
25526 let editor = workspace
25527 .update(cx, |workspace, window, cx| {
25528 workspace.open_abs_path(
25529 PathBuf::from(path!("/a/first.rs")),
25530 OpenOptions::default(),
25531 window,
25532 cx,
25533 )
25534 })
25535 .unwrap()
25536 .await
25537 .unwrap()
25538 .downcast::<Editor>()
25539 .unwrap();
25540 let fake_server = fake_servers.next().await.unwrap();
25541 let server_id = fake_server.server.server_id();
25542 let mut first_request = fake_server
25543 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25544 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25545 let result_id = Some(new_result_id.to_string());
25546 assert_eq!(
25547 params.text_document.uri,
25548 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25549 );
25550 async move {
25551 Ok(lsp::DocumentDiagnosticReportResult::Report(
25552 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25553 related_documents: None,
25554 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25555 items: Vec::new(),
25556 result_id,
25557 },
25558 }),
25559 ))
25560 }
25561 });
25562
25563 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25564 project.update(cx, |project, cx| {
25565 let buffer_id = editor
25566 .read(cx)
25567 .buffer()
25568 .read(cx)
25569 .as_singleton()
25570 .expect("created a singleton buffer")
25571 .read(cx)
25572 .remote_id();
25573 let buffer_result_id = project
25574 .lsp_store()
25575 .read(cx)
25576 .result_id(server_id, buffer_id, cx);
25577 assert_eq!(expected, buffer_result_id);
25578 });
25579 };
25580
25581 ensure_result_id(None, cx);
25582 cx.executor().advance_clock(Duration::from_millis(60));
25583 cx.executor().run_until_parked();
25584 assert_eq!(
25585 diagnostic_requests.load(atomic::Ordering::Acquire),
25586 1,
25587 "Opening file should trigger diagnostic request"
25588 );
25589 first_request
25590 .next()
25591 .await
25592 .expect("should have sent the first diagnostics pull request");
25593 ensure_result_id(Some("1".to_string()), cx);
25594
25595 // Editing should trigger diagnostics
25596 editor.update_in(cx, |editor, window, cx| {
25597 editor.handle_input("2", window, cx)
25598 });
25599 cx.executor().advance_clock(Duration::from_millis(60));
25600 cx.executor().run_until_parked();
25601 assert_eq!(
25602 diagnostic_requests.load(atomic::Ordering::Acquire),
25603 2,
25604 "Editing should trigger diagnostic request"
25605 );
25606 ensure_result_id(Some("2".to_string()), cx);
25607
25608 // Moving cursor should not trigger diagnostic request
25609 editor.update_in(cx, |editor, window, cx| {
25610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25611 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25612 });
25613 });
25614 cx.executor().advance_clock(Duration::from_millis(60));
25615 cx.executor().run_until_parked();
25616 assert_eq!(
25617 diagnostic_requests.load(atomic::Ordering::Acquire),
25618 2,
25619 "Cursor movement should not trigger diagnostic request"
25620 );
25621 ensure_result_id(Some("2".to_string()), cx);
25622 // Multiple rapid edits should be debounced
25623 for _ in 0..5 {
25624 editor.update_in(cx, |editor, window, cx| {
25625 editor.handle_input("x", window, cx)
25626 });
25627 }
25628 cx.executor().advance_clock(Duration::from_millis(60));
25629 cx.executor().run_until_parked();
25630
25631 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25632 assert!(
25633 final_requests <= 4,
25634 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25635 );
25636 ensure_result_id(Some(final_requests.to_string()), cx);
25637}
25638
25639#[gpui::test]
25640async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25641 // Regression test for issue #11671
25642 // Previously, adding a cursor after moving multiple cursors would reset
25643 // the cursor count instead of adding to the existing cursors.
25644 init_test(cx, |_| {});
25645 let mut cx = EditorTestContext::new(cx).await;
25646
25647 // Create a simple buffer with cursor at start
25648 cx.set_state(indoc! {"
25649 ˇaaaa
25650 bbbb
25651 cccc
25652 dddd
25653 eeee
25654 ffff
25655 gggg
25656 hhhh"});
25657
25658 // Add 2 cursors below (so we have 3 total)
25659 cx.update_editor(|editor, window, cx| {
25660 editor.add_selection_below(&Default::default(), window, cx);
25661 editor.add_selection_below(&Default::default(), window, cx);
25662 });
25663
25664 // Verify we have 3 cursors
25665 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25666 assert_eq!(
25667 initial_count, 3,
25668 "Should have 3 cursors after adding 2 below"
25669 );
25670
25671 // Move down one line
25672 cx.update_editor(|editor, window, cx| {
25673 editor.move_down(&MoveDown, window, cx);
25674 });
25675
25676 // Add another cursor below
25677 cx.update_editor(|editor, window, cx| {
25678 editor.add_selection_below(&Default::default(), window, cx);
25679 });
25680
25681 // Should now have 4 cursors (3 original + 1 new)
25682 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25683 assert_eq!(
25684 final_count, 4,
25685 "Should have 4 cursors after moving and adding another"
25686 );
25687}
25688
25689#[gpui::test(iterations = 10)]
25690async fn test_document_colors(cx: &mut TestAppContext) {
25691 let expected_color = Rgba {
25692 r: 0.33,
25693 g: 0.33,
25694 b: 0.33,
25695 a: 0.33,
25696 };
25697
25698 init_test(cx, |_| {});
25699
25700 let fs = FakeFs::new(cx.executor());
25701 fs.insert_tree(
25702 path!("/a"),
25703 json!({
25704 "first.rs": "fn main() { let a = 5; }",
25705 }),
25706 )
25707 .await;
25708
25709 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25711 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25712
25713 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25714 language_registry.add(rust_lang());
25715 let mut fake_servers = language_registry.register_fake_lsp(
25716 "Rust",
25717 FakeLspAdapter {
25718 capabilities: lsp::ServerCapabilities {
25719 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25720 ..lsp::ServerCapabilities::default()
25721 },
25722 name: "rust-analyzer",
25723 ..FakeLspAdapter::default()
25724 },
25725 );
25726 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25727 "Rust",
25728 FakeLspAdapter {
25729 capabilities: lsp::ServerCapabilities {
25730 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25731 ..lsp::ServerCapabilities::default()
25732 },
25733 name: "not-rust-analyzer",
25734 ..FakeLspAdapter::default()
25735 },
25736 );
25737
25738 let editor = workspace
25739 .update(cx, |workspace, window, cx| {
25740 workspace.open_abs_path(
25741 PathBuf::from(path!("/a/first.rs")),
25742 OpenOptions::default(),
25743 window,
25744 cx,
25745 )
25746 })
25747 .unwrap()
25748 .await
25749 .unwrap()
25750 .downcast::<Editor>()
25751 .unwrap();
25752 let fake_language_server = fake_servers.next().await.unwrap();
25753 let fake_language_server_without_capabilities =
25754 fake_servers_without_capabilities.next().await.unwrap();
25755 let requests_made = Arc::new(AtomicUsize::new(0));
25756 let closure_requests_made = Arc::clone(&requests_made);
25757 let mut color_request_handle = fake_language_server
25758 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25759 let requests_made = Arc::clone(&closure_requests_made);
25760 async move {
25761 assert_eq!(
25762 params.text_document.uri,
25763 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25764 );
25765 requests_made.fetch_add(1, atomic::Ordering::Release);
25766 Ok(vec![
25767 lsp::ColorInformation {
25768 range: lsp::Range {
25769 start: lsp::Position {
25770 line: 0,
25771 character: 0,
25772 },
25773 end: lsp::Position {
25774 line: 0,
25775 character: 1,
25776 },
25777 },
25778 color: lsp::Color {
25779 red: 0.33,
25780 green: 0.33,
25781 blue: 0.33,
25782 alpha: 0.33,
25783 },
25784 },
25785 lsp::ColorInformation {
25786 range: lsp::Range {
25787 start: lsp::Position {
25788 line: 0,
25789 character: 0,
25790 },
25791 end: lsp::Position {
25792 line: 0,
25793 character: 1,
25794 },
25795 },
25796 color: lsp::Color {
25797 red: 0.33,
25798 green: 0.33,
25799 blue: 0.33,
25800 alpha: 0.33,
25801 },
25802 },
25803 ])
25804 }
25805 });
25806
25807 let _handle = fake_language_server_without_capabilities
25808 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25809 panic!("Should not be called");
25810 });
25811 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25812 color_request_handle.next().await.unwrap();
25813 cx.run_until_parked();
25814 assert_eq!(
25815 1,
25816 requests_made.load(atomic::Ordering::Acquire),
25817 "Should query for colors once per editor open"
25818 );
25819 editor.update_in(cx, |editor, _, cx| {
25820 assert_eq!(
25821 vec![expected_color],
25822 extract_color_inlays(editor, cx),
25823 "Should have an initial inlay"
25824 );
25825 });
25826
25827 // opening another file in a split should not influence the LSP query counter
25828 workspace
25829 .update(cx, |workspace, window, cx| {
25830 assert_eq!(
25831 workspace.panes().len(),
25832 1,
25833 "Should have one pane with one editor"
25834 );
25835 workspace.move_item_to_pane_in_direction(
25836 &MoveItemToPaneInDirection {
25837 direction: SplitDirection::Right,
25838 focus: false,
25839 clone: true,
25840 },
25841 window,
25842 cx,
25843 );
25844 })
25845 .unwrap();
25846 cx.run_until_parked();
25847 workspace
25848 .update(cx, |workspace, _, cx| {
25849 let panes = workspace.panes();
25850 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25851 for pane in panes {
25852 let editor = pane
25853 .read(cx)
25854 .active_item()
25855 .and_then(|item| item.downcast::<Editor>())
25856 .expect("Should have opened an editor in each split");
25857 let editor_file = editor
25858 .read(cx)
25859 .buffer()
25860 .read(cx)
25861 .as_singleton()
25862 .expect("test deals with singleton buffers")
25863 .read(cx)
25864 .file()
25865 .expect("test buffese should have a file")
25866 .path();
25867 assert_eq!(
25868 editor_file.as_ref(),
25869 rel_path("first.rs"),
25870 "Both editors should be opened for the same file"
25871 )
25872 }
25873 })
25874 .unwrap();
25875
25876 cx.executor().advance_clock(Duration::from_millis(500));
25877 let save = editor.update_in(cx, |editor, window, cx| {
25878 editor.move_to_end(&MoveToEnd, window, cx);
25879 editor.handle_input("dirty", window, cx);
25880 editor.save(
25881 SaveOptions {
25882 format: true,
25883 autosave: true,
25884 },
25885 project.clone(),
25886 window,
25887 cx,
25888 )
25889 });
25890 save.await.unwrap();
25891
25892 color_request_handle.next().await.unwrap();
25893 cx.run_until_parked();
25894 assert_eq!(
25895 2,
25896 requests_made.load(atomic::Ordering::Acquire),
25897 "Should query for colors once per save (deduplicated) and once per formatting after save"
25898 );
25899
25900 drop(editor);
25901 let close = workspace
25902 .update(cx, |workspace, window, cx| {
25903 workspace.active_pane().update(cx, |pane, cx| {
25904 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25905 })
25906 })
25907 .unwrap();
25908 close.await.unwrap();
25909 let close = workspace
25910 .update(cx, |workspace, window, cx| {
25911 workspace.active_pane().update(cx, |pane, cx| {
25912 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25913 })
25914 })
25915 .unwrap();
25916 close.await.unwrap();
25917 assert_eq!(
25918 2,
25919 requests_made.load(atomic::Ordering::Acquire),
25920 "After saving and closing all editors, no extra requests should be made"
25921 );
25922 workspace
25923 .update(cx, |workspace, _, cx| {
25924 assert!(
25925 workspace.active_item(cx).is_none(),
25926 "Should close all editors"
25927 )
25928 })
25929 .unwrap();
25930
25931 workspace
25932 .update(cx, |workspace, window, cx| {
25933 workspace.active_pane().update(cx, |pane, cx| {
25934 pane.navigate_backward(&workspace::GoBack, window, cx);
25935 })
25936 })
25937 .unwrap();
25938 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25939 cx.run_until_parked();
25940 let editor = workspace
25941 .update(cx, |workspace, _, cx| {
25942 workspace
25943 .active_item(cx)
25944 .expect("Should have reopened the editor again after navigating back")
25945 .downcast::<Editor>()
25946 .expect("Should be an editor")
25947 })
25948 .unwrap();
25949
25950 assert_eq!(
25951 2,
25952 requests_made.load(atomic::Ordering::Acquire),
25953 "Cache should be reused on buffer close and reopen"
25954 );
25955 editor.update(cx, |editor, cx| {
25956 assert_eq!(
25957 vec![expected_color],
25958 extract_color_inlays(editor, cx),
25959 "Should have an initial inlay"
25960 );
25961 });
25962
25963 drop(color_request_handle);
25964 let closure_requests_made = Arc::clone(&requests_made);
25965 let mut empty_color_request_handle = fake_language_server
25966 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25967 let requests_made = Arc::clone(&closure_requests_made);
25968 async move {
25969 assert_eq!(
25970 params.text_document.uri,
25971 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25972 );
25973 requests_made.fetch_add(1, atomic::Ordering::Release);
25974 Ok(Vec::new())
25975 }
25976 });
25977 let save = editor.update_in(cx, |editor, window, cx| {
25978 editor.move_to_end(&MoveToEnd, window, cx);
25979 editor.handle_input("dirty_again", window, cx);
25980 editor.save(
25981 SaveOptions {
25982 format: false,
25983 autosave: true,
25984 },
25985 project.clone(),
25986 window,
25987 cx,
25988 )
25989 });
25990 save.await.unwrap();
25991
25992 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25993 empty_color_request_handle.next().await.unwrap();
25994 cx.run_until_parked();
25995 assert_eq!(
25996 3,
25997 requests_made.load(atomic::Ordering::Acquire),
25998 "Should query for colors once per save only, as formatting was not requested"
25999 );
26000 editor.update(cx, |editor, cx| {
26001 assert_eq!(
26002 Vec::<Rgba>::new(),
26003 extract_color_inlays(editor, cx),
26004 "Should clear all colors when the server returns an empty response"
26005 );
26006 });
26007}
26008
26009#[gpui::test]
26010async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26011 init_test(cx, |_| {});
26012 let (editor, cx) = cx.add_window_view(Editor::single_line);
26013 editor.update_in(cx, |editor, window, cx| {
26014 editor.set_text("oops\n\nwow\n", window, cx)
26015 });
26016 cx.run_until_parked();
26017 editor.update(cx, |editor, cx| {
26018 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26019 });
26020 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26021 cx.run_until_parked();
26022 editor.update(cx, |editor, cx| {
26023 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26024 });
26025}
26026
26027#[gpui::test]
26028async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26029 init_test(cx, |_| {});
26030
26031 cx.update(|cx| {
26032 register_project_item::<Editor>(cx);
26033 });
26034
26035 let fs = FakeFs::new(cx.executor());
26036 fs.insert_tree("/root1", json!({})).await;
26037 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26038 .await;
26039
26040 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26041 let (workspace, cx) =
26042 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26043
26044 let worktree_id = project.update(cx, |project, cx| {
26045 project.worktrees(cx).next().unwrap().read(cx).id()
26046 });
26047
26048 let handle = workspace
26049 .update_in(cx, |workspace, window, cx| {
26050 let project_path = (worktree_id, rel_path("one.pdf"));
26051 workspace.open_path(project_path, None, true, window, cx)
26052 })
26053 .await
26054 .unwrap();
26055
26056 assert_eq!(
26057 handle.to_any().entity_type(),
26058 TypeId::of::<InvalidBufferView>()
26059 );
26060}
26061
26062#[gpui::test]
26063async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26064 init_test(cx, |_| {});
26065
26066 let language = Arc::new(Language::new(
26067 LanguageConfig::default(),
26068 Some(tree_sitter_rust::LANGUAGE.into()),
26069 ));
26070
26071 // Test hierarchical sibling navigation
26072 let text = r#"
26073 fn outer() {
26074 if condition {
26075 let a = 1;
26076 }
26077 let b = 2;
26078 }
26079
26080 fn another() {
26081 let c = 3;
26082 }
26083 "#;
26084
26085 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26086 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26087 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26088
26089 // Wait for parsing to complete
26090 editor
26091 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26092 .await;
26093
26094 editor.update_in(cx, |editor, window, cx| {
26095 // Start by selecting "let a = 1;" inside the if block
26096 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26097 s.select_display_ranges([
26098 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26099 ]);
26100 });
26101
26102 let initial_selection = editor.selections.display_ranges(cx);
26103 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26104
26105 // Test select next sibling - should move up levels to find the next sibling
26106 // Since "let a = 1;" has no siblings in the if block, it should move up
26107 // to find "let b = 2;" which is a sibling of the if block
26108 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26109 let next_selection = editor.selections.display_ranges(cx);
26110
26111 // Should have a selection and it should be different from the initial
26112 assert_eq!(
26113 next_selection.len(),
26114 1,
26115 "Should have one selection after next"
26116 );
26117 assert_ne!(
26118 next_selection[0], initial_selection[0],
26119 "Next sibling selection should be different"
26120 );
26121
26122 // Test hierarchical navigation by going to the end of the current function
26123 // and trying to navigate to the next function
26124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26125 s.select_display_ranges([
26126 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26127 ]);
26128 });
26129
26130 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26131 let function_next_selection = editor.selections.display_ranges(cx);
26132
26133 // Should move to the next function
26134 assert_eq!(
26135 function_next_selection.len(),
26136 1,
26137 "Should have one selection after function next"
26138 );
26139
26140 // Test select previous sibling navigation
26141 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26142 let prev_selection = editor.selections.display_ranges(cx);
26143
26144 // Should have a selection and it should be different
26145 assert_eq!(
26146 prev_selection.len(),
26147 1,
26148 "Should have one selection after prev"
26149 );
26150 assert_ne!(
26151 prev_selection[0], function_next_selection[0],
26152 "Previous sibling selection should be different from next"
26153 );
26154 });
26155}
26156
26157#[gpui::test]
26158async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26159 init_test(cx, |_| {});
26160
26161 let mut cx = EditorTestContext::new(cx).await;
26162 cx.set_state(
26163 "let ˇvariable = 42;
26164let another = variable + 1;
26165let result = variable * 2;",
26166 );
26167
26168 // Set up document highlights manually (simulating LSP response)
26169 cx.update_editor(|editor, _window, cx| {
26170 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26171
26172 // Create highlights for "variable" occurrences
26173 let highlight_ranges = [
26174 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26175 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26176 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26177 ];
26178
26179 let anchor_ranges: Vec<_> = highlight_ranges
26180 .iter()
26181 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26182 .collect();
26183
26184 editor.highlight_background::<DocumentHighlightRead>(
26185 &anchor_ranges,
26186 |theme| theme.colors().editor_document_highlight_read_background,
26187 cx,
26188 );
26189 });
26190
26191 // Go to next highlight - should move to second "variable"
26192 cx.update_editor(|editor, window, cx| {
26193 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26194 });
26195 cx.assert_editor_state(
26196 "let variable = 42;
26197let another = ˇvariable + 1;
26198let result = variable * 2;",
26199 );
26200
26201 // Go to next highlight - should move to third "variable"
26202 cx.update_editor(|editor, window, cx| {
26203 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26204 });
26205 cx.assert_editor_state(
26206 "let variable = 42;
26207let another = variable + 1;
26208let result = ˇvariable * 2;",
26209 );
26210
26211 // Go to next highlight - should stay at third "variable" (no wrap-around)
26212 cx.update_editor(|editor, window, cx| {
26213 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26214 });
26215 cx.assert_editor_state(
26216 "let variable = 42;
26217let another = variable + 1;
26218let result = ˇvariable * 2;",
26219 );
26220
26221 // Now test going backwards from third position
26222 cx.update_editor(|editor, window, cx| {
26223 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26224 });
26225 cx.assert_editor_state(
26226 "let variable = 42;
26227let another = ˇvariable + 1;
26228let result = variable * 2;",
26229 );
26230
26231 // Go to previous highlight - should move to first "variable"
26232 cx.update_editor(|editor, window, cx| {
26233 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26234 });
26235 cx.assert_editor_state(
26236 "let ˇvariable = 42;
26237let another = variable + 1;
26238let result = variable * 2;",
26239 );
26240
26241 // Go to previous highlight - should stay on first "variable"
26242 cx.update_editor(|editor, window, cx| {
26243 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26244 });
26245 cx.assert_editor_state(
26246 "let ˇvariable = 42;
26247let another = variable + 1;
26248let result = variable * 2;",
26249 );
26250}
26251
26252#[gpui::test]
26253async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26254 cx: &mut gpui::TestAppContext,
26255) {
26256 init_test(cx, |_| {});
26257
26258 let url = "https://zed.dev";
26259
26260 let markdown_language = Arc::new(Language::new(
26261 LanguageConfig {
26262 name: "Markdown".into(),
26263 ..LanguageConfig::default()
26264 },
26265 None,
26266 ));
26267
26268 let mut cx = EditorTestContext::new(cx).await;
26269 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26270 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26271
26272 cx.update_editor(|editor, window, cx| {
26273 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26274 editor.paste(&Paste, window, cx);
26275 });
26276
26277 cx.assert_editor_state(&format!(
26278 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26279 ));
26280}
26281
26282#[gpui::test]
26283async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26284 cx: &mut gpui::TestAppContext,
26285) {
26286 init_test(cx, |_| {});
26287
26288 let url = "https://zed.dev";
26289
26290 let markdown_language = Arc::new(Language::new(
26291 LanguageConfig {
26292 name: "Markdown".into(),
26293 ..LanguageConfig::default()
26294 },
26295 None,
26296 ));
26297
26298 let mut cx = EditorTestContext::new(cx).await;
26299 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26300 cx.set_state(&format!(
26301 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26302 ));
26303
26304 cx.update_editor(|editor, window, cx| {
26305 editor.copy(&Copy, window, cx);
26306 });
26307
26308 cx.set_state(&format!(
26309 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26310 ));
26311
26312 cx.update_editor(|editor, window, cx| {
26313 editor.paste(&Paste, window, cx);
26314 });
26315
26316 cx.assert_editor_state(&format!(
26317 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26318 ));
26319}
26320
26321#[gpui::test]
26322async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26323 cx: &mut gpui::TestAppContext,
26324) {
26325 init_test(cx, |_| {});
26326
26327 let url = "https://zed.dev";
26328
26329 let markdown_language = Arc::new(Language::new(
26330 LanguageConfig {
26331 name: "Markdown".into(),
26332 ..LanguageConfig::default()
26333 },
26334 None,
26335 ));
26336
26337 let mut cx = EditorTestContext::new(cx).await;
26338 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26339 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26340
26341 cx.update_editor(|editor, window, cx| {
26342 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26343 editor.paste(&Paste, window, cx);
26344 });
26345
26346 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26347}
26348
26349#[gpui::test]
26350async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26351 cx: &mut gpui::TestAppContext,
26352) {
26353 init_test(cx, |_| {});
26354
26355 let text = "Awesome";
26356
26357 let markdown_language = Arc::new(Language::new(
26358 LanguageConfig {
26359 name: "Markdown".into(),
26360 ..LanguageConfig::default()
26361 },
26362 None,
26363 ));
26364
26365 let mut cx = EditorTestContext::new(cx).await;
26366 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26367 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26368
26369 cx.update_editor(|editor, window, cx| {
26370 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26371 editor.paste(&Paste, window, cx);
26372 });
26373
26374 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26375}
26376
26377#[gpui::test]
26378async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26379 cx: &mut gpui::TestAppContext,
26380) {
26381 init_test(cx, |_| {});
26382
26383 let url = "https://zed.dev";
26384
26385 let markdown_language = Arc::new(Language::new(
26386 LanguageConfig {
26387 name: "Rust".into(),
26388 ..LanguageConfig::default()
26389 },
26390 None,
26391 ));
26392
26393 let mut cx = EditorTestContext::new(cx).await;
26394 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26395 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26396
26397 cx.update_editor(|editor, window, cx| {
26398 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26399 editor.paste(&Paste, window, cx);
26400 });
26401
26402 cx.assert_editor_state(&format!(
26403 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26404 ));
26405}
26406
26407#[gpui::test]
26408async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26409 cx: &mut TestAppContext,
26410) {
26411 init_test(cx, |_| {});
26412
26413 let url = "https://zed.dev";
26414
26415 let markdown_language = Arc::new(Language::new(
26416 LanguageConfig {
26417 name: "Markdown".into(),
26418 ..LanguageConfig::default()
26419 },
26420 None,
26421 ));
26422
26423 let (editor, cx) = cx.add_window_view(|window, cx| {
26424 let multi_buffer = MultiBuffer::build_multi(
26425 [
26426 ("this will embed -> link", vec![Point::row_range(0..1)]),
26427 ("this will replace -> link", vec![Point::row_range(0..1)]),
26428 ],
26429 cx,
26430 );
26431 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26433 s.select_ranges(vec![
26434 Point::new(0, 19)..Point::new(0, 23),
26435 Point::new(1, 21)..Point::new(1, 25),
26436 ])
26437 });
26438 let first_buffer_id = multi_buffer
26439 .read(cx)
26440 .excerpt_buffer_ids()
26441 .into_iter()
26442 .next()
26443 .unwrap();
26444 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26445 first_buffer.update(cx, |buffer, cx| {
26446 buffer.set_language(Some(markdown_language.clone()), cx);
26447 });
26448
26449 editor
26450 });
26451 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26452
26453 cx.update_editor(|editor, window, cx| {
26454 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26455 editor.paste(&Paste, window, cx);
26456 });
26457
26458 cx.assert_editor_state(&format!(
26459 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26460 ));
26461}
26462
26463#[gpui::test]
26464async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26465 init_test(cx, |_| {});
26466
26467 let fs = FakeFs::new(cx.executor());
26468 fs.insert_tree(
26469 path!("/project"),
26470 json!({
26471 "first.rs": "# First Document\nSome content here.",
26472 "second.rs": "Plain text content for second file.",
26473 }),
26474 )
26475 .await;
26476
26477 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26478 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26479 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26480
26481 let language = rust_lang();
26482 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26483 language_registry.add(language.clone());
26484 let mut fake_servers = language_registry.register_fake_lsp(
26485 "Rust",
26486 FakeLspAdapter {
26487 ..FakeLspAdapter::default()
26488 },
26489 );
26490
26491 let buffer1 = project
26492 .update(cx, |project, cx| {
26493 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26494 })
26495 .await
26496 .unwrap();
26497 let buffer2 = project
26498 .update(cx, |project, cx| {
26499 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26500 })
26501 .await
26502 .unwrap();
26503
26504 let multi_buffer = cx.new(|cx| {
26505 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26506 multi_buffer.set_excerpts_for_path(
26507 PathKey::for_buffer(&buffer1, cx),
26508 buffer1.clone(),
26509 [Point::zero()..buffer1.read(cx).max_point()],
26510 3,
26511 cx,
26512 );
26513 multi_buffer.set_excerpts_for_path(
26514 PathKey::for_buffer(&buffer2, cx),
26515 buffer2.clone(),
26516 [Point::zero()..buffer1.read(cx).max_point()],
26517 3,
26518 cx,
26519 );
26520 multi_buffer
26521 });
26522
26523 let (editor, cx) = cx.add_window_view(|window, cx| {
26524 Editor::new(
26525 EditorMode::full(),
26526 multi_buffer,
26527 Some(project.clone()),
26528 window,
26529 cx,
26530 )
26531 });
26532
26533 let fake_language_server = fake_servers.next().await.unwrap();
26534
26535 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26536
26537 let save = editor.update_in(cx, |editor, window, cx| {
26538 assert!(editor.is_dirty(cx));
26539
26540 editor.save(
26541 SaveOptions {
26542 format: true,
26543 autosave: true,
26544 },
26545 project,
26546 window,
26547 cx,
26548 )
26549 });
26550 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26551 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26552 let mut done_edit_rx = Some(done_edit_rx);
26553 let mut start_edit_tx = Some(start_edit_tx);
26554
26555 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26556 start_edit_tx.take().unwrap().send(()).unwrap();
26557 let done_edit_rx = done_edit_rx.take().unwrap();
26558 async move {
26559 done_edit_rx.await.unwrap();
26560 Ok(None)
26561 }
26562 });
26563
26564 start_edit_rx.await.unwrap();
26565 buffer2
26566 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26567 .unwrap();
26568
26569 done_edit_tx.send(()).unwrap();
26570
26571 save.await.unwrap();
26572 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26573}
26574
26575#[track_caller]
26576fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26577 editor
26578 .all_inlays(cx)
26579 .into_iter()
26580 .filter_map(|inlay| inlay.get_color())
26581 .map(Rgba::from)
26582 .collect()
26583}
26584
26585#[gpui::test]
26586fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26587 init_test(cx, |_| {});
26588
26589 let editor = cx.add_window(|window, cx| {
26590 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26591 build_editor(buffer, window, cx)
26592 });
26593
26594 editor
26595 .update(cx, |editor, window, cx| {
26596 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26597 s.select_display_ranges([
26598 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26599 ])
26600 });
26601
26602 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26603
26604 assert_eq!(
26605 editor.display_text(cx),
26606 "line1\nline2\nline2",
26607 "Duplicating last line upward should create duplicate above, not on same line"
26608 );
26609
26610 assert_eq!(
26611 editor.selections.display_ranges(cx),
26612 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26613 "Selection should remain on the original line"
26614 );
26615 })
26616 .unwrap();
26617}
26618
26619#[gpui::test]
26620async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26621 init_test(cx, |_| {});
26622
26623 let mut cx = EditorTestContext::new(cx).await;
26624
26625 cx.set_state("line1\nline2ˇ");
26626
26627 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26628
26629 let clipboard_text = cx
26630 .read_from_clipboard()
26631 .and_then(|item| item.text().as_deref().map(str::to_string));
26632
26633 assert_eq!(
26634 clipboard_text,
26635 Some("line2\n".to_string()),
26636 "Copying a line without trailing newline should include a newline"
26637 );
26638
26639 cx.set_state("line1\nˇ");
26640
26641 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26642
26643 cx.assert_editor_state("line1\nline2\nˇ");
26644}